Adding more bulletin functionality.
This commit is contained in:
@@ -1,14 +1,24 @@
|
|||||||
# packetserver/http/routers/bulletins.py
|
# packetserver/http/routers/bulletins.py
|
||||||
from fastapi import APIRouter, Path, Query, Depends, HTTPException
|
from fastapi import APIRouter, Path, Query, Depends, HTTPException, status, Request, APIRouter as PrefixedRouter
|
||||||
|
from fastapi.responses import HTMLResponse
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import transaction
|
import transaction
|
||||||
|
from pydantic import BaseModel, Field, constr
|
||||||
|
from persistent.list import PersistentList
|
||||||
|
|
||||||
from packetserver.http.dependencies import get_current_http_user
|
from packetserver.http.dependencies import get_current_http_user
|
||||||
from packetserver.http.auth import HttpUser
|
from packetserver.http.auth import HttpUser
|
||||||
|
from packetserver.http.server import templates
|
||||||
from packetserver.server.bulletin import Bulletin # core class
|
from packetserver.server.bulletin import Bulletin # core class
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/v1", tags=["bulletins"])
|
router = APIRouter(prefix="/api/v1", tags=["bulletins"])
|
||||||
|
html_router = APIRouter(tags=["bulletins-html"])
|
||||||
|
|
||||||
|
|
||||||
|
class CreateBulletinRequest(BaseModel):
|
||||||
|
subject: constr(min_length=1, max_length=100) = Field(..., description="Bulletin subject/title")
|
||||||
|
body: constr(min_length=1) = Field(..., description="Bulletin body text")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/bulletins")
|
@router.get("/bulletins")
|
||||||
@@ -71,4 +81,77 @@ async def get_bulletin(
|
|||||||
"updated_at": bull.updated_at.isoformat() + "Z",
|
"updated_at": bull.updated_at.isoformat() + "Z",
|
||||||
}
|
}
|
||||||
|
|
||||||
raise HTTPException(status_code=404, detail="Bulletin not found")
|
raise HTTPException(status_code=404, detail="Bulletin not found")
|
||||||
|
|
||||||
|
from fastapi import status
|
||||||
|
from pydantic import BaseModel, Field, constr
|
||||||
|
# datetime already imported elsewhere, but ensure
|
||||||
|
|
||||||
|
class CreateBulletinRequest(BaseModel):
|
||||||
|
subject: constr(min_length=1, max_length=100) = Field(..., description="Bulletin subject/title")
|
||||||
|
body: constr(min_length=1) = Field(..., description="Bulletin body text")
|
||||||
|
|
||||||
|
@router.post("/bulletins", status_code=status.HTTP_201_CREATED)
|
||||||
|
async def create_bulletin(
|
||||||
|
payload: CreateBulletinRequest,
|
||||||
|
current_user: HttpUser = Depends(get_current_http_user)
|
||||||
|
):
|
||||||
|
from packetserver.runners.http_server import get_db_connection
|
||||||
|
from packetserver.server.bulletin import Bulletin
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
root = conn.root()
|
||||||
|
|
||||||
|
# Ensure the bulletins list and counter exist
|
||||||
|
if 'bulletins' not in root:
|
||||||
|
root['bulletins'] = persistent.list.PersistentList()
|
||||||
|
|
||||||
|
new_bulletin = Bulletin(
|
||||||
|
author=current_user.username, # uppercase callsign
|
||||||
|
subject=payload.subject.strip(),
|
||||||
|
text=payload.body.strip() # note: 'text' param, assigned to body
|
||||||
|
)
|
||||||
|
|
||||||
|
# This assigns the ID, sets timestamps (if not already), and appends
|
||||||
|
new_id = new_bulletin.write_new(root)
|
||||||
|
|
||||||
|
transaction.commit()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": new_id, # or new_bulletin.id
|
||||||
|
"author": new_bulletin.author,
|
||||||
|
"subject": new_bulletin.subject,
|
||||||
|
"body": new_bulletin.body,
|
||||||
|
"created_at": new_bulletin.created_at.isoformat() + "Z",
|
||||||
|
"updated_at": new_bulletin.updated_at.isoformat() + "Z",
|
||||||
|
}
|
||||||
|
|
||||||
|
@html_router.get("/bulletins", response_class=HTMLResponse)
|
||||||
|
async def bulletin_list_page(
|
||||||
|
request: Request,
|
||||||
|
limit: Optional[int] = Query(50, le=100),
|
||||||
|
current_user: Optional[HttpUser] = Depends(get_current_http_user, use_cache=False) # optional auth
|
||||||
|
):
|
||||||
|
# Internal call to existing API function
|
||||||
|
api_resp = await list_bulletins(limit=limit, since=None)
|
||||||
|
bulletins = api_resp["bulletins"]
|
||||||
|
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"bulletin_list.html",
|
||||||
|
{"request": request, "bulletins": bulletins, "current_user": current_user.username if current_user else None}
|
||||||
|
)
|
||||||
|
|
||||||
|
@html_router.get("/bulletins/{bid}", response_class=HTMLResponse)
|
||||||
|
async def bulletin_detail_page(
|
||||||
|
request: Request,
|
||||||
|
bid: int = Path(...),
|
||||||
|
current_user: Optional[HttpUser] = Depends(get_current_http_user, use_cache=False)
|
||||||
|
):
|
||||||
|
# Internal call to existing detail function
|
||||||
|
# Pass a dummy None user since the API requires it but doesn't enforce privileges
|
||||||
|
bulletin = await get_bulletin(bid=bid, current_user=None)
|
||||||
|
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"bulletin_detail.html",
|
||||||
|
{"request": request, "bulletin": bulletin, "current_user": current_user.username if current_user else None}
|
||||||
|
)
|
||||||
@@ -5,6 +5,7 @@ from fastapi.templating import Jinja2Templates
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from .routers import public, profile, messages, send, bulletins
|
from .routers import public, profile, messages, send, bulletins
|
||||||
|
from packetserver.http.routers.bulletins import html_router
|
||||||
|
|
||||||
BASE_DIR = Path(__file__).parent.resolve()
|
BASE_DIR = Path(__file__).parent.resolve()
|
||||||
|
|
||||||
@@ -43,4 +44,5 @@ app.include_router(profile.router)
|
|||||||
app.include_router(messages.router)
|
app.include_router(messages.router)
|
||||||
app.include_router(send.router)
|
app.include_router(send.router)
|
||||||
app.include_router(dashboard.router)
|
app.include_router(dashboard.router)
|
||||||
app.include_router(bulletins.router)
|
app.include_router(bulletins.router)
|
||||||
|
app.include_router(html_router)
|
||||||
23
packetserver/http/templates/bulletin_detail.html
Normal file
23
packetserver/http/templates/bulletin_detail.html
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>{{ bulletin.subject }} - PacketServer</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; max-width: 800px; margin: 2em auto; padding: 1em; background: #f9f9f9; }
|
||||||
|
pre { background: white; padding: 1em; border-radius: 8px; white-space: pre-wrap; }
|
||||||
|
.meta { color: #666; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>{{ bulletin.subject }}</h1>
|
||||||
|
<p class="meta">By {{ bulletin.author }} on {{ bulletin.created_at[:10] }}</p>
|
||||||
|
|
||||||
|
<pre>{{ bulletin.body }}</pre>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="/bulletins">← All Bulletins</a> |
|
||||||
|
<a href="/dashboard">Dashboard</a>
|
||||||
|
</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
40
packetserver/http/templates/bulletin_list.html
Normal file
40
packetserver/http/templates/bulletin_list.html
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Bulletins - PacketServer</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; max-width: 900px; margin: 2em auto; padding: 1em; background: #f9f9f9; }
|
||||||
|
h1 { text-align: center; }
|
||||||
|
ul { list-style: none; padding: 0; }
|
||||||
|
li { background: white; margin: 1em 0; padding: 1em; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||||
|
.meta { color: #666; font-size: 0.9em; }
|
||||||
|
.preview { margin-top: 0.5em; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Bulletins</h1>
|
||||||
|
|
||||||
|
{% if current_user %}
|
||||||
|
<p><a href="/bulletins/new">Create New Bulletin</a></p>
|
||||||
|
{% else %}
|
||||||
|
<p><em>Log in to create bulletins.</em></p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if bulletins %}
|
||||||
|
<ul>
|
||||||
|
{% for bull in bulletins %}
|
||||||
|
<li>
|
||||||
|
<strong><a href="/bulletins/{{ bull.id }}">{{ bull.subject }}</a></strong>
|
||||||
|
<div class="meta">by {{ bull.author }} on {{ bull.created_at[:10] }}</div>
|
||||||
|
<div class="preview">{{ bull.body[:200] }}{% if bull.body|length > 200 %}...{% endif %}</div>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<p>No bulletins yet.</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<p><a href="/dashboard">← Back to Dashboard</a></p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user