116 lines
4.4 KiB
Python
Raw Normal View History

2026-05-10 09:08:12 +02:00
import logging
2026-05-23 05:44:04 +02:00
from typing import Annotated
2026-05-30 20:16:39 +02:00
from fastapi import APIRouter, Request, Form
2026-05-10 09:08:12 +02:00
from fastapi.responses import RedirectResponse, HTMLResponse
2026-05-13 21:17:57 +02:00
from devplacepy.constants import TOPICS
2026-05-30 20:16:39 +02:00
from devplacepy.database import db
2026-05-10 09:08:12 +02:00
from devplacepy.templating import templates
2026-05-30 20:16:39 +02:00
from devplacepy.utils import get_current_user, require_user, time_ago, not_found, XP_POST
2026-06-05 20:05:07 +02:00
from devplacepy.content import load_detail, edit_content_item, delete_content_item, create_content_item, detail_context, canonical_redirect, first_image_url
2026-05-23 08:34:13 +02:00
from devplacepy.seo import base_seo_context, site_url, website_schema, discussion_forum_posting, truncate
2026-05-30 20:16:39 +02:00
from devplacepy.attachments import save_inline_image
2026-05-23 05:44:04 +02:00
from devplacepy.models import PostForm, PostEditForm
2026-05-10 09:08:12 +02:00
logger = logging.getLogger(__name__)
router = APIRouter()
@router.post("/create")
2026-05-23 05:44:04 +02:00
async def create_post(request: Request, data: Annotated[PostForm, Form()]):
2026-05-10 09:08:12 +02:00
user = require_user(request)
2026-05-23 05:44:04 +02:00
content = data.content.strip()
title = data.title.strip()
topic = data.topic
project_uid = data.project_uid
2026-05-10 09:08:12 +02:00
2026-05-11 00:41:41 +02:00
image_filename = None
2026-05-23 05:44:04 +02:00
form = await request.form()
2026-05-11 00:41:41 +02:00
image_file = form.get("image")
2026-05-23 05:44:04 +02:00
if image_file is not None and hasattr(image_file, "filename") and image_file.filename:
2026-05-23 01:50:31 +02:00
image_filename = save_inline_image(await image_file.read(), image_file.filename)
if image_filename:
content += f"\n\n![](/static/uploads/{image_filename})"
2026-05-11 00:41:41 +02:00
2026-05-11 20:49:45 +02:00
slug_text = title if title else content[:50]
2026-05-30 20:16:39 +02:00
uid, post_slug = create_content_item("posts", "post", user, {
2026-05-10 09:08:12 +02:00
"title": title or None,
"content": content,
"topic": topic,
"project_uid": project_uid or None,
2026-05-11 00:41:41 +02:00
"image": image_filename,
2026-05-30 20:16:39 +02:00
}, slug_text, XP_POST, "First Post", content, data.attachment_uids)
2026-05-11 20:49:45 +02:00
return RedirectResponse(url=f"/posts/{post_slug}", status_code=302)
2026-05-10 09:08:12 +02:00
2026-05-11 20:49:45 +02:00
@router.get("/{post_slug}", response_class=HTMLResponse)
async def view_post(request: Request, post_slug: str):
2026-05-10 09:08:12 +02:00
user = get_current_user(request)
2026-05-30 20:16:39 +02:00
detail = load_detail("posts", "post", post_slug, user)
if not detail:
raise not_found("Post not found")
post = detail["item"]
2026-06-05 20:05:07 +02:00
redirect = canonical_redirect("posts", post, post_slug)
if redirect:
return redirect
2026-05-30 20:16:39 +02:00
author = detail["author"]
top_level = detail["comments"]
2026-05-11 20:49:45 +02:00
def count_all(items):
total = len(items)
for item in items:
total += count_all(item.get("children", []))
return total
comment_count = count_all(top_level)
2026-05-11 05:30:51 +02:00
base = site_url(request)
seo_ctx = base_seo_context(
request,
title=post.get("title") or "Post",
description=truncate(post.get("content", ""), 160),
breadcrumbs=[
{"name": "Home", "url": "/feed"},
{"name": "Feed", "url": "/feed"},
2026-05-11 20:49:45 +02:00
{"name": post.get("title") or "Post", "url": f"/posts/{post['slug'] or post['uid']}"},
2026-05-11 05:30:51 +02:00
],
og_type="article",
2026-06-05 20:05:07 +02:00
og_image=first_image_url(post, detail["attachments"]),
2026-05-11 05:30:51 +02:00
schemas=[
website_schema(base),
2026-05-30 20:16:39 +02:00
discussion_forum_posting(post, author, comment_count, detail["star_count"], base),
2026-05-11 05:30:51 +02:00
],
)
related_posts = []
if "posts" in db.tables:
rows = list(db.query(
"SELECT p.*, u.username FROM posts p JOIN users u ON p.user_uid = u.uid WHERE p.topic = :t AND p.uid != :uid ORDER BY p.created_at DESC LIMIT 5",
2026-05-11 20:49:45 +02:00
t=post.get("topic", ""), uid=post["uid"],
2026-05-11 05:30:51 +02:00
))
for r in rows:
related_posts.append({
"post": r,
"author": {"username": r["username"]},
"time_ago": time_ago(r["created_at"]),
})
2026-05-30 20:16:39 +02:00
return templates.TemplateResponse(request, "post.html", detail_context(request, user, detail, "post", seo_ctx, {
2026-05-11 05:30:51 +02:00
"comment_count": comment_count,
"related_posts": related_posts,
2026-05-12 15:07:34 +02:00
"topics": list(TOPICS),
2026-05-30 20:16:39 +02:00
}))
2026-05-11 05:30:51 +02:00
2026-05-11 20:49:45 +02:00
@router.post("/edit/{post_slug}")
2026-05-23 05:44:04 +02:00
async def edit_post(request: Request, post_slug: str, data: Annotated[PostEditForm, Form()]):
2026-05-11 07:02:06 +02:00
user = require_user(request)
2026-05-23 08:34:13 +02:00
return edit_content_item("posts", user, post_slug, {
2026-05-23 05:44:04 +02:00
"content": data.content.strip(),
"title": data.title.strip() or None,
"topic": data.topic,
2026-05-23 08:34:13 +02:00
}, "/feed")
2026-05-11 07:02:06 +02:00
2026-05-11 20:49:45 +02:00
@router.post("/delete/{post_slug}")
async def delete_post(request: Request, post_slug: str):
2026-05-11 05:30:51 +02:00
user = require_user(request)
2026-05-23 08:34:13 +02:00
return delete_content_item("posts", "post", user, post_slug, "/feed", inline_image_field="image")