intent/templates/comments/_comment.html
Alec Perkins 604c2bbf91
Some checks failed
ci / test (push) Failing after 1m6s
ci / deploy (push) Failing after 0s
Harden for launch: limits, CSP, moderation, admin 2FA (Phase 5)
- Rate limits in the shared cache for comments, issues, uploads and
  invites; a limited HTMX reply keeps the draft.
- Content-Security-Policy (self only, no inline script or style, bucket
  origin for images and frames) and Permissions-Policy on every page.
  htmx configured not to inject styles or eval.
- Moderation: deactivating a user ends their sessions and keeps their
  content; deleted accounts show as "ghost"; reserved usernames; the
  admin won't delete an org's last owner.
- Admin requires a TOTP code (django-otp), with add_admin_totp to enrol.
- Email provider chosen by environment through django-anymail.
- Playwright browser tests: axe WCAG 2.1 AA on every page in light and
  dark, no CSP violations, keyboard walkthrough, theme toggle. Explicit
  Canvas/CanvasText on :root fixes dark-mode contrast detection.
- bin/start creates the data dir if missing; docs/operations.md runbook.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 10:51:06 -04:00

48 lines
2.5 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% comment %}
One comment. Needs `c` (prepared by intent.comments.presenters), `thread`, `project`, `focus`.
Forms post normally; with HTMX they swap the whole thread (`thread.dom_id`) in place.
{% endcomment %}
{% with by=c.author.username|default:"ghost" comment_id="comment-"|add:c.pk %}
<article id="{{ comment_id }}" class="comment" tabindex="-1"{% if focus == comment_id %} data-autofocus{% endif %}>
<header>
<strong>{{ by }}</strong>
<a href="#{{ comment_id }}"><time datetime="{{ c.created_at|date:'c' }}">{{ c.created_at|date:"j M Y, H:i" }}</time></a>
{% if c.edited_at and not c.is_deleted %}
· <a href="{% url 'comments:history' org_slug=project.org.slug project_slug=project.slug comment_id=c.pk %}">edited<span class="visually-hidden"> (see earlier versions of {{ by }}’s comment)</span></a>
{% endif %}
</header>
{% if c.is_deleted %}
<p class="muted"><em>Comment deleted.</em></p>
{% else %}
<div class="prose">{{ c.html }}</div>
{% endif %}
{% if c.can_edit or c.can_delete %}
<div class="actions">
{% if c.can_edit %}
{% url 'comments:edit' org_slug=project.org.slug project_slug=project.slug comment_id=c.pk as edit_url %}
<details>
<summary>Edit<span class="visually-hidden"> {{ by }}’s comment</span></summary>
<form method="post" action="{{ edit_url }}" hx-post="{{ edit_url }}" hx-target="#{{ thread.dom_id }}" hx-swap="outerHTML">
{% csrf_token %}
<label for="edit-{{ c.pk }}" class="visually-hidden">Comment text</label>
<textarea id="edit-{{ c.pk }}" name="body" rows="4" required>{{ c.body }}</textarea>
<button type="submit">Save edit</button>
</form>
</details>
{% endif %}
{% if c.can_delete %}
{% url 'comments:delete' org_slug=project.org.slug project_slug=project.slug comment_id=c.pk as delete_url %}
{# A disclosure rather than a script confirm, so the second step works without JS. #}
<details>
<summary>Delete<span class="visually-hidden"> {{ by }}’s comment</span></summary>
<form method="post" action="{{ delete_url }}" hx-post="{{ delete_url }}" hx-target="#{{ thread.dom_id }}" hx-swap="outerHTML">
{% csrf_token %}
<p>The text and its edit history will be removed. This can't be undone.</p>
<button type="submit">Delete comment</button>
</form>
</details>
{% endif %}
</div>
{% endif %}
</article>
{% endwith %}