|
import logging
|
|
from typing import Annotated
|
|
from datetime import datetime, timezone
|
|
from fastapi import APIRouter, Request, Form
|
|
from fastapi.responses import RedirectResponse, HTMLResponse
|
|
from devplacepy.constants import TOPICS
|
|
from devplacepy.database import db, get_table
|
|
from devplacepy.templating import templates
|
|
from devplacepy.utils import get_current_user, require_user, time_ago, not_found, generate_uid, XP_POST
|
|
from devplacepy.content import load_detail, edit_content_item, delete_content_item, create_content_item, detail_context, canonical_redirect, first_image_url
|
|
from devplacepy.seo import base_seo_context, site_url, website_schema, discussion_forum_posting, truncate
|
|
from devplacepy.attachments import save_inline_image
|
|
from devplacepy.models import PostForm, PostEditForm
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter()
|
|
|
|
|
|
@router.post("/create")
|
|
async def create_post(request: Request, data: Annotated[PostForm, Form()]):
|
|
user = require_user(request)
|
|
content = data.content.strip()
|
|
title = data.title.strip()
|
|
topic = data.topic
|
|
project_uid = data.project_uid
|
|
|
|
image_filename = None
|
|
form = await request.form()
|
|
image_file = form.get("image")
|
|
if image_file is not None and hasattr(image_file, "filename") and image_file.filename:
|
|
image_filename = save_inline_image(await image_file.read(), image_file.filename)
|
|
if image_filename:
|
|
content += f"\n\n"
|
|
|
|
slug_text = title if title else content[:50]
|
|
uid, post_slug = create_content_item("posts", "post", user, {
|
|
"title": title or None,
|
|
"content": content,
|
|
"topic": topic,
|
|
"project_uid": project_uid or None,
|
|
"image": image_filename,
|
|
}, slug_text, XP_POST, "First Post", content, data.attachment_uids)
|
|
|
|
create_poll(uid, user, data.poll_question, data.poll_options)
|
|
return RedirectResponse(url=f"/posts/{post_slug}", status_code=302)
|
|
|
|
|
|
def create_poll(post_uid: str, user: dict, question: str, options: list[str]) -> None:
|
|
question = question.strip()
|
|
labels = [option.strip() for option in options if option.strip()][:6]
|
|
if not question or len(labels) < 2:
|
|
return
|
|
poll_uid = generate_uid()
|
|
get_table("polls").insert({
|
|
"uid": poll_uid,
|
|
"post_uid": post_uid,
|
|
"user_uid": user["uid"],
|
|
"question": question,
|
|
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
})
|
|
get_table("poll_options").insert_many([{
|
|
"uid": generate_uid(),
|
|
"poll_uid": poll_uid,
|
|
"label": label,
|
|
"position": index,
|
|
} for index, label in enumerate(labels)])
|
|
|
|
|
|
@router.get("/{post_slug}", response_class=HTMLResponse)
|
|
async def view_post(request: Request, post_slug: str):
|
|
user = get_current_user(request)
|
|
detail = load_detail("posts", "post", post_slug, user)
|
|
if not detail:
|
|
raise not_found("Post not found")
|
|
post = detail["item"]
|
|
redirect = canonical_redirect("posts", post, post_slug)
|
|
if redirect:
|
|
return redirect
|
|
author = detail["author"]
|
|
top_level = detail["comments"]
|
|
|
|
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)
|
|
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"},
|
|
{"name": post.get("title") or "Post", "url": f"/posts/{post['slug'] or post['uid']}"},
|
|
],
|
|
og_type="article",
|
|
og_image=first_image_url(post, detail["attachments"]),
|
|
schemas=[
|
|
website_schema(base),
|
|
discussion_forum_posting(post, author, comment_count, detail["star_count"], base),
|
|
],
|
|
)
|
|
|
|
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",
|
|
t=post.get("topic", ""), uid=post["uid"],
|
|
))
|
|
for r in rows:
|
|
related_posts.append({
|
|
"post": r,
|
|
"author": {"username": r["username"]},
|
|
"time_ago": time_ago(r["created_at"]),
|
|
})
|
|
|
|
return templates.TemplateResponse(request, "post.html", detail_context(request, user, detail, "post", seo_ctx, {
|
|
"comment_count": comment_count,
|
|
"related_posts": related_posts,
|
|
"topics": list(TOPICS),
|
|
}))
|
|
|
|
|
|
@router.post("/edit/{post_slug}")
|
|
async def edit_post(request: Request, post_slug: str, data: Annotated[PostEditForm, Form()]):
|
|
user = require_user(request)
|
|
return edit_content_item("posts", user, post_slug, {
|
|
"content": data.content.strip(),
|
|
"title": data.title.strip() or None,
|
|
"topic": data.topic,
|
|
}, "/feed")
|
|
|
|
|
|
@router.post("/delete/{post_slug}")
|
|
async def delete_post(request: Request, post_slug: str):
|
|
user = require_user(request)
|
|
return delete_content_item("posts", "post", user, post_slug, "/feed", inline_image_field="image")
|