From 6fcedcf080814d1aabecfe93f8cd5704487d48e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Leichtfu=C3=9F?= Date: Mon, 12 Jul 2021 22:13:47 +0200 Subject: [PATCH 1/4] rebuild the comment post view using the generic FormView class We tried to strictly follow the logic of the original code and keep as much of it as possible while just migrating the code into the structure of the generic FormView. --- django_comments/urls.py | 4 +- django_comments/views/comments.py | 225 +++++++++++++++++++----------- 2 files changed, 144 insertions(+), 85 deletions(-) diff --git a/django_comments/urls.py b/django_comments/urls.py index f5ccf41..4e3eab1 100644 --- a/django_comments/urls.py +++ b/django_comments/urls.py @@ -1,14 +1,14 @@ from django.contrib.contenttypes.views import shortcut from django.urls import path, re_path -from .views.comments import post_comment, comment_done +from .views.comments import CommentPostView, comment_done from .views.moderation import ( flag, flag_done, delete, delete_done, approve, approve_done, ) urlpatterns = [ - path('post/', post_comment, name='comments-post-comment'), + path('post/', CommentPostView.as_view(), name='comments-post-comment'), path('posted/', comment_done, name='comments-comment-done'), path('flag//', flag, name='comments-flag'), path('flagged/', flag_done, name='comments-flag-done'), diff --git a/django_comments/views/comments.py b/django_comments/views/comments.py index 185231d..2645a45 100644 --- a/django_comments/views/comments.py +++ b/django_comments/views/comments.py @@ -1,3 +1,5 @@ +from urllib.parse import urlencode + from django import http from django.apps import apps from django.conf import settings @@ -6,12 +8,25 @@ from django.shortcuts import render from django.template.loader import render_to_string from django.utils.html import escape +from django.utils.decorators import method_decorator from django.views.decorators.csrf import csrf_protect -from django.views.decorators.http import require_POST +from django.views.generic.edit import FormView +from django.shortcuts import render, resolve_url + +from ..compat import url_has_allowed_host_and_scheme import django_comments from django_comments import signals -from django_comments.views.utils import next_redirect, confirmation_view +from django_comments.views.utils import confirmation_view + + +class BadRequest(Exception): + """ + Exception raised for a bad post request holding the CommentPostBadRequest + object. + """ + def __init__(self, response): + self.response = response class CommentPostBadRequest(http.HttpResponseBadRequest): @@ -27,59 +42,102 @@ def __init__(self, why): self.content = render_to_string("comments/400-debug.html", {"why": why}) -@csrf_protect -@require_POST -def post_comment(request, next=None, using=None): - """ - Post a comment. +class CommentPostView(FormView): + http_method_names = ['post'] + + def get_target_object(self, data): + # Look up the object we're trying to comment about + ctype = data.get("content_type") + object_pk = data.get("object_pk") + if ctype is None or object_pk is None: + raise BadRequest(CommentPostBadRequest("Missing content_type or object_pk field.")) + try: + model = apps.get_model(*ctype.split(".", 1)) + return model._default_manager.using(self.kwargs.get('using')).get(pk=object_pk) + except TypeError: + raise BadRequest(CommentPostBadRequest( + "Invalid content_type value: %r" % escape(ctype))) + except AttributeError: + raise BadRequest(CommentPostBadRequest( + "The given content-type %r does not resolve to a valid model." % escape(ctype))) + except ObjectDoesNotExist: + raise BadRequest(CommentPostBadRequest( + "No object matching content-type %r and object PK %r exists." % ( + escape(ctype), escape(object_pk)))) + except (ValueError, ValidationError) as e: + raise BadRequest(CommentPostBadRequest( + "Attempting to get content-type %r and object PK %r raised %s" % ( + escape(ctype), escape(object_pk), e.__class__.__name__))) + + def get_form_kwargs(self): + data = self.request.POST.copy() + if self.request.user.is_authenticated: + if not data.get('name', ''): + data["name"] = self.request.user.get_full_name() or self.request.user.get_username() + if not data.get('email', ''): + data["email"] = self.request.user.email + return data + + def get_form_class(self): + """Return the form class to use.""" + return django_comments.get_form() + + def get_form(self, form_class=None): + """Return an instance of the form to be used in this view.""" + if form_class is None: + form_class = self.get_form_class() + return form_class(self.target_object, data=self.data) + + def get_success_url(self): + """Return the URL to redirect to after processing a valid form.""" + fallback = self.kwargs.get('next') or 'comments-comment-done' + get_kwargs = dict(c=self.object._get_pk_val()) + next = self.request.POST.get('next') + + if not url_has_allowed_host_and_scheme(url=next, allowed_hosts={self.request.get_host()}): + next = resolve_url(fallback) + + if '#' in next: + tmp = next.rsplit('#', 1) + next = tmp[0] + anchor = '#' + tmp[1] + else: + anchor = '' + + joiner = ('?' in next) and '&' or '?' + next += joiner + urlencode(get_kwargs) + anchor + + return next + + def create_comment(self, form): + comment = form.get_comment_object(site_id=get_current_site(self.request).id) + comment.ip_address = self.request.META.get("REMOTE_ADDR", None) or None + if self.request.user.is_authenticated: + comment.user = self.request.user + + # Signal that the comment is about to be saved + responses = signals.comment_will_be_posted.send( + sender=comment.__class__, + comment=comment, + request=self.request + ) - HTTP POST is required. If ``POST['submit'] == "preview"`` or if there are - errors a preview template, ``comments/preview.html``, will be rendered. - """ - # Fill out some initial data fields from an authenticated user, if present - data = request.POST.copy() - if request.user.is_authenticated: - if not data.get('name', ''): - data["name"] = request.user.get_full_name() or request.user.get_username() - if not data.get('email', ''): - data["email"] = request.user.email - - # Look up the object we're trying to comment about - ctype = data.get("content_type") - object_pk = data.get("object_pk") - if ctype is None or object_pk is None: - return CommentPostBadRequest("Missing content_type or object_pk field.") - try: - model = apps.get_model(*ctype.split(".", 1)) - target = model._default_manager.using(using).get(pk=object_pk) - except TypeError: - return CommentPostBadRequest( - "Invalid content_type value: %r" % escape(ctype)) - except AttributeError: - return CommentPostBadRequest( - "The given content-type %r does not resolve to a valid model." % escape(ctype)) - except ObjectDoesNotExist: - return CommentPostBadRequest( - "No object matching content-type %r and object PK %r exists." % ( - escape(ctype), escape(object_pk))) - except (ValueError, ValidationError) as e: - return CommentPostBadRequest( - "Attempting to get content-type %r and object PK %r raised %s" % ( - escape(ctype), escape(object_pk), e.__class__.__name__)) - - # Do we want to preview the comment? - preview = "preview" in data - - # Construct the comment form - form = django_comments.get_form()(target, data=data) - - # Check security information - if form.security_errors(): - return CommentPostBadRequest( - "The comment form failed security verification: %s" % escape(str(form.security_errors()))) - - # If there are errors or if we requested a preview show the comment - if form.errors or preview: + for (receiver, response) in responses: + if response is False: + raise BadRequest(CommentPostBadRequest( + "comment_will_be_posted receiver %r killed the comment" % receiver.__name__)) + + # Save the comment and signal that it was saved + comment.save() + signals.comment_was_posted.send( + sender=comment.__class__, + comment=comment, + request=self.request + ) + return comment + + def form_invalid(self, form): + model = type(self.target_object) template_list = [ # These first two exist for purely historical reasons. # Django v1.0 and v1.1 allowed the underscore format for @@ -91,41 +149,42 @@ def post_comment(request, next=None, using=None): "comments/%s/preview.html" % model._meta.app_label, "comments/preview.html", ] - return render(request, template_list, { + return render(self.request, template_list, { "comment": form.data.get("comment", ""), "form": form, - "next": data.get("next", next), + "next": self.data.get("next", self.kwargs.get('next')), }, ) - # Otherwise create the comment - comment = form.get_comment_object(site_id=get_current_site(request).id) - comment.ip_address = request.META.get("REMOTE_ADDR", None) or None - if request.user.is_authenticated: - comment.user = request.user - - # Signal that the comment is about to be saved - responses = signals.comment_will_be_posted.send( - sender=comment.__class__, - comment=comment, - request=request - ) - - for (receiver, response) in responses: - if response is False: + @method_decorator(csrf_protect) + def dispatch(self, *args, **kwargs): + return super().dispatch(*args, **kwargs) + + def post(self, request, **kwargs): + self.object = None + self.target_object = None + self.data = self.get_form_kwargs() + try: + self.target_object = self.get_target_object(self.data) + except BadRequest as exc: + return exc.response + + form = self.get_form() + + # Check security information + if form.security_errors(): return CommentPostBadRequest( - "comment_will_be_posted receiver %r killed the comment" % receiver.__name__) - - # Save the comment and signal that it was saved - comment.save() - signals.comment_was_posted.send( - sender=comment.__class__, - comment=comment, - request=request - ) - - return next_redirect(request, fallback=next or 'comments-comment-done', - c=comment._get_pk_val()) + "The comment form failed security verification: %s" % escape(str(form.security_errors()))) + + if not form.is_valid() or "preview" in self.data: + return self.form_invalid(form) + else: + try: + self.object = self.create_comment(form) + except BadRequest as exc: + return exc.response + else: + return self.form_valid(form) comment_done = confirmation_view( From eb9ca70c9fc9d07d517a52877467a5383f1eac64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Leichtfu=C3=9F?= Date: Tue, 13 Jul 2021 08:09:19 +0200 Subject: [PATCH 2/4] made the use of the BadRequest exception more straight foreward Instead of passing the CommentPostBadRequest object we just initialize it within the BadRequest itself. --- django_comments/views/comments.py | 39 ++++++++++++++----------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/django_comments/views/comments.py b/django_comments/views/comments.py index 2645a45..f78e449 100644 --- a/django_comments/views/comments.py +++ b/django_comments/views/comments.py @@ -20,15 +20,6 @@ from django_comments.views.utils import confirmation_view -class BadRequest(Exception): - """ - Exception raised for a bad post request holding the CommentPostBadRequest - object. - """ - def __init__(self, response): - self.response = response - - class CommentPostBadRequest(http.HttpResponseBadRequest): """ Response returned when a comment post is invalid. If ``DEBUG`` is on a @@ -42,6 +33,15 @@ def __init__(self, why): self.content = render_to_string("comments/400-debug.html", {"why": why}) +class BadRequest(Exception): + """ + Exception raised for a bad post request holding the CommentPostBadRequest + object. + """ + def __init__(self, why): + self.response = CommentPostBadRequest(why) + + class CommentPostView(FormView): http_method_names = ['post'] @@ -50,24 +50,20 @@ def get_target_object(self, data): ctype = data.get("content_type") object_pk = data.get("object_pk") if ctype is None or object_pk is None: - raise BadRequest(CommentPostBadRequest("Missing content_type or object_pk field.")) + raise BadRequest("Missing content_type or object_pk field.") try: model = apps.get_model(*ctype.split(".", 1)) return model._default_manager.using(self.kwargs.get('using')).get(pk=object_pk) except TypeError: - raise BadRequest(CommentPostBadRequest( - "Invalid content_type value: %r" % escape(ctype))) + raise BadRequest("Invalid content_type value: %r" % escape(ctype)) except AttributeError: - raise BadRequest(CommentPostBadRequest( - "The given content-type %r does not resolve to a valid model." % escape(ctype))) + raise BadRequest("The given content-type %r does not resolve to a valid model." % escape(ctype)) except ObjectDoesNotExist: - raise BadRequest(CommentPostBadRequest( - "No object matching content-type %r and object PK %r exists." % ( - escape(ctype), escape(object_pk)))) + raise BadRequest("No object matching content-type %r and object PK %r exists." % ( + escape(ctype), escape(object_pk))) except (ValueError, ValidationError) as e: - raise BadRequest(CommentPostBadRequest( - "Attempting to get content-type %r and object PK %r raised %s" % ( - escape(ctype), escape(object_pk), e.__class__.__name__))) + raise BadRequest("Attempting to get content-type %r and object PK %r raised %s" % ( + escape(ctype), escape(object_pk), e.__class__.__name__)) def get_form_kwargs(self): data = self.request.POST.copy() @@ -124,8 +120,7 @@ def create_comment(self, form): for (receiver, response) in responses: if response is False: - raise BadRequest(CommentPostBadRequest( - "comment_will_be_posted receiver %r killed the comment" % receiver.__name__)) + raise BadRequest("comment_will_be_posted receiver %r killed the comment" % receiver.__name__) # Save the comment and signal that it was saved comment.save() From 9ca91bbf95dc37040fac5d10abd2f90ea98c8207 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Leichtfu=C3=9F?= Date: Tue, 13 Jul 2021 09:06:08 +0200 Subject: [PATCH 3/4] use the TemplateResponseMixin logic for rendering a the comment form --- django_comments/views/comments.py | 40 +++++++++++++++++-------------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/django_comments/views/comments.py b/django_comments/views/comments.py index f78e449..a5488f2 100644 --- a/django_comments/views/comments.py +++ b/django_comments/views/comments.py @@ -131,24 +131,28 @@ def create_comment(self, form): ) return comment - def form_invalid(self, form): - model = type(self.target_object) - template_list = [ - # These first two exist for purely historical reasons. - # Django v1.0 and v1.1 allowed the underscore format for - # preview templates, so we have to preserve that format. - "comments/%s_%s_preview.html" % (model._meta.app_label, model._meta.model_name), - "comments/%s_preview.html" % model._meta.app_label, - # Now the usual directory based template hierarchy. - "comments/%s/%s/preview.html" % (model._meta.app_label, model._meta.model_name), - "comments/%s/preview.html" % model._meta.app_label, - "comments/preview.html", - ] - return render(self.request, template_list, { - "comment": form.data.get("comment", ""), - "form": form, - "next": self.data.get("next", self.kwargs.get('next')), - }, + def get_template_names(self): + if self.template_name is None: + model = type(self.target_object) + return [ + # These first two exist for purely historical reasons. + # Django v1.0 and v1.1 allowed the underscore format for + # preview templates, so we have to preserve that format. + "comments/%s_%s_preview.html" % (model._meta.app_label, model._meta.model_name), + "comments/%s_preview.html" % model._meta.app_label, + # Now the usual directory based template hierarchy. + "comments/%s/%s/preview.html" % (model._meta.app_label, model._meta.model_name), + "comments/%s/preview.html" % model._meta.app_label, + "comments/preview.html", + ] + else: + return [self.template_name] + + def get_context_data(self, form): + return dict( + form=form, + comment=form.data.get("comment", ""), + next=self.data.get("next", self.kwargs.get('next')), ) @method_decorator(csrf_protect) From dd5724db1b80c682dff29344383093a37e109bd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Leichtfu=C3=9F?= Date: Tue, 13 Jul 2021 19:01:41 +0200 Subject: [PATCH 4/4] just smooth the code a bit --- django_comments/views/comments.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_comments/views/comments.py b/django_comments/views/comments.py index a5488f2..6808b92 100644 --- a/django_comments/views/comments.py +++ b/django_comments/views/comments.py @@ -86,9 +86,9 @@ def get_form(self, form_class=None): def get_success_url(self): """Return the URL to redirect to after processing a valid form.""" + next = self.data.get('next') fallback = self.kwargs.get('next') or 'comments-comment-done' get_kwargs = dict(c=self.object._get_pk_val()) - next = self.request.POST.get('next') if not url_has_allowed_host_and_scheme(url=next, allowed_hosts={self.request.get_host()}): next = resolve_url(fallback)