app/django/contrib/comments/views/comments.py
changeset 323 ff1a9aa48cfd
parent 54 03e267d67478
--- a/app/django/contrib/comments/views/comments.py	Tue Oct 14 12:36:55 2008 +0000
+++ b/app/django/contrib/comments/views/comments.py	Tue Oct 14 16:00:59 2008 +0000
@@ -1,345 +1,121 @@
-from django.core import validators
-from django import oldforms
-from django.core.mail import mail_admins, mail_managers
-from django.http import Http404
+from django import http
+from django.conf import settings
+from utils import next_redirect, confirmation_view
 from django.core.exceptions import ObjectDoesNotExist
+from django.db import models
 from django.shortcuts import render_to_response
 from django.template import RequestContext
-from django.contrib.comments.models import Comment, FreeComment, RATINGS_REQUIRED, RATINGS_OPTIONAL, IS_PUBLIC
-from django.contrib.contenttypes.models import ContentType
-from django.contrib.auth.forms import AuthenticationForm
-from django.http import HttpResponseRedirect
-from django.utils.text import normalize_newlines
-from django.conf import settings
-from django.utils.translation import ungettext, ugettext as _
-from django.utils.encoding import smart_unicode
-import base64, datetime
+from django.template.loader import render_to_string
+from django.utils.html import escape
+from django.views.decorators.http import require_POST
+from django.contrib import comments
+from django.contrib.comments import signals
 
-COMMENTS_PER_PAGE = 20
+class CommentPostBadRequest(http.HttpResponseBadRequest):
+    """
+    Response returned when a comment post is invalid. If ``DEBUG`` is on a
+    nice-ish error message will be displayed (for debugging purposes), but in
+    production mode a simple opaque 400 page will be displayed.
+    """
+    def __init__(self, why):
+        super(CommentPostBadRequest, self).__init__()
+        if settings.DEBUG:
+            self.content = render_to_string("comments/400-debug.html", {"why": why})
+
+def post_comment(request, next=None):
+    """
+    Post a comment.
+
+    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.username
+        if not data.get('email', ''):
+            data["email"] = request.user.email
 
-class PublicCommentManipulator(AuthenticationForm):
-    "Manipulator that handles public registered comments"
-    def __init__(self, user, ratings_required, ratings_range, num_rating_choices):
-        AuthenticationForm.__init__(self)
-        self.ratings_range, self.num_rating_choices = ratings_range, num_rating_choices
-        choices = [(c, c) for c in ratings_range]
-        def get_validator_list(rating_num):
-            if rating_num <= num_rating_choices:
-                return [validators.RequiredIfOtherFieldsGiven(['rating%d' % i for i in range(1, 9) if i != rating_num], _("This rating is required because you've entered at least one other rating."))]
-            else:
-                return []
-        self.fields.extend([
-            oldforms.LargeTextField(field_name="comment", max_length=3000, is_required=True,
-                validator_list=[self.hasNoProfanities]),
-            oldforms.RadioSelectField(field_name="rating1", choices=choices,
-                is_required=ratings_required and num_rating_choices > 0,
-                validator_list=get_validator_list(1),
-            ),
-            oldforms.RadioSelectField(field_name="rating2", choices=choices,
-                is_required=ratings_required and num_rating_choices > 1,
-                validator_list=get_validator_list(2),
-            ),
-            oldforms.RadioSelectField(field_name="rating3", choices=choices,
-                is_required=ratings_required and num_rating_choices > 2,
-                validator_list=get_validator_list(3),
-            ),
-            oldforms.RadioSelectField(field_name="rating4", choices=choices,
-                is_required=ratings_required and num_rating_choices > 3,
-                validator_list=get_validator_list(4),
-            ),
-            oldforms.RadioSelectField(field_name="rating5", choices=choices,
-                is_required=ratings_required and num_rating_choices > 4,
-                validator_list=get_validator_list(5),
-            ),
-            oldforms.RadioSelectField(field_name="rating6", choices=choices,
-                is_required=ratings_required and num_rating_choices > 5,
-                validator_list=get_validator_list(6),
-            ),
-            oldforms.RadioSelectField(field_name="rating7", choices=choices,
-                is_required=ratings_required and num_rating_choices > 6,
-                validator_list=get_validator_list(7),
-            ),
-            oldforms.RadioSelectField(field_name="rating8", choices=choices,
-                is_required=ratings_required and num_rating_choices > 7,
-                validator_list=get_validator_list(8),
-            ),
-        ])
-        if user.is_authenticated():
-            self["username"].is_required = False
-            self["username"].validator_list = []
-            self["password"].is_required = False
-            self["password"].validator_list = []
-            self.user_cache = user
+    # 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 = models.get_model(*ctype.split(".", 1))
+        target = model._default_manager.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)))
+
+    # Do we want to preview the comment?
+    preview = "preview" in data
 
-    def hasNoProfanities(self, field_data, all_data):
-        if settings.COMMENTS_ALLOW_PROFANITIES:
-            return
-        return validators.hasNoProfanities(field_data, all_data)
+    # Construct the comment form
+    form = comments.get_form()(target, data=data)
 
-    def get_comment(self, new_data):
-        "Helper function"
-        return Comment(None, self.get_user_id(), new_data["content_type_id"],
-            new_data["object_id"], new_data.get("headline", "").strip(),
-            new_data["comment"].strip(), new_data.get("rating1", None),
-            new_data.get("rating2", None), new_data.get("rating3", None),
-            new_data.get("rating4", None), new_data.get("rating5", None),
-            new_data.get("rating6", None), new_data.get("rating7", None),
-            new_data.get("rating8", None), new_data.get("rating1", None) is not None,
-            datetime.datetime.now(), new_data["is_public"], new_data["ip_address"], False, settings.SITE_ID)
+    # Check security information
+    if form.security_errors():
+        return CommentPostBadRequest(
+            "The comment form failed security verification: %s" % \
+                escape(str(form.security_errors())))
 
-    def save(self, new_data):
-        today = datetime.date.today()
-        c = self.get_comment(new_data)
-        for old in Comment.objects.filter(content_type__id__exact=new_data["content_type_id"],
-            object_id__exact=new_data["object_id"], user__id__exact=self.get_user_id()):
-            # Check that this comment isn't duplicate. (Sometimes people post
-            # comments twice by mistake.) If it is, fail silently by pretending
-            # the comment was posted successfully.
-            if old.submit_date.date() == today and old.comment == c.comment \
-                and old.rating1 == c.rating1 and old.rating2 == c.rating2 \
-                and old.rating3 == c.rating3 and old.rating4 == c.rating4 \
-                and old.rating5 == c.rating5 and old.rating6 == c.rating6 \
-                and old.rating7 == c.rating7 and old.rating8 == c.rating8:
-                return old
-            # If the user is leaving a rating, invalidate all old ratings.
-            if c.rating1 is not None:
-                old.valid_rating = False
-                old.save()
-        c.save()
-        # If the commentor has posted fewer than COMMENTS_FIRST_FEW comments,
-        # send the comment to the managers.
-        if self.user_cache.comment_set.count() <= settings.COMMENTS_FIRST_FEW:
-            message = ungettext('This comment was posted by a user who has posted fewer than %(count)s comment:\n\n%(text)s',
-                'This comment was posted by a user who has posted fewer than %(count)s comments:\n\n%(text)s', settings.COMMENTS_FIRST_FEW) % \
-                {'count': settings.COMMENTS_FIRST_FEW, 'text': c.get_as_text()}
-            mail_managers("Comment posted by rookie user", message)
-        if settings.COMMENTS_SKETCHY_USERS_GROUP and settings.COMMENTS_SKETCHY_USERS_GROUP in [g.id for g in self.user_cache.groups.all()]:
-            message = _('This comment was posted by a sketchy user:\n\n%(text)s') % {'text': c.get_as_text()}
-            mail_managers("Comment posted by sketchy user (%s)" % self.user_cache.username, c.get_as_text())
-        return c
-
-class PublicFreeCommentManipulator(oldforms.Manipulator):
-    "Manipulator that handles public free (unregistered) comments"
-    def __init__(self):
-        self.fields = (
-            oldforms.TextField(field_name="person_name", max_length=50, is_required=True,
-                validator_list=[self.hasNoProfanities]),
-            oldforms.LargeTextField(field_name="comment", max_length=3000, is_required=True,
-                validator_list=[self.hasNoProfanities]),
+    # If there are errors or if we requested a preview show the comment
+    if form.errors or preview:
+        template_list = [
+            "comments/%s_%s_preview.html" % tuple(str(model._meta).split(".")),
+            "comments/%s_preview.html" % model._meta.app_label,
+            "comments/preview.html",
+        ]
+        return render_to_response(
+            template_list, {
+                "comment" : form.data.get("comment", ""),
+                "form" : form,
+            },
+            RequestContext(request, {})
         )
 
-    def hasNoProfanities(self, field_data, all_data):
-        if settings.COMMENTS_ALLOW_PROFANITIES:
-            return
-        return validators.hasNoProfanities(field_data, all_data)
-
-    def get_comment(self, new_data):
-        "Helper function"
-        return FreeComment(None, new_data["content_type_id"],
-            new_data["object_id"], new_data["comment"].strip(),
-            new_data["person_name"].strip(), datetime.datetime.now(), new_data["is_public"],
-            new_data["ip_address"], False, settings.SITE_ID)
+    # Otherwise create the comment
+    comment = form.get_comment_object()
+    comment.ip_address = request.META.get("REMOTE_ADDR", None)
+    if request.user.is_authenticated():
+        comment.user = request.user
 
-    def save(self, new_data):
-        today = datetime.date.today()
-        c = self.get_comment(new_data)
-        # Check that this comment isn't duplicate. (Sometimes people post
-        # comments twice by mistake.) If it is, fail silently by pretending
-        # the comment was posted successfully.
-        for old_comment in FreeComment.objects.filter(content_type__id__exact=new_data["content_type_id"],
-            object_id__exact=new_data["object_id"], person_name__exact=new_data["person_name"],
-            submit_date__year=today.year, submit_date__month=today.month,
-            submit_date__day=today.day):
-            if old_comment.comment == c.comment:
-                return old_comment
-        c.save()
-        return c
+    # Signal that the comment is about to be saved
+    responses = signals.comment_will_be_posted.send(
+        sender  = comment.__class__,
+        comment = comment,
+        request = request
+    )
 
-def post_comment(request, extra_context=None, context_processors=None):
-    """
-    Post a comment
-
-    Redirects to the `comments.comments.comment_was_posted` view upon success.
+    for (receiver, response) in responses:
+        if response == False:
+            return CommentPostBadRequest(
+                "comment_will_be_posted receiver %r killed the comment" % receiver.__name__)
 
-    Templates: `comment_preview`
-    Context:
-        comment
-            the comment being posted
-        comment_form
-            the comment form
-        options
-            comment options
-        target
-            comment target
-        hash
-            security hash (must be included in a posted form to succesfully
-            post a comment).
-        rating_options
-            comment ratings options
-        ratings_optional
-            are ratings optional?
-        ratings_required
-            are ratings required?
-        rating_range
-            range of ratings
-        rating_choices
-            choice of ratings
-    """
-    if extra_context is None: extra_context = {}
-    if not request.POST:
-        raise Http404, _("Only POSTs are allowed")
-    try:
-        options, target, security_hash = request.POST['options'], request.POST['target'], request.POST['gonzo']
-    except KeyError:
-        raise Http404, _("One or more of the required fields wasn't submitted")
-    photo_options = request.POST.get('photo_options', '')
-    rating_options = normalize_newlines(request.POST.get('rating_options', ''))
-    if Comment.objects.get_security_hash(options, photo_options, rating_options, target) != security_hash:
-        raise Http404, _("Somebody tampered with the comment form (security violation)")
-    # Now we can be assured the data is valid.
-    if rating_options:
-        rating_range, rating_choices = Comment.objects.get_rating_options(base64.decodestring(rating_options))
-    else:
-        rating_range, rating_choices = [], []
-    content_type_id, object_id = target.split(':') # target is something like '52:5157'
-    try:
-        obj = ContentType.objects.get(pk=content_type_id).get_object_for_this_type(pk=object_id)
-    except ObjectDoesNotExist:
-        raise Http404, _("The comment form had an invalid 'target' parameter -- the object ID was invalid")
-    option_list = options.split(',') # options is something like 'pa,ra'
-    new_data = request.POST.copy()
-    new_data['content_type_id'] = content_type_id
-    new_data['object_id'] = object_id
-    new_data['ip_address'] = request.META.get('REMOTE_ADDR')
-    new_data['is_public'] = IS_PUBLIC in option_list
-    manipulator = PublicCommentManipulator(request.user,
-        ratings_required=RATINGS_REQUIRED in option_list,
-        ratings_range=rating_range,
-        num_rating_choices=len(rating_choices))
-    errors = manipulator.get_validation_errors(new_data)
-    # If user gave correct username/password and wasn't already logged in, log them in
-    # so they don't have to enter a username/password again.
-    if manipulator.get_user() and not manipulator.get_user().is_authenticated() and 'password' in new_data and manipulator.get_user().check_password(new_data['password']):
-        from django.contrib.auth import login
-        login(request, manipulator.get_user())
-    if errors or 'preview' in request.POST:
-        class CommentFormWrapper(oldforms.FormWrapper):
-            def __init__(self, manipulator, new_data, errors, rating_choices):
-                oldforms.FormWrapper.__init__(self, manipulator, new_data, errors)
-                self.rating_choices = rating_choices
-            def ratings(self):
-                field_list = [self['rating%d' % (i+1)] for i in range(len(rating_choices))]
-                for i, f in enumerate(field_list):
-                    f.choice = rating_choices[i]
-                return field_list
-        comment = errors and '' or manipulator.get_comment(new_data)
-        comment_form = CommentFormWrapper(manipulator, new_data, errors, rating_choices)
-        return render_to_response('comments/preview.html', {
-            'comment': comment,
-            'comment_form': comment_form,
-            'options': options,
-            'target': target,
-            'hash': security_hash,
-            'rating_options': rating_options,
-            'ratings_optional': RATINGS_OPTIONAL in option_list,
-            'ratings_required': RATINGS_REQUIRED in option_list,
-            'rating_range': rating_range,
-            'rating_choices': rating_choices,
-        }, context_instance=RequestContext(request, extra_context, context_processors))
-    elif 'post' in request.POST:
-        # If the IP is banned, mail the admins, do NOT save the comment, and
-        # serve up the "Thanks for posting" page as if the comment WAS posted.
-        if request.META['REMOTE_ADDR'] in settings.BANNED_IPS:
-            mail_admins("Banned IP attempted to post comment", smart_unicode(request.POST) + "\n\n" + str(request.META))
-        else:
-            manipulator.do_html2python(new_data)
-            comment = manipulator.save(new_data)
-        return HttpResponseRedirect("../posted/?c=%s:%s" % (content_type_id, object_id))
-    else:
-        raise Http404, _("The comment form didn't provide either 'preview' or 'post'")
+    # Save the comment and signal that it was saved
+    comment.save()
+    signals.comment_was_posted.send(
+        sender  = comment.__class__,
+        comment = comment,
+        request = request
+    )
 
-def post_free_comment(request, extra_context=None, context_processors=None):
-    """
-    Post a free comment (not requiring a log in)
+    return next_redirect(data, next, comment_done, c=comment._get_pk_val())
 
-    Redirects to `comments.comments.comment_was_posted` view on success.
+post_comment = require_POST(post_comment)
 
-    Templates: `comment_free_preview`
-    Context:
-        comment
-            comment being posted
-        comment_form
-            comment form object
-        options
-            comment options
-        target
-            comment target
-        hash
-            security hash (must be included in a posted form to succesfully
-            post a comment).
-    """
-    if extra_context is None: extra_context = {}
-    if not request.POST:
-        raise Http404, _("Only POSTs are allowed")
-    try:
-        options, target, security_hash = request.POST['options'], request.POST['target'], request.POST['gonzo']
-    except KeyError:
-        raise Http404, _("One or more of the required fields wasn't submitted")
-    if Comment.objects.get_security_hash(options, '', '', target) != security_hash:
-        raise Http404, _("Somebody tampered with the comment form (security violation)")
-    content_type_id, object_id = target.split(':') # target is something like '52:5157'
-    content_type = ContentType.objects.get(pk=content_type_id)
-    try:
-        obj = content_type.get_object_for_this_type(pk=object_id)
-    except ObjectDoesNotExist:
-        raise Http404, _("The comment form had an invalid 'target' parameter -- the object ID was invalid")
-    option_list = options.split(',')
-    new_data = request.POST.copy()
-    new_data['content_type_id'] = content_type_id
-    new_data['object_id'] = object_id
-    new_data['ip_address'] = request.META['REMOTE_ADDR']
-    new_data['is_public'] = IS_PUBLIC in option_list
-    manipulator = PublicFreeCommentManipulator()
-    errors = manipulator.get_validation_errors(new_data)
-    if errors or 'preview' in request.POST:
-        comment = errors and '' or manipulator.get_comment(new_data)
-        return render_to_response('comments/free_preview.html', {
-            'comment': comment,
-            'comment_form': oldforms.FormWrapper(manipulator, new_data, errors),
-            'options': options,
-            'target': target,
-            'hash': security_hash,
-        }, context_instance=RequestContext(request, extra_context, context_processors))
-    elif 'post' in request.POST:
-        # If the IP is banned, mail the admins, do NOT save the comment, and
-        # serve up the "Thanks for posting" page as if the comment WAS posted.
-        if request.META['REMOTE_ADDR'] in settings.BANNED_IPS:
-            from django.core.mail import mail_admins
-            mail_admins("Practical joker", smart_unicode(request.POST) + "\n\n" + str(request.META))
-        else:
-            manipulator.do_html2python(new_data)
-            comment = manipulator.save(new_data)
-        return HttpResponseRedirect("../posted/?c=%s:%s" % (content_type_id, object_id))
-    else:
-        raise Http404, _("The comment form didn't provide either 'preview' or 'post'")
+comment_done = confirmation_view(
+    template = "comments/posted.html",
+    doc = """Display a "comment was posted" success page."""
+)
 
-def comment_was_posted(request, extra_context=None, context_processors=None):
-    """
-    Display "comment was posted" success page
-
-    Templates: `comment_posted`
-    Context:
-        object
-            The object the comment was posted on
-    """
-    if extra_context is None: extra_context = {}
-    obj = None
-    if 'c' in request.GET:
-        content_type_id, object_id = request.GET['c'].split(':')
-        try:
-            content_type = ContentType.objects.get(pk=content_type_id)
-            obj = content_type.get_object_for_this_type(pk=object_id)
-        except ObjectDoesNotExist:
-            pass
-    return render_to_response('comments/posted.html', {'object': obj},
-        context_instance=RequestContext(request, extra_context, context_processors))