fix: resolve review round 2, E2E failures, and mypy error
Some checks failed
CI / nightly-e2e (pull_request) Has been skipped
CI / deploy (pull_request) Has been skipped
CI / pr-e2e (pull_request) Successful in 1m30s
CI / ci (pull_request) Failing after 1m48s

Review blocker A — form error swap and false success:
- Change HTMX contract so forms target their own container (outerHTML)
  instead of appending to #comments-list
- Use OOB swaps to append approved comments to the correct target
- Add success/error message display inside form templates
- Remove hx-on::after-request handlers (no longer needed)

Review blocker B — reply rendering shape:
- Create _reply.html partial with compact reply markup
- Approved replies via HTMX now use compact template + OOB swap
  into parent's .replies-container
- Reply form errors render inside reply form container

E2E test fixes:
- Update 4 failing tests to wait for inline HTMX messages instead
  of redirect-based URL assertions
- Add aria-label='Comment form errors' to form error display
- Rename test_reply_submission_redirects to
  test_reply_submission_shows_moderation_message

Mypy internal error workaround:
- Add mypy override for apps.comments.views (django-stubs triggers
  internal error on ORM annotate() chain with mypy 1.11.2)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Mark
2026-03-03 23:47:12 +00:00
parent 88ce59aecc
commit c01fc14258
8 changed files with 188 additions and 79 deletions

View File

@@ -152,13 +152,6 @@
</div>
{% endif %}
{% if comment_form and comment_form.errors %}
<div aria-label="Comment form errors" class="mb-6 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 font-mono text-sm text-red-600 dark:text-red-400">
{{ comment_form.non_field_errors }}
{% for field in comment_form %}{{ field.errors }}{% endfor %}
</div>
{% endif %}
{% include "comments/_comment_form.html" %}
</section>
{% endif %}

View File

@@ -1,9 +1,19 @@
{% load static %}
<div id="comment-form-container" class="bg-brand-surfaceLight dark:bg-brand-surfaceDark border border-zinc-200 dark:border-zinc-800 p-6">
<h3 class="font-display font-bold text-xl mb-6">Post a Comment</h3>
{% if success_message %}
<div class="mb-4 p-3 font-mono text-sm bg-brand-cyan/10 text-brand-cyan border border-brand-cyan/20">
{{ success_message }}
</div>
{% endif %}
{% if comment_form.errors %}
<div aria-label="Comment form errors" class="mb-4 p-3 font-mono text-sm bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 border border-red-200 dark:border-red-800">
{% for error in comment_form.non_field_errors %}<p>{{ error }}</p>{% endfor %}
{% for field in comment_form %}{% for error in field.errors %}<p>{{ field.label }}: {{ error }}</p>{% endfor %}{% endfor %}
</div>
{% endif %}
<form method="post" action="{% url 'comment_post' %}" data-comment-form class="space-y-4"
hx-post="{% url 'comment_post' %}" hx-target="#comments-list" hx-swap="beforeend"
hx-on::after-request="if(event.detail.successful) { this.reset(); document.getElementById('comment-success')?.remove(); this.insertAdjacentHTML('beforebegin', '<div id=&quot;comment-success&quot; class=&quot;mb-4 p-3 font-mono text-sm bg-brand-cyan/10 text-brand-cyan border border-brand-cyan/20&quot;>Comment posted!</div>'); }">
hx-post="{% url 'comment_post' %}" hx-target="#comment-form-container" hx-swap="outerHTML">
{% csrf_token %}
<input type="hidden" name="article_id" value="{{ page.id }}" />
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">

View File

@@ -0,0 +1,10 @@
<article id="comment-{{ comment.id }}" class="mt-6 ml-8 bg-zinc-50 dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 border-l-2 border-l-brand-cyan p-4">
<div class="flex items-center gap-3 mb-2">
<div class="w-7 h-7 bg-gradient-to-tr from-brand-pink to-brand-cyan shrink-0"></div>
<div>
<div class="font-display font-bold text-sm">{{ comment.author_name }}</div>
<div class="font-mono text-xs text-zinc-500">{{ comment.created_at|date:"M j, Y" }}</div>
</div>
</div>
<p class="text-zinc-700 dark:text-zinc-300 text-sm leading-relaxed">{{ comment.body }}</p>
</article>

View File

@@ -1,20 +1,30 @@
{% load static %}
<form method="post" action="{% url 'comment_post' %}" class="mt-4 pt-4 border-t border-zinc-100 dark:border-zinc-800"
hx-post="{% url 'comment_post' %}" hx-target="#comments-list" hx-swap="beforeend" hx-on::after-request="if(event.detail.successful) this.reset()">
{% csrf_token %}
<input type="hidden" name="article_id" value="{{ page.id }}" />
<input type="hidden" name="parent_id" value="{{ comment.id }}" />
<div class="flex gap-3 mb-3">
<input type="text" name="author_name" required placeholder="Your name"
class="flex-1 bg-transparent border border-zinc-300 dark:border-zinc-700 px-3 py-2 font-mono text-sm focus:outline-none focus:border-brand-pink focus:shadow-neon-pink transition-colors" />
<input type="email" name="author_email" required placeholder="your@email.com"
class="flex-1 bg-transparent border border-zinc-300 dark:border-zinc-700 px-3 py-2 font-mono text-sm focus:outline-none focus:border-brand-pink focus:shadow-neon-pink transition-colors" />
</div>
<textarea name="body" required placeholder="Write a reply..." rows="2"
class="w-full bg-transparent border border-zinc-300 dark:border-zinc-700 px-3 py-2 font-mono text-sm focus:outline-none focus:border-brand-pink focus:shadow-neon-pink transition-colors mb-3 resize-none"></textarea>
<input type="text" name="honeypot" hidden />
{% if turnstile_site_key %}
<div class="cf-turnstile mb-3" data-sitekey="{{ turnstile_site_key }}" data-theme="auto" data-size="compact"></div>
<div id="reply-form-container-{{ comment.id }}" class="mt-4 pt-4 border-t border-zinc-100 dark:border-zinc-800">
{% if reply_success_message %}
<div class="mb-3 p-2 font-mono text-sm bg-brand-cyan/10 text-brand-cyan border border-brand-cyan/20">{{ reply_success_message }}</div>
{% endif %}
<button type="submit" class="px-4 py-2 bg-zinc-200 dark:bg-zinc-800 font-display font-bold text-sm hover:bg-brand-pink hover:text-white transition-colors">Reply</button>
</form>
{% if reply_form_errors %}
<div aria-label="Comment form errors" class="mb-3 p-2 font-mono text-sm bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 border border-red-200 dark:border-red-800">
{% for field, errors in reply_form_errors.items %}{% for error in errors %}<p>{{ error }}</p>{% endfor %}{% endfor %}
</div>
{% endif %}
<form method="post" action="{% url 'comment_post' %}"
hx-post="{% url 'comment_post' %}" hx-target="#reply-form-container-{{ comment.id }}" hx-swap="outerHTML">
{% csrf_token %}
<input type="hidden" name="article_id" value="{{ page.id }}" />
<input type="hidden" name="parent_id" value="{{ comment.id }}" />
<div class="flex gap-3 mb-3">
<input type="text" name="author_name" required placeholder="Your name"
class="flex-1 bg-transparent border border-zinc-300 dark:border-zinc-700 px-3 py-2 font-mono text-sm focus:outline-none focus:border-brand-pink focus:shadow-neon-pink transition-colors" />
<input type="email" name="author_email" required placeholder="your@email.com"
class="flex-1 bg-transparent border border-zinc-300 dark:border-zinc-700 px-3 py-2 font-mono text-sm focus:outline-none focus:border-brand-pink focus:shadow-neon-pink transition-colors" />
</div>
<textarea name="body" required placeholder="Write a reply..." rows="2"
class="w-full bg-transparent border border-zinc-300 dark:border-zinc-700 px-3 py-2 font-mono text-sm focus:outline-none focus:border-brand-pink focus:shadow-neon-pink transition-colors mb-3 resize-none"></textarea>
<input type="text" name="honeypot" hidden />
{% if turnstile_site_key %}
<div class="cf-turnstile mb-3" data-sitekey="{{ turnstile_site_key }}" data-theme="auto" data-size="compact"></div>
{% endif %}
<button type="submit" class="px-4 py-2 bg-zinc-200 dark:bg-zinc-800 font-display font-bold text-sm hover:bg-brand-pink hover:text-white transition-colors">Reply</button>
</form>
</div>