|
# retoor <retoor@molodetz.nl>
|
|
|
|
import logging
|
|
from typing import Annotated
|
|
from datetime import datetime, timezone
|
|
from fastapi import Depends, APIRouter, Request
|
|
from fastapi.responses import HTMLResponse
|
|
from devplacepy.constants import TOPICS
|
|
from devplacepy.database import (
|
|
db,
|
|
get_table,
|
|
resolve_by_slug,
|
|
resolve_object_url,
|
|
mark_notifications_read_by_target,
|
|
)
|
|
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.responses import respond, action_result
|
|
from devplacepy.schemas import PostDetailOut
|
|
from devplacepy.seo import (
|
|
base_seo_context,
|
|
site_url,
|
|
website_schema,
|
|
discussion_forum_posting,
|
|
)
|
|
from devplacepy.attachments import save_inline_image
|
|
from devplacepy.models import PostForm, PostEditForm
|
|
from devplacepy.services.audit import record as audit
|
|
from devplacepy.dependencies import json_or_form
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter()
|
|
|
|
@router.post("/create")
|
|
async def create_post(request: Request, data: Annotated[PostForm, Depends(json_or_form(PostForm))]):
|
|
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,
|
|
request,
|
|
)
|
|
|
|
create_poll(uid, user, data.poll_question, data.poll_options, request)
|
|
url = f"/posts/{post_slug}"
|
|
return action_result(request, url, data={"uid": uid, "slug": post_slug, "url": url})
|
|
|
|
def create_poll(
|
|
post_uid: str, user: dict, question: str, options: list[str], request=None
|
|
) -> 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(),
|
|
"deleted_at": None,
|
|
"deleted_by": None,
|
|
}
|
|
)
|
|
get_table("poll_options").insert_many(
|
|
[
|
|
{
|
|
"uid": generate_uid(),
|
|
"poll_uid": poll_uid,
|
|
"label": label,
|
|
"position": index,
|
|
"deleted_at": None,
|
|
"deleted_by": None,
|
|
}
|
|
for index, label in enumerate(labels)
|
|
]
|
|
)
|
|
audit.record(
|
|
request,
|
|
"poll.create",
|
|
user=user,
|
|
target_type="poll",
|
|
target_uid=poll_uid,
|
|
target_label=question,
|
|
metadata={"option_count": len(labels), "question": question},
|
|
summary=f"{user['username']} added a poll to post {post_uid}",
|
|
links=[audit.poll(poll_uid, question), audit.parent("post", post_uid)],
|
|
)
|
|
|
|
@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
|
|
if user:
|
|
mark_notifications_read_by_target(
|
|
user["uid"], resolve_object_url("post", post["uid"])
|
|
)
|
|
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=post.get("content", ""),
|
|
seo_target=("post", post["uid"]),
|
|
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 AND p.deleted_at IS NULL 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 respond(
|
|
request,
|
|
"post.html",
|
|
detail_context(
|
|
request,
|
|
user,
|
|
detail,
|
|
"post",
|
|
seo_ctx,
|
|
{
|
|
"comment_count": comment_count,
|
|
"related_posts": related_posts,
|
|
"topics": list(TOPICS),
|
|
},
|
|
),
|
|
model=PostDetailOut,
|
|
)
|
|
|
|
@router.post("/edit/{post_slug}")
|
|
async def edit_post(
|
|
request: Request, post_slug: str, data: Annotated[PostEditForm, Depends(json_or_form(PostEditForm))]
|
|
):
|
|
user = require_user(request)
|
|
result = edit_content_item(
|
|
request,
|
|
"posts",
|
|
user,
|
|
post_slug,
|
|
{
|
|
"content": data.content.strip(),
|
|
"title": data.title.strip() or None,
|
|
"topic": data.topic,
|
|
},
|
|
"/feed",
|
|
target_type="post",
|
|
)
|
|
if data.poll_question.strip():
|
|
post = resolve_by_slug(get_table("posts"), post_slug)
|
|
if (
|
|
post
|
|
and post["user_uid"] == user["uid"]
|
|
and not get_table("polls").find_one(post_uid=post["uid"], deleted_at=None)
|
|
):
|
|
create_poll(
|
|
post["uid"], user, data.poll_question, data.poll_options, request
|
|
)
|
|
return result
|
|
|
|
@router.post("/delete/{post_slug}")
|
|
async def delete_post(request: Request, post_slug: str):
|
|
user = require_user(request)
|
|
return delete_content_item(
|
|
request, "posts", "post", user, post_slug, "/feed", inline_image_field="image"
|
|
)
|