- Replace nav inline newsletter form with Subscribe CTA link per wireframe - Remove newsletter form from footer; add Connect section with social/RSS links - Fix honeypot inputs using hidden attribute (inline style blocked by CSP) - Add available_tags to HomePage.get_context for Explore Topics section - Add data-comment-form attribute to main comment form for reliable locating - Seed approved comment in E2E content for reply flow testing - Expand test_comments.py: moderation message, not-immediately-visible, missing fields, reply form visible, reply submission - Make COMMENT_RATE_LIMIT_PER_MINUTE configurable; set 100 in dev to prevent E2E test exhaustion; update rate limit unit test with override_settings - Update newsletter/home E2E tests to reflect nav form removal - Update unit test to assert no nav/footer newsletter forms Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
65 lines
2.5 KiB
Python
65 lines
2.5 KiB
Python
from __future__ import annotations
|
|
|
|
from django.conf import settings
|
|
from django.contrib import messages
|
|
from django.core.cache import cache
|
|
from django.core.exceptions import ValidationError
|
|
from django.http import HttpResponse
|
|
from django.shortcuts import get_object_or_404, redirect, render
|
|
from django.views import View
|
|
|
|
from apps.blog.models import ArticlePage
|
|
from apps.comments.forms import CommentForm
|
|
from apps.comments.models import Comment
|
|
|
|
|
|
def client_ip_from_request(request) -> str:
|
|
remote_addr = request.META.get("REMOTE_ADDR", "").strip()
|
|
trusted_proxies = getattr(settings, "TRUSTED_PROXY_IPS", [])
|
|
x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR", "")
|
|
if remote_addr in trusted_proxies and x_forwarded_for:
|
|
return x_forwarded_for.split(",")[0].strip()
|
|
return remote_addr
|
|
|
|
|
|
class CommentCreateView(View):
|
|
def _render_article_with_errors(self, request, article, form):
|
|
context = article.get_context(request)
|
|
context["page"] = article
|
|
context["comment_form"] = form
|
|
return render(request, "blog/article_page.html", context, status=200)
|
|
|
|
def post(self, request):
|
|
ip = client_ip_from_request(request)
|
|
key = f"comment-rate:{ip}"
|
|
count = cache.get(key, 0)
|
|
rate_limit = getattr(settings, "COMMENT_RATE_LIMIT_PER_MINUTE", 3)
|
|
if count >= rate_limit:
|
|
return HttpResponse(status=429)
|
|
cache.set(key, count + 1, timeout=60)
|
|
|
|
form = CommentForm(request.POST)
|
|
article = get_object_or_404(ArticlePage, pk=request.POST.get("article_id"))
|
|
if not article.comments_enabled:
|
|
return HttpResponse(status=404)
|
|
|
|
if form.is_valid():
|
|
if form.cleaned_data.get("honeypot"):
|
|
return redirect(f"{article.url}?commented=1")
|
|
comment = form.save(commit=False)
|
|
comment.article = article
|
|
parent_id = form.cleaned_data.get("parent_id")
|
|
if parent_id:
|
|
comment.parent = Comment.objects.filter(pk=parent_id, article=article).first()
|
|
comment.ip_address = ip or None
|
|
try:
|
|
comment.full_clean()
|
|
except ValidationError:
|
|
form.add_error(None, "Reply depth exceeds the allowed limit")
|
|
return self._render_article_with_errors(request, article, form)
|
|
comment.save()
|
|
messages.success(request, "Your comment is awaiting moderation")
|
|
return redirect(f"{article.url}?commented=1")
|
|
|
|
return self._render_article_with_errors(request, article, form)
|