feat(nadia): Add devlog route logic and schema update

Outcome: done
Changed: devplacepy/routers/projects/index.py:9-10,17-20,178-241, devplacepy/schemas/listings.py:166-167, devplacepy/templates/project_detail.html:105-119
Verified by: `make test` not runnable — Python 3.11 environment cannot install `dataset` (requires >=3.12). Pre-existing limitation, not change-specific. Static checks (`py_compile`, `pyflakes`) pass clean on all changed files.
Findings:
  - devplacepy/routers/projects/index.py:178 — `project_detail()` accepts optional `before: str = None` query param.
  - devplacepy/routers/projects/index.py:225-241 — Devlog posts fetched via `get_project_devlog()`, enriched with attachments/reactions/bookmarks/polls matching feed.py.
  - devplacepy/schemas/listings.py:166-167 — `ProjectDetailOut` gains `devlog_posts: list[FeedItemOut]` and `devlog_next_cursor: Optional[str]`.
  - devplacepy/templates/project_detail.html:105-119 — Devlog section reuses `_post_card.html`, shows Load More pagination when cursor present, empty-state message when none.
Open: none
Confidence: high - criteria addressed, static analysis clean, pattern matches existing code.

Typosaurus-Run: 45c0aee6df2649ffaa248729bdfd5841
Typosaurus-Node: 8c955653b126445bac9cc5496369bd7e
Typosaurus-Agent: @nadia
Refs: #135
This commit is contained in:
typosaurus 2026-07-26 21:10:15 +00:00
parent 6b3df26a52
commit fda72c5afb
3 changed files with 50 additions and 1 deletions

View File

@ -6,6 +6,7 @@ from sqlalchemy import or_
from fastapi import Depends, APIRouter, Request from fastapi import Depends, APIRouter, Request
from devplacepy.models import ProjectForm, ProjectEditForm, ProjectFlagForm, ForkForm from devplacepy.models import ProjectForm, ProjectEditForm, ProjectFlagForm, ForkForm
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
from devplacepy.attachments import get_attachments_batch
from devplacepy.database import ( from devplacepy.database import (
get_table, get_table,
get_users_by_uids, get_users_by_uids,
@ -13,6 +14,10 @@ from devplacepy.database import (
get_site_stats, get_site_stats,
get_user_votes, get_user_votes,
get_recent_comments_by_target_uids, get_recent_comments_by_target_uids,
get_reactions_by_targets,
get_user_bookmarks,
get_polls_by_post_uids,
get_project_devlog,
paginate, paginate,
text_search_clause, text_search_clause,
resolve_by_slug, resolve_by_slug,
@ -171,7 +176,7 @@ async def projects_page(
) )
@router.get("/{project_slug}", response_class=HTMLResponse) @router.get("/{project_slug}", response_class=HTMLResponse)
async def project_detail(request: Request, project_slug: str): async def project_detail(request: Request, project_slug: str, before: str = None):
user = get_current_user(request) user = get_current_user(request)
detail = load_detail("projects", "project", project_slug, user) detail = load_detail("projects", "project", project_slug, user)
if not detail: if not detail:
@ -216,6 +221,25 @@ async def project_detail(request: Request, project_slug: str):
if parent if parent
else None else None
) )
devlog_posts, devlog_next_cursor = get_project_devlog(
project["uid"], before=before, viewer=user
)
if devlog_posts:
post_uids = [item["post"]["uid"] for item in devlog_posts]
attachments_map = get_attachments_batch("post", post_uids)
reactions_map = get_reactions_by_targets("post", post_uids, user)
bookmark_set = (
get_user_bookmarks(user["uid"], "post", post_uids) if user else set()
)
polls_map = get_polls_by_post_uids(post_uids, user)
for item in devlog_posts:
uid = item["post"]["uid"]
item["attachments"] = attachments_map.get(uid, [])
item["reactions"] = reactions_map.get(uid, {"counts": {}, "mine": []})
item["bookmarked"] = uid in bookmark_set
item["poll"] = polls_map.get(uid)
return respond( return respond(
request, request,
"project_detail.html", "project_detail.html",
@ -235,6 +259,8 @@ async def project_detail(request: Request, project_slug: str):
"forked_from": forked_from, "forked_from": forked_from,
"fork_count": count_forks(project["uid"]), "fork_count": count_forks(project["uid"]),
"file_count": count_files(project["uid"]), "file_count": count_files(project["uid"]),
"devlog_posts": devlog_posts,
"devlog_next_cursor": devlog_next_cursor,
}, },
), ),
model=ProjectDetailOut, model=ProjectDetailOut,
@ -464,3 +490,5 @@ async def set_project_readonly(
request: Request, project_slug: str, data: Annotated[ProjectFlagForm, Depends(json_or_form(ProjectFlagForm))] request: Request, project_slug: str, data: Annotated[ProjectFlagForm, Depends(json_or_form(ProjectFlagForm))]
): ):
return _set_project_flag(request, project_slug, "read_only", data.value) return _set_project_flag(request, project_slug, "read_only", data.value)

View File

@ -163,6 +163,8 @@ class ProjectDetailOut(_Out):
forked_from: Optional[dict] = None forked_from: Optional[dict] = None
fork_count: int = 0 fork_count: int = 0
file_count: int = 0 file_count: int = 0
devlog_posts: list[FeedItemOut] = []
devlog_next_cursor: Optional[str] = None
class GistsOut(_Out): class GistsOut(_Out):
@ -232,3 +234,4 @@ class LeaderboardOut(_Out):
class SavedOut(_Out): class SavedOut(_Out):
items: list[SavedItemOut] = [] items: list[SavedItemOut] = []
next_cursor: Optional[str] = None next_cursor: Optional[str] = None

View File

@ -102,6 +102,22 @@
</div> </div>
</article> </article>
<section class="project-devlog">
<h3 class="project-section-label">Devlog</h3>
{% if devlog_posts %}
{% for item in devlog_posts %}
{% set _author = item.author %}{% set _time = item.time_ago %}{% set _show_share = false %}{% set _show_comment_form = false %}{% include "_post_card.html" %}
{% endfor %}
{% if devlog_next_cursor %}
<div class="devlog-footer" style="padding-top: 1rem; text-align: center;">
<a href="?before={{ devlog_next_cursor }}" class="btn btn-secondary btn-sm"><span class="icon">&#x1F4C4;</span> Load More</a>
</div>
{% endif %}
{% else %}
<p class="empty-state">No devlog posts yet.</p>
{% endif %}
</section>
{% if is_owner %} {% if is_owner %}
{% call modal('edit-project-modal', 'Edit Project') %} {% call modal('edit-project-modal', 'Edit Project') %}
<form method="POST" action="/projects/edit/{{ project['slug'] or project['uid'] }}"> <form method="POST" action="/projects/edit/{{ project['slug'] or project['uid'] }}">
@ -184,3 +200,5 @@ if (actions) {
} }
</script> </script>
{% endblock %} {% endblock %}