feat: add soft delete and media tab for user attachments with admin restore
This commit is contained in:
@@ -105,7 +105,7 @@ Docs (`routers/docs.py` `DOCS_PAGES`) entries take optional `admin: True` (hidde
|
||||
- All routers MUST import the shared `templates` from `devplacepy.templating`. Do NOT instantiate `Jinja2Templates` per router - globals (`avatar_url`, `get_unread_count`, `get_user_projects`, `format_date`) are registered on that single instance.
|
||||
- Templates extend `base.html`, add page CSS via `{% block extra_head %}`, page JS via `{% block extra_js %}`.
|
||||
- All JS in `static/js/` is ES6 modules, one class per file, instantiated as `app` and reachable everywhere. The browser-side application root is `Application.js`.
|
||||
- **Custom web components** live in `static/js/components/` (prefix `dp-`): `dp-avatar`, `dp-code`, `dp-content` (wraps the `contentRenderer` markdown/sanitize/media engine behind `data-render`), `dp-toast`, `dp-dialog`, `dp-context-menu`, `dp-upload` (the single file-upload button used everywhere; replaced the old `AttachmentUploader`). Each extends `Component` (a thin `HTMLElement` base with `attr`/`boolAttr`/`intAttr` helpers), renders into the **light DOM** (no shadow root, so global CSS applies) and self-registers via `customElements.define` at the bottom of its file. `components/index.js` imports them all and is imported by `Application.js`, so every element is defined site-wide. The singletons are created once in `Application.js` and exposed as `app.dialog` / `app.contextMenu` / `app.toast`. Self-contained presentational widgets become components; partial-bound controllers (votes, reactions, comments) and pure utilities stay plain modules because they enhance server-rendered markup rather than render standalone UI. They are documented in the public docs **Components** section.
|
||||
- **Custom web components** live in `static/js/components/` (prefix `dp-`): `dp-avatar`, `dp-code`, `dp-content` (wraps the `contentRenderer` markdown/sanitize/media engine behind `data-render`), `dp-toast`, `dp-dialog`, `dp-context-menu`, `dp-upload` (the single file-upload button used everywhere; replaced the old `AttachmentUploader`), `dp-lightbox` (`app.lightbox`: the single image lightbox, attribute-wired via one delegated listener so any `img[data-lightbox]` opens full-size - `data-full` for a distinct full-res URL; attachment thumbnails and all `data-render` markdown images are marked centrally, the latter in `ContentEnhancer`). Each extends `Component` (a thin `HTMLElement` base with `attr`/`boolAttr`/`intAttr` helpers), renders into the **light DOM** (no shadow root, so global CSS applies) and self-registers via `customElements.define` at the bottom of its file. `components/index.js` imports them all and is imported by `Application.js`, so every element is defined site-wide. The singletons are created once in `Application.js` and exposed as `app.dialog` / `app.contextMenu` / `app.toast`. Self-contained presentational widgets become components; partial-bound controllers (votes, reactions, comments) and pure utilities stay plain modules because they enhance server-rendered markup rather than render standalone UI. They are documented in the public docs **Components** section.
|
||||
- **Shared frontend utilities (do not re-implement these):** `Http` (`static/js/Http.js`) is the single fetch helper (`getJson`, `sendForm`, and `send` which throws `error.message` on non-2xx or a `200 {ok:false}` body); every live-update loop uses `Poller` (`new Poller(fn, intervalMs, {pauseHidden})`); async-job status polls use `JobPoller.run(statusUrl, {onDone, onFailed, onTimeout})` (`ProjectForker`, `ZipDownloader`); click-to-POST engagement controllers (`VoteManager`/`ReactionBar`/`BookmarkManager`/`PollManager`) extend `OptimisticAction` and call `this.submit(url, params, errorTarget, render)`. Floating windows (container terminals and Devii) extend `FloatingWindow`. See `AGENTS.md` -> "Shared frontend utilities (DRY)".
|
||||
- CDN scripts in `base.html` (marked, highlight.js, emoji-picker-element) MUST use `defer` or `type="module"` - otherwise `wait_until="domcontentloaded"` in Playwright tests will time out.
|
||||
- For clickable avatars/usernames, reuse `templates/_avatar_link.html` and `templates/_user_link.html` via `{% include %}` with `_user`, `_size`, `_size_class` locals.
|
||||
@@ -182,6 +182,10 @@ Devii partially configures its OWN system message: every system prompt ends with
|
||||
|
||||
The `comments` table uses `(target_type, target_uid)` so the same `_comment_section.html` component works for `post`, `project`, `gist`, and `news`. Votes follow the same shape via `/votes/{target_type}/{uid}`. `resolve_target_redirect()` in `comments.py` maps target_type back to the correct detail URL.
|
||||
|
||||
## Media (attachments) soft delete
|
||||
|
||||
The profile **Media** tab lists every attachment a user uploaded (`get_user_media`); a single upload can be removed via `POST /media/{uid}/delete` (owner or admin) as a **soft delete** - it stamps a `deleted_at` column WITHOUT touching the `target_type`/`target_uid` relation or unlinking the file, so the item disappears from every display surface (the gallery AND its parent post/project) yet stays restorable. The three user-facing read paths (`get_attachments`, `get_attachments_batch`, `get_user_media`) filter `deleted_at IS NULL`; the hard-delete cascades (`delete_attachments_for`, `delete_target_attachments`) stay UNFILTERED. The only hard deletes from the UI are admin **Media** trash purge (`/admin/media`, `POST /admin/media/{uid}/purge`) and parent-object deletion. See `AGENTS.md` -> "Profile media gallery and soft-deleted attachments".
|
||||
|
||||
## Testing
|
||||
|
||||
Playwright (NOT pytest-playwright - uninstall if present, it conflicts). 419 tests across 34 files in `tests/` (mixed Playwright UI tests and in-process `TestClient`/asyncio unit tests). The fixture stack:
|
||||
|
||||
Reference in New Issue
Block a user