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:
parent
6b3df26a52
commit
fda72c5afb
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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">📄</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 %}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user