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
This commit is contained in:
Jacob Kaplan-Moss 2009-03-21 13:45:31 +00:00
parent e923b545a4
commit 231a7e0419
4 changed files with 143 additions and 75 deletions

View File

@ -13,15 +13,10 @@ from django.utils.translation import ungettext, ugettext_lazy as _
COMMENT_MAX_LENGTH = getattr(settings,'COMMENT_MAX_LENGTH', 3000) COMMENT_MAX_LENGTH = getattr(settings,'COMMENT_MAX_LENGTH', 3000)
class CommentForm(forms.Form): class CommentSecurityForm(forms.Form):
name = forms.CharField(label=_("Name"), max_length=50) """
email = forms.EmailField(label=_("Email address")) Handles the security aspects (anti-spoofing) for comment forms.
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'))
content_type = forms.CharField(widget=forms.HiddenInput) content_type = forms.CharField(widget=forms.HiddenInput)
object_pk = forms.CharField(widget=forms.HiddenInput) object_pk = forms.CharField(widget=forms.HiddenInput)
timestamp = forms.IntegerField(widget=forms.HiddenInput) timestamp = forms.IntegerField(widget=forms.HiddenInput)
@ -32,8 +27,75 @@ class CommentForm(forms.Form):
if initial is None: if initial is None:
initial = {} initial = {}
initial.update(self.generate_security_data()) 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): def get_comment_object(self):
""" """
Return a new (unsaved) comment object based on the information in this Return a new (unsaved) comment object based on the information in this
@ -97,41 +159,6 @@ class CommentForm(forms.Form):
return new 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): def clean_comment(self):
""" """
If COMMENTS_ALLOW_PROFANITIES is False, check that the comment doesn't 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')) get_text_list(['"%s%s%s"' % (i[0], '-'*(len(i)-2), i[-1]) for i in bad_words], 'and'))
return comment return comment
def generate_security_data(self): class CommentForm(CommentDetailsForm):
"""Generate a dict of security data for "initial" data.""" honeypot = forms.CharField(required=False,
timestamp = int(time.time()) label=_('If you enter anything in this field '\
security_dict = { 'your comment will be treated as spam'))
'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): def clean_honeypot(self):
""" """Check that nothing's been entered into the honeypot."""
Generate the initial security hash from self.content_object value = self.cleaned_data["honeypot"]
and a (unix) timestamp. if value:
""" raise forms.ValidationError(self.fields["honeypot"].label)
return value
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()

View File

@ -93,7 +93,12 @@ field::
data['title'] = self.cleaned_data['title'] data['title'] = self.cleaned_data['title']
return data 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.models import CommentWithTitle
from my_comments_app.forms import CommentFormWithTitle 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(): def get_form():
return CommentFormWithTitle 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:
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() .. function:: get_model()

View File

@ -0,0 +1,48 @@
.. _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.

View File

@ -215,3 +215,4 @@ More information
signals signals
upgrade upgrade
custom custom
forms