Files
devplacepy/devplacepy/schemas/listings.py
T
blindxfishandClaude Fable 5 72e088c160 Dedicate the project page to the project
The project detail page becomes a full project showcase built entirely
from existing platform mechanisms. One encompassing dark card wraps the
page; inner panels (tab bar, sidebar cards, devlog entries, comments)
sit one elevation lighter. The hero opens with a cover banner and an
optional logo tile, both plain attachment references
(cover_attachment_uid/logo_attachment_uid) uploaded through the
standard dp-upload attachment widget and linked via the existing
link_attachments choke point - the route validates each uid belongs to
the actor and is an image, and an empty value on edit keeps the current
one. The title block, type/platform chips and author row overlay the
banner behind a scrim with a dark text shadow, next to an owner-set
Visit Website CTA; website_url and repo_url are normalized in models
and render with rel noopener nofollow.

An anchor tab bar (Overview, Devlog, Screenshots when present,
Comments, Files) navigates the page. The main column keeps About, the
devlog timeline (with devlog_count and an owner Post update button
opening the shared composer preset to the devlog topic + project - the
form now lives once in _post_composer_form.html, included by feed.html
and project_detail.html), a Screenshots gallery built from image
attachments minus the cover/logo (thumbnails, lightbox, 12 rendered),
and the comment thread; the sidebar holds Links, Stats and the Author
card. Owners add gallery images from the More menu via
POST /projects/{slug}/screenshots (owner-only, audit
project.screenshots.add, Devii action project_add_screenshots, docs id
projects-screenshots). comment_count/devlog_count ride
ProjectDetailOut, the new fields ride ProjectOut, and the create/edit
faces (modals, Devii actions, API docs) carry them. The project
comment/files e2e tests scope their locators per the documented
dual-control idiom, and new unit/api/e2e tests cover URL normalization,
the counts, the hero attachment guard, the screenshots flow and the
preset composer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 23:00:12 +02:00

254 lines
6.7 KiB
Python

# retoor <retoor@molodetz.nl>
from __future__ import annotations
from typing import Any, Optional
from devplacepy.schemas.base import _Out
from devplacepy.schemas.content import (
AttachmentOut,
CommentItemOut,
GistOut,
MessageOut,
NewsOut,
NotificationOut,
PollOut,
PostOut,
ProjectLinkOut,
ProjectOut,
ReactionsOut,
UserOut,
)
class FeedItemOut(_Out):
post: PostOut
maturity: Optional[str] = None
author: Optional[UserOut] = None
time_ago: Optional[str] = None
my_vote: int = 0
comment_count: int = 0
attachments: list[AttachmentOut] = []
recent_comments: list[CommentItemOut] = []
reactions: ReactionsOut = ReactionsOut()
bookmarked: bool = False
poll: Optional[PollOut] = None
project_link: Optional[ProjectLinkOut] = None
class GistItemOut(_Out):
gist: GistOut
maturity: Optional[str] = None
author: Optional[UserOut] = None
time_ago: Optional[str] = None
my_vote: int = 0
comment_count: int = 0
recent_comments: list[CommentItemOut] = []
class ProjectListItemOut(ProjectOut):
author_name: Optional[str] = None
my_vote: int = 0
recent_comments: list[CommentItemOut] = []
class NewsListItemOut(_Out):
article: NewsOut
time_ago: Optional[str] = None
image_url: Optional[str] = None
grade: Optional[int] = None
featured: Optional[int] = None
recent_comments: list[CommentItemOut] = []
class ConversationOut(_Out):
other_user: Optional[UserOut] = None
last_message: Optional[str] = None
last_message_at: Optional[str] = None
unread: bool = False
class MessageItemOut(_Out):
message: MessageOut
sender: Optional[UserOut] = None
is_mine: bool = False
time_ago: Optional[str] = None
attachments: list[AttachmentOut] = []
grouped: bool = False
class NotificationItemOut(_Out):
notification: NotificationOut
actor: Optional[UserOut] = None
time_ago: Optional[str] = None
class NotificationGroupOut(_Out):
label: Optional[str] = None
entries: list[NotificationItemOut] = []
class LeaderboardEntryOut(_Out):
uid: Optional[str] = None
username: Optional[str] = None
avatar_seed: Optional[str] = None
stars: Optional[int] = None
level: Optional[int] = None
rank: Optional[int] = None
class SavedItemOut(_Out):
target_type: Optional[str] = None
target_uid: Optional[str] = None
type_label: Optional[str] = None
title: Optional[str] = None
url: Optional[str] = None
time_ago: Optional[str] = None
class FeedOut(_Out):
posts: list[FeedItemOut] = []
current_tab: Optional[str] = None
current_topic: Optional[str] = None
search: Optional[str] = None
next_cursor: Optional[str] = None
total_members: Optional[int] = None
posts_today: Optional[int] = None
total_projects: Optional[int] = None
total_gists: Optional[int] = None
top_authors: list[UserOut] = []
daily_topic: Optional[Any] = None
online_users: list[UserOut] = []
class PostDetailOut(_Out):
maturity: Optional[str] = None
post: PostOut
author: Optional[UserOut] = None
is_owner: bool = False
star_count: int = 0
my_vote: int = 0
time_ago: Optional[str] = None
comments: list[CommentItemOut] = []
attachments: list[AttachmentOut] = []
reactions: ReactionsOut = ReactionsOut()
bookmarked: bool = False
poll: Optional[PollOut] = None
comment_count: Optional[int] = None
related_posts: list[FeedItemOut] = []
topics: list[str] = []
project_link: Optional[ProjectLinkOut] = None
class ProjectsOut(_Out):
projects: list[ProjectListItemOut] = []
current_tab: Optional[str] = None
search: Optional[str] = None
project_type: Optional[str] = None
total_count: Optional[int] = None
next_cursor: Optional[str] = None
total_members: Optional[int] = None
top_authors: list[UserOut] = []
class ProjectDetailOut(_Out):
maturity: Optional[str] = None
project: ProjectOut
author: Optional[UserOut] = None
is_owner: bool = False
star_count: int = 0
my_vote: int = 0
time_ago: Optional[str] = None
comments: list[CommentItemOut] = []
attachments: list[AttachmentOut] = []
reactions: ReactionsOut = ReactionsOut()
bookmarked: bool = False
platforms: Optional[Any] = None
is_private: bool = False
read_only: bool = False
viewer_can_containers: bool = False
viewer_can_workspace: bool = False
workspace_editor_url: Optional[str] = None
workspace_editor_mode: Optional[str] = None
workspace_editor_width: Optional[int] = None
workspace_editor_height: Optional[int] = None
forked_from: Optional[dict] = None
fork_count: int = 0
file_count: int = 0
comment_count: int = 0
devlog_posts: list[FeedItemOut] = []
devlog_next_cursor: Optional[str] = None
devlog_count: int = 0
class GistsOut(_Out):
gists: list[GistItemOut] = []
total_count: Optional[int] = None
next_cursor: Optional[str] = None
current_language: Optional[str] = None
search: Optional[str] = None
languages: list[tuple[str, str]] = []
gist_language_codes: list[str] = []
class GistDetailOut(_Out):
maturity: Optional[str] = None
gist: GistOut
author: Optional[UserOut] = None
is_owner: bool = False
star_count: int = 0
my_vote: int = 0
time_ago: Optional[str] = None
comments: list[CommentItemOut] = []
attachments: list[AttachmentOut] = []
reactions: ReactionsOut = ReactionsOut()
bookmarked: bool = False
class NewsListOut(_Out):
articles: list[NewsListItemOut] = []
next_cursor: Optional[str] = None
class NewsDetailOut(_Out):
maturity: Optional[str] = None
article: NewsOut
canonical_slug: Optional[str] = None
image_url: Optional[str] = None
grade: Optional[int] = None
time_ago: Optional[str] = None
comments: list[CommentItemOut] = []
bookmarked: bool = False
class MessagesOut(_Out):
conversations: list[ConversationOut] = []
messages: list[MessageItemOut] = []
other_user: Optional[UserOut] = None
current_conversation: Optional[str] = None
search: Optional[str] = None
other_online: bool = False
other_last_seen: Optional[str] = None
class NotificationsOut(_Out):
notification_groups: list[NotificationGroupOut] = []
next_cursor: Optional[str] = None
class LeaderboardOut(_Out):
entries: list[LeaderboardEntryOut] = []
user_rank: Optional[int] = None
total_members: Optional[int] = None
posts_today: Optional[int] = None
total_projects: Optional[int] = None
total_gists: Optional[int] = None
top_authors: list[UserOut] = []
featured_news: list[NewsOut] = []
class SavedOut(_Out):
items: list[SavedItemOut] = []
next_cursor: Optional[str] = None