Commit 231a7e04 authored by Jacob Kaplan-Moss's avatar Jacob Kaplan-Moss
Browse files

Fixed #9958: split the `CommentForm` into a set of smaller forms. This for...

Fixed #9958: split the `CommentForm` into a set of smaller forms. This for better encapsulation, but also so that it's easier for subclasses to get at the pieces they might need. Thanks to Thejaswi Puthraya.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@10110 bcc190cf-cafb-0310-a4f2-bffc1f526a37
parent e923b545
Loading
Loading
Loading
Loading
+82 −72
Original line number Diff line number Diff line
@@ -13,15 +13,10 @@ from django.utils.translation import ungettext, ugettext_lazy as _

COMMENT_MAX_LENGTH = getattr(settings,'COMMENT_MAX_LENGTH', 3000)

class CommentForm(forms.Form):
    name          = forms.CharField(label=_("Name"), max_length=50)
    email         = forms.EmailField(label=_("Email address"))
    url           = forms.URLField(label=_("URL"), required=False)
    comment       = forms.CharField(label=_('Comment'), widget=forms.Textarea,
                                    max_length=COMMENT_MAX_LENGTH)
    honeypot      = forms.CharField(required=False,
                                    label=_('If you enter anything in this field '\
                                            'your comment will be treated as spam'))
class CommentSecurityForm(forms.Form):
    """
    Handles the security aspects (anti-spoofing) for comment forms.
    """
    content_type  = forms.CharField(widget=forms.HiddenInput)
    object_pk     = forms.CharField(widget=forms.HiddenInput)
    timestamp     = forms.IntegerField(widget=forms.HiddenInput)
@@ -32,7 +27,74 @@ class CommentForm(forms.Form):
        if initial is None:
            initial = {}
        initial.update(self.generate_security_data())
        super(CommentForm, self).__init__(data=data, initial=initial)
        super(CommentSecurityForm, self).__init__(data=data, initial=initial)
        
    def security_errors(self):
        """Return just those errors associated with security"""
        errors = ErrorDict()
        for f in ["honeypot", "timestamp", "security_hash"]:
            if f in self.errors:
                errors[f] = self.errors[f]
        return errors

    def clean_security_hash(self):
        """Check the security hash."""
        security_hash_dict = {
            'content_type' : self.data.get("content_type", ""),
            'object_pk' : self.data.get("object_pk", ""),
            'timestamp' : self.data.get("timestamp", ""),
        }
        expected_hash = self.generate_security_hash(**security_hash_dict)
        actual_hash = self.cleaned_data["security_hash"]
        if expected_hash != actual_hash:
            raise forms.ValidationError("Security hash check failed.")
        return actual_hash

    def clean_timestamp(self):
        """Make sure the timestamp isn't too far (> 2 hours) in the past."""
        ts = self.cleaned_data["timestamp"]
        if time.time() - ts > (2 * 60 * 60):
            raise forms.ValidationError("Timestamp check failed")
        return ts

    def generate_security_data(self):
        """Generate a dict of security data for "initial" data."""
        timestamp = int(time.time())
        security_dict =   {
            'content_type'  : str(self.target_object._meta),
            'object_pk'     : str(self.target_object._get_pk_val()),
            'timestamp'     : str(timestamp),
            'security_hash' : self.initial_security_hash(timestamp),
        }
        return security_dict

    def initial_security_hash(self, timestamp):
        """
        Generate the initial security hash from self.content_object
        and a (unix) timestamp.
        """

        initial_security_dict = {
            'content_type' : str(self.target_object._meta),
            'object_pk' : str(self.target_object._get_pk_val()),
            'timestamp' : str(timestamp),
          }
        return self.generate_security_hash(**initial_security_dict)

    def generate_security_hash(self, content_type, object_pk, timestamp):
        """Generate a (SHA1) security hash from the provided info."""
        info = (content_type, object_pk, timestamp, settings.SECRET_KEY)
        return sha_constructor("".join(info)).hexdigest()

class CommentDetailsForm(CommentSecurityForm):
    """
    Handles the specific details of the comment (name, comment, etc.).
    """
    name          = forms.CharField(label=_("Name"), max_length=50)
    email         = forms.EmailField(label=_("Email address"))
    url           = forms.URLField(label=_("URL"), required=False)
    comment       = forms.CharField(label=_('Comment'), widget=forms.Textarea,
                                    max_length=COMMENT_MAX_LENGTH)

    def get_comment_object(self):
        """
@@ -97,41 +159,6 @@ class CommentForm(forms.Form):
                
        return new

    def security_errors(self):
        """Return just those errors associated with security"""
        errors = ErrorDict()
        for f in ["honeypot", "timestamp", "security_hash"]:
            if f in self.errors:
                errors[f] = self.errors[f]
        return errors

    def clean_honeypot(self):
        """Check that nothing's been entered into the honeypot."""
        value = self.cleaned_data["honeypot"]
        if value:
            raise forms.ValidationError(self.fields["honeypot"].label)
        return value

    def clean_security_hash(self):
        """Check the security hash."""
        security_hash_dict = {
            'content_type' : self.data.get("content_type", ""),
            'object_pk' : self.data.get("object_pk", ""),
            'timestamp' : self.data.get("timestamp", ""),
        }
        expected_hash = self.generate_security_hash(**security_hash_dict)
        actual_hash = self.cleaned_data["security_hash"]
        if expected_hash != actual_hash:
            raise forms.ValidationError("Security hash check failed.")
        return actual_hash

    def clean_timestamp(self):
        """Make sure the timestamp isn't too far (> 2 hours) in the past."""
        ts = self.cleaned_data["timestamp"]
        if time.time() - ts > (2 * 60 * 60):
            raise forms.ValidationError("Timestamp check failed")
        return ts

    def clean_comment(self):
        """
        If COMMENTS_ALLOW_PROFANITIES is False, check that the comment doesn't
@@ -148,31 +175,14 @@ class CommentForm(forms.Form):
                    get_text_list(['"%s%s%s"' % (i[0], '-'*(len(i)-2), i[-1]) for i in bad_words], 'and'))
        return comment

    def generate_security_data(self):
        """Generate a dict of security data for "initial" data."""
        timestamp = int(time.time())
        security_dict =   {
            'content_type'  : str(self.target_object._meta),
            'object_pk'     : str(self.target_object._get_pk_val()),
            'timestamp'     : str(timestamp),
            'security_hash' : self.initial_security_hash(timestamp),
        }
        return security_dict

    def initial_security_hash(self, timestamp):
        """
        Generate the initial security hash from self.content_object
        and a (unix) timestamp.
        """

        initial_security_dict = {
            'content_type' : str(self.target_object._meta),
            'object_pk' : str(self.target_object._get_pk_val()),
            'timestamp' : str(timestamp),
          }
        return self.generate_security_hash(**initial_security_dict)
class CommentForm(CommentDetailsForm):
    honeypot      = forms.CharField(required=False,
                                    label=_('If you enter anything in this field '\
                                            'your comment will be treated as spam'))

    def generate_security_hash(self, content_type, object_pk, timestamp):
        """Generate a (SHA1) security hash from the provided info."""
        info = (content_type, object_pk, timestamp, settings.SECRET_KEY)
        return sha_constructor("".join(info)).hexdigest()
    def clean_honeypot(self):
        """Check that nothing's been entered into the honeypot."""
        value = self.cleaned_data["honeypot"]
        if value:
            raise forms.ValidationError(self.fields["honeypot"].label)
        return value
+12 −3
Original line number Diff line number Diff line
@@ -93,7 +93,12 @@ field::
            data['title'] = self.cleaned_data['title']
            return data

Finally, we'll define a couple of methods in ``my_custom_app/__init__.py`` to point Django at these classes we've created::
Django provides a couple of "helper" classes to make writing certain types of
custom comment forms easier; see :mod:`django.contrib.comments.forms` for
more.

Finally, we'll define a couple of methods in ``my_custom_app/__init__.py`` to
point Django at these classes we've created::

    from my_comments_app.models import CommentWithTitle
    from my_comments_app.forms import CommentFormWithTitle
@@ -104,14 +109,18 @@ Finally, we'll define a couple of methods in ``my_custom_app/__init__.py`` to po
    def get_form():
        return CommentFormWithTitle

The above process should take care of most common situations. For more advanced usage, there are additional methods you can define. Those are explained in the next section.
The above process should take care of most common situations. For more
advanced usage, there are additional methods you can define. Those are
explained in the next section.

.. _custom-comment-app-api:

Custom comment app API
======================

The :mod:`django.contrib.comments` app defines the following methods; any custom comment app must define at least one of them. All are optional, however.
The :mod:`django.contrib.comments` app defines the following methods; any
custom comment app must define at least one of them. All are optional,
however.

.. function:: get_model()

+48 −0
Original line number Diff line number Diff line
.. _ref-contrib-comments-forms:

====================
Comment form classes
====================

.. module:: django.contrib.comments.forms
   :synopsis: Forms for dealing with the built-in comment model.

The ``django.contrib.comments.forms`` module contains a handful of forms
you'll use when writing custom views dealing with comments, or when writing
:ref:`custom comment apps <ref-contrib-comments-custom>`.

.. class:: CommentForm

   The main comment form representing the standard, built-in way of handling
   submitted comments. This is the class used by all the views
   :mod:`django.contrib.comments` to handle submitted comments.

   If you want to build custom views that are similar to Django's built-in
   comment handling views, you'll probably want to use this form.

Abstract comment forms for custom comment apps
----------------------------------------------

If you're building a :ref:`custom comment app <ref-contrib-comments-custom>`,
you might want to replace *some* of the form logic but still rely on parts of
the existing form.

:class:`CommentForm` is actually composed of a couple of abstract base class
forms that you can subclass to reuse pieces of the form handling logic:

.. class:: CommentSecurityForm

   Handles the anti-spoofing protection aspects of the comment form handling.

   This class contains the ``content_type`` and ``object_pk`` fields pointing
   to the object the comment is attached to, along with a ``timestamp`` and a
   ``security_hash`` of all the form data. Together, the timestamp and the
   security hash ensure that spammers can't "replay" form submissions and
   flood you with comments.

.. class:: CommentDetailsForm

   Handles the details of the comment itself.

   This class contains the ``name``, ``email``, ``url``, and the ``comment``
   field itself, along with the associated valdation logic.
 No newline at end of file
+1 −0
Original line number Diff line number Diff line
@@ -215,3 +215,4 @@ More information
   signals
   upgrade
   custom
   forms