Files
devplacepy/devplacepy/schemas/content.py
T
blindxfishandClaude Fable 5 72e088c160
DevPlace CI / test (pull_request) Has been cancelled
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

225 lines
5.6 KiB
Python

# retoor <retoor@molodetz.nl>
from __future__ import annotations
from typing import Any, Optional
from pydantic import ConfigDict, Field
from devplacepy.schemas.base import _Out
class UserOut(_Out):
uid: str = ""
username: str = ""
avatar_seed: Optional[str] = None
bio: Optional[str] = None
location: Optional[str] = None
git_link: Optional[str] = None
website: Optional[str] = None
level: Optional[int] = None
xp: Optional[int] = None
xp_progress_pct: Optional[int] = None
xp_next_level: Optional[int] = None
stars: Optional[int] = None
created_at: Optional[str] = None
last_seen: Optional[str] = None
class AttachmentOut(_Out):
uid: str = ""
filename: Optional[str] = None
url: Optional[str] = None
size: Optional[int] = None
is_image: Optional[bool] = None
is_video: Optional[bool] = None
mime_type: Optional[str] = None
created_at: Optional[str] = None
can_modify: bool = False
class VotesOut(_Out):
up: int = 0
down: int = 0
class ReactionsOut(_Out):
counts: dict = {}
mine: list = []
class PollOptionOut(_Out):
uid: str = ""
label: Optional[str] = None
count: Optional[int] = None
votes: Optional[int] = None
pct: Optional[int] = None
class PollOut(_Out):
uid: str = ""
question: Optional[str] = None
options: list[PollOptionOut] = []
total: int = 0
my_choice: Optional[str] = None
voted: Optional[str] = None
class BadgeOut(_Out):
name: Optional[str] = Field(None, alias="badge_name")
icon: Optional[str] = None
description: Optional[str] = None
created_at: Optional[str] = None
model_config = ConfigDict(populate_by_name=True)
class ProjectLinkOut(_Out):
uid: str = ""
name: Optional[str] = None
slug: Optional[str] = None
url: Optional[str] = None
class PostOut(_Out):
uid: str = ""
slug: Optional[str] = None
user_uid: Optional[str] = None
title: Optional[str] = None
content: Optional[str] = None
topic: Optional[str] = None
stars: Optional[int] = None
image: Optional[str] = None
project_uid: Optional[str] = None
created_at: Optional[str] = None
updated_at: Optional[str] = None
class GistOut(_Out):
uid: str = ""
slug: Optional[str] = None
user_uid: Optional[str] = None
title: Optional[str] = None
description: Optional[str] = None
source_code: Optional[str] = None
language: Optional[str] = None
stars: Optional[int] = None
created_at: Optional[str] = None
updated_at: Optional[str] = None
class ProjectOut(_Out):
uid: str = ""
slug: Optional[str] = None
user_uid: Optional[str] = None
title: Optional[str] = None
description: Optional[str] = None
project_type: Optional[str] = None
status: Optional[str] = None
platforms: Optional[Any] = None
stars: Optional[int] = None
is_private: Optional[bool] = None
read_only: Optional[bool] = None
release_date: Optional[str] = None
demo_date: Optional[str] = None
website_url: Optional[str] = None
repo_url: Optional[str] = None
cover_attachment_uid: Optional[str] = None
logo_attachment_uid: Optional[str] = None
created_at: Optional[str] = None
updated_at: Optional[str] = None
class ProjectFileOut(_Out):
uid: str = ""
path: str = ""
name: Optional[str] = None
type: Optional[str] = None
is_binary: Optional[bool] = None
mime_type: Optional[str] = None
size: Optional[int] = None
url: Optional[str] = None
updated_at: Optional[str] = None
class ProjectFileRawOut(ProjectFileOut):
content: Optional[str] = None
class ProjectFilesOut(_Out):
project: ProjectOut
files: list[ProjectFileOut] = []
is_owner: bool = False
read_only: bool = False
is_private: bool = False
class NewsOut(_Out):
uid: str = ""
slug: Optional[str] = None
title: Optional[str] = None
description: Optional[str] = None
content: Optional[str] = None
url: Optional[str] = None
source_name: Optional[str] = None
author: Optional[str] = None
grade: Optional[int] = None
ai_grade: Optional[int] = None
status: Optional[str] = None
image_url: Optional[str] = None
featured: Optional[int] = None
has_unique_image: Optional[int] = None
article_published: Optional[str] = None
created_at: Optional[str] = None
synced_at: Optional[str] = None
class CommentOut(_Out):
uid: str = ""
user_uid: Optional[str] = None
content: Optional[str] = None
parent_uid: Optional[str] = None
target_type: Optional[str] = None
target_uid: Optional[str] = None
created_at: Optional[str] = None
class CommentEditOut(_Out):
uid: str = ""
content: str = ""
url: str = ""
updated_at: Optional[str] = None
class CommentItemOut(_Out):
comment: CommentOut
author: Optional[UserOut] = None
time_ago: Optional[str] = None
votes: VotesOut = VotesOut()
my_vote: int = 0
children: list["CommentItemOut"] = []
attachments: list[AttachmentOut] = []
reactions: ReactionsOut = ReactionsOut()
class NotificationOut(_Out):
uid: str = ""
type: Optional[str] = None
message: Optional[str] = None
read: Optional[bool] = None
related_uid: Optional[str] = None
target_url: Optional[str] = None
created_at: Optional[str] = None
class MessageOut(_Out):
uid: str = ""
sender_uid: Optional[str] = None
receiver_uid: Optional[str] = None
content: Optional[str] = None
read: Optional[bool] = None
created_at: Optional[str] = None
CommentItemOut.model_rebuild()