summaryrefslogtreecommitdiff
path: root/parts/django/django/contrib/comments
diff options
context:
space:
mode:
Diffstat (limited to 'parts/django/django/contrib/comments')
-rw-r--r--parts/django/django/contrib/comments/__init__.py91
-rw-r--r--parts/django/django/contrib/comments/admin.py73
-rw-r--r--parts/django/django/contrib/comments/feeds.py38
-rw-r--r--parts/django/django/contrib/comments/forms.py190
-rw-r--r--parts/django/django/contrib/comments/managers.py22
-rw-r--r--parts/django/django/contrib/comments/models.py191
-rw-r--r--parts/django/django/contrib/comments/moderation.py353
-rw-r--r--parts/django/django/contrib/comments/signals.py21
-rw-r--r--parts/django/django/contrib/comments/templates/comments/400-debug.html53
-rw-r--r--parts/django/django/contrib/comments/templates/comments/approve.html15
-rw-r--r--parts/django/django/contrib/comments/templates/comments/approved.html8
-rw-r--r--parts/django/django/contrib/comments/templates/comments/base.html10
-rw-r--r--parts/django/django/contrib/comments/templates/comments/delete.html15
-rw-r--r--parts/django/django/contrib/comments/templates/comments/deleted.html8
-rw-r--r--parts/django/django/contrib/comments/templates/comments/flag.html15
-rw-r--r--parts/django/django/contrib/comments/templates/comments/flagged.html8
-rw-r--r--parts/django/django/contrib/comments/templates/comments/form.html20
-rw-r--r--parts/django/django/contrib/comments/templates/comments/list.html10
-rw-r--r--parts/django/django/contrib/comments/templates/comments/posted.html8
-rw-r--r--parts/django/django/contrib/comments/templates/comments/preview.html36
-rw-r--r--parts/django/django/contrib/comments/templatetags/__init__.py0
-rw-r--r--parts/django/django/contrib/comments/templatetags/comments.py333
-rw-r--r--parts/django/django/contrib/comments/urls.py16
-rw-r--r--parts/django/django/contrib/comments/views/__init__.py0
-rw-r--r--parts/django/django/contrib/comments/views/comments.py136
-rw-r--r--parts/django/django/contrib/comments/views/moderation.py159
-rw-r--r--parts/django/django/contrib/comments/views/utils.py58
27 files changed, 1887 insertions, 0 deletions
diff --git a/parts/django/django/contrib/comments/__init__.py b/parts/django/django/contrib/comments/__init__.py
new file mode 100644
index 0000000..42384e7
--- /dev/null
+++ b/parts/django/django/contrib/comments/__init__.py
@@ -0,0 +1,91 @@
+from django.conf import settings
+from django.core import urlresolvers
+from django.core.exceptions import ImproperlyConfigured
+from django.contrib.comments.models import Comment
+from django.contrib.comments.forms import CommentForm
+from django.utils.importlib import import_module
+
+DEFAULT_COMMENTS_APP = 'django.contrib.comments'
+
+def get_comment_app():
+ """
+ Get the comment app (i.e. "django.contrib.comments") as defined in the settings
+ """
+ # Make sure the app's in INSTALLED_APPS
+ comments_app = get_comment_app_name()
+ if comments_app not in settings.INSTALLED_APPS:
+ raise ImproperlyConfigured("The COMMENTS_APP (%r) "\
+ "must be in INSTALLED_APPS" % settings.COMMENTS_APP)
+
+ # Try to import the package
+ try:
+ package = import_module(comments_app)
+ except ImportError:
+ raise ImproperlyConfigured("The COMMENTS_APP setting refers to "\
+ "a non-existing package.")
+
+ return package
+
+def get_comment_app_name():
+ """
+ Returns the name of the comment app (either the setting value, if it
+ exists, or the default).
+ """
+ return getattr(settings, 'COMMENTS_APP', DEFAULT_COMMENTS_APP)
+
+def get_model():
+ """
+ Returns the comment model class.
+ """
+ if get_comment_app_name() != DEFAULT_COMMENTS_APP and hasattr(get_comment_app(), "get_model"):
+ return get_comment_app().get_model()
+ else:
+ return Comment
+
+def get_form():
+ """
+ Returns the comment ModelForm class.
+ """
+ if get_comment_app_name() != DEFAULT_COMMENTS_APP and hasattr(get_comment_app(), "get_form"):
+ return get_comment_app().get_form()
+ else:
+ return CommentForm
+
+def get_form_target():
+ """
+ Returns the target URL for the comment form submission view.
+ """
+ if get_comment_app_name() != DEFAULT_COMMENTS_APP and hasattr(get_comment_app(), "get_form_target"):
+ return get_comment_app().get_form_target()
+ else:
+ return urlresolvers.reverse("django.contrib.comments.views.comments.post_comment")
+
+def get_flag_url(comment):
+ """
+ Get the URL for the "flag this comment" view.
+ """
+ if get_comment_app_name() != DEFAULT_COMMENTS_APP and hasattr(get_comment_app(), "get_flag_url"):
+ return get_comment_app().get_flag_url(comment)
+ else:
+ return urlresolvers.reverse("django.contrib.comments.views.moderation.flag",
+ args=(comment.id,))
+
+def get_delete_url(comment):
+ """
+ Get the URL for the "delete this comment" view.
+ """
+ if get_comment_app_name() != DEFAULT_COMMENTS_APP and hasattr(get_comment_app(), "get_delete_url"):
+ return get_comment_app().get_delete_url(comment)
+ else:
+ return urlresolvers.reverse("django.contrib.comments.views.moderation.delete",
+ args=(comment.id,))
+
+def get_approve_url(comment):
+ """
+ Get the URL for the "approve this comment from moderation" view.
+ """
+ if get_comment_app_name() != DEFAULT_COMMENTS_APP and hasattr(get_comment_app(), "get_approve_url"):
+ return get_comment_app().get_approve_url(comment)
+ else:
+ return urlresolvers.reverse("django.contrib.comments.views.moderation.approve",
+ args=(comment.id,))
diff --git a/parts/django/django/contrib/comments/admin.py b/parts/django/django/contrib/comments/admin.py
new file mode 100644
index 0000000..4cb9066
--- /dev/null
+++ b/parts/django/django/contrib/comments/admin.py
@@ -0,0 +1,73 @@
+from django.contrib import admin
+from django.contrib.comments.models import Comment
+from django.utils.translation import ugettext_lazy as _, ungettext
+from django.contrib.comments import get_model
+from django.contrib.comments.views.moderation import perform_flag, perform_approve, perform_delete
+
+class CommentsAdmin(admin.ModelAdmin):
+ fieldsets = (
+ (None,
+ {'fields': ('content_type', 'object_pk', 'site')}
+ ),
+ (_('Content'),
+ {'fields': ('user', 'user_name', 'user_email', 'user_url', 'comment')}
+ ),
+ (_('Metadata'),
+ {'fields': ('submit_date', 'ip_address', 'is_public', 'is_removed')}
+ ),
+ )
+
+ list_display = ('name', 'content_type', 'object_pk', 'ip_address', 'submit_date', 'is_public', 'is_removed')
+ list_filter = ('submit_date', 'site', 'is_public', 'is_removed')
+ date_hierarchy = 'submit_date'
+ ordering = ('-submit_date',)
+ raw_id_fields = ('user',)
+ search_fields = ('comment', 'user__username', 'user_name', 'user_email', 'user_url', 'ip_address')
+ actions = ["flag_comments", "approve_comments", "remove_comments"]
+
+ def get_actions(self, request):
+ actions = super(CommentsAdmin, self).get_actions(request)
+ # Only superusers should be able to delete the comments from the DB.
+ if not request.user.is_superuser and 'delete_selected' in actions:
+ actions.pop('delete_selected')
+ if not request.user.has_perm('comments.can_moderate'):
+ if 'approve_comments' in actions:
+ actions.pop('approve_comments')
+ if 'remove_comments' in actions:
+ actions.pop('remove_comments')
+ return actions
+
+ def flag_comments(self, request, queryset):
+ self._bulk_flag(request, queryset, perform_flag,
+ lambda n: ungettext('flagged', 'flagged', n))
+ flag_comments.short_description = _("Flag selected comments")
+
+ def approve_comments(self, request, queryset):
+ self._bulk_flag(request, queryset, perform_approve,
+ lambda n: ungettext('approved', 'approved', n))
+ approve_comments.short_description = _("Approve selected comments")
+
+ def remove_comments(self, request, queryset):
+ self._bulk_flag(request, queryset, perform_delete,
+ lambda n: ungettext('removed', 'removed', n))
+ remove_comments.short_description = _("Remove selected comments")
+
+ def _bulk_flag(self, request, queryset, action, done_message):
+ """
+ Flag, approve, or remove some comments from an admin action. Actually
+ calls the `action` argument to perform the heavy lifting.
+ """
+ n_comments = 0
+ for comment in queryset:
+ action(request, comment)
+ n_comments += 1
+
+ msg = ungettext(u'1 comment was successfully %(action)s.',
+ u'%(count)s comments were successfully %(action)s.',
+ n_comments)
+ self.message_user(request, msg % {'count': n_comments, 'action': done_message(n_comments)})
+
+# Only register the default admin if the model is the built-in comment model
+# (this won't be true if there's a custom comment app).
+if get_model() is Comment:
+ admin.site.register(Comment, CommentsAdmin)
diff --git a/parts/django/django/contrib/comments/feeds.py b/parts/django/django/contrib/comments/feeds.py
new file mode 100644
index 0000000..e74ca2d
--- /dev/null
+++ b/parts/django/django/contrib/comments/feeds.py
@@ -0,0 +1,38 @@
+from django.conf import settings
+from django.contrib.syndication.views import Feed
+from django.contrib.sites.models import Site
+from django.contrib import comments
+from django.utils.translation import ugettext as _
+
+class LatestCommentFeed(Feed):
+ """Feed of latest comments on the current site."""
+
+ def title(self):
+ if not hasattr(self, '_site'):
+ self._site = Site.objects.get_current()
+ return _("%(site_name)s comments") % dict(site_name=self._site.name)
+
+ def link(self):
+ if not hasattr(self, '_site'):
+ self._site = Site.objects.get_current()
+ return "http://%s/" % (self._site.domain)
+
+ def description(self):
+ if not hasattr(self, '_site'):
+ self._site = Site.objects.get_current()
+ return _("Latest comments on %(site_name)s") % dict(site_name=self._site.name)
+
+ def items(self):
+ qs = comments.get_model().objects.filter(
+ site__pk = settings.SITE_ID,
+ is_public = True,
+ is_removed = False,
+ )
+ if getattr(settings, 'COMMENTS_BANNED_USERS_GROUP', None):
+ where = ['user_id NOT IN (SELECT user_id FROM auth_user_groups WHERE group_id = %s)']
+ params = [settings.COMMENTS_BANNED_USERS_GROUP]
+ qs = qs.extra(where=where, params=params)
+ return qs.order_by('-submit_date')[:40]
+
+ def item_pubdate(self, item):
+ return item.submit_date
diff --git a/parts/django/django/contrib/comments/forms.py b/parts/django/django/contrib/comments/forms.py
new file mode 100644
index 0000000..0c4b285
--- /dev/null
+++ b/parts/django/django/contrib/comments/forms.py
@@ -0,0 +1,190 @@
+import time
+import datetime
+
+from django import forms
+from django.forms.util import ErrorDict
+from django.conf import settings
+from django.contrib.contenttypes.models import ContentType
+from models import Comment
+from django.utils.encoding import force_unicode
+from django.utils.hashcompat import sha_constructor
+from django.utils.text import get_text_list
+from django.utils.translation import ungettext, ugettext_lazy as _
+
+COMMENT_MAX_LENGTH = getattr(settings,'COMMENT_MAX_LENGTH', 3000)
+
+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)
+ security_hash = forms.CharField(min_length=40, max_length=40, widget=forms.HiddenInput)
+
+ def __init__(self, target_object, data=None, initial=None):
+ self.target_object = target_object
+ if initial is None:
+ initial = {}
+ initial.update(self.generate_security_data())
+ 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):
+ """
+ Return a new (unsaved) comment object based on the information in this
+ form. Assumes that the form is already validated and will throw a
+ ValueError if not.
+
+ Does not set any of the fields that would come from a Request object
+ (i.e. ``user`` or ``ip_address``).
+ """
+ if not self.is_valid():
+ raise ValueError("get_comment_object may only be called on valid forms")
+
+ CommentModel = self.get_comment_model()
+ new = CommentModel(**self.get_comment_create_data())
+ new = self.check_for_duplicate_comment(new)
+
+ return new
+
+ def get_comment_model(self):
+ """
+ Get the comment model to create with this form. Subclasses in custom
+ comment apps should override this, get_comment_create_data, and perhaps
+ check_for_duplicate_comment to provide custom comment models.
+ """
+ return Comment
+
+ def get_comment_create_data(self):
+ """
+ Returns the dict of data to be used to create a comment. Subclasses in
+ custom comment apps that override get_comment_model can override this
+ method to add extra fields onto a custom comment model.
+ """
+ return dict(
+ content_type = ContentType.objects.get_for_model(self.target_object),
+ object_pk = force_unicode(self.target_object._get_pk_val()),
+ user_name = self.cleaned_data["name"],
+ user_email = self.cleaned_data["email"],
+ user_url = self.cleaned_data["url"],
+ comment = self.cleaned_data["comment"],
+ submit_date = datetime.datetime.now(),
+ site_id = settings.SITE_ID,
+ is_public = True,
+ is_removed = False,
+ )
+
+ def check_for_duplicate_comment(self, new):
+ """
+ Check that a submitted comment isn't a duplicate. This might be caused
+ by someone posting a comment twice. If it is a dup, silently return the *previous* comment.
+ """
+ possible_duplicates = self.get_comment_model()._default_manager.using(
+ self.target_object._state.db
+ ).filter(
+ content_type = new.content_type,
+ object_pk = new.object_pk,
+ user_name = new.user_name,
+ user_email = new.user_email,
+ user_url = new.user_url,
+ )
+ for old in possible_duplicates:
+ if old.submit_date.date() == new.submit_date.date() and old.comment == new.comment:
+ return old
+
+ return new
+
+ def clean_comment(self):
+ """
+ If COMMENTS_ALLOW_PROFANITIES is False, check that the comment doesn't
+ contain anything in PROFANITIES_LIST.
+ """
+ comment = self.cleaned_data["comment"]
+ if settings.COMMENTS_ALLOW_PROFANITIES == False:
+ bad_words = [w for w in settings.PROFANITIES_LIST if w in comment.lower()]
+ if bad_words:
+ plural = len(bad_words) > 1
+ raise forms.ValidationError(ungettext(
+ "Watch your mouth! The word %s is not allowed here.",
+ "Watch your mouth! The words %s are not allowed here.", plural) % \
+ get_text_list(['"%s%s%s"' % (i[0], '-'*(len(i)-2), i[-1]) for i in bad_words], 'and'))
+ return comment
+
+class CommentForm(CommentDetailsForm):
+ honeypot = forms.CharField(required=False,
+ label=_('If you enter anything in this field '\
+ 'your comment will be treated as spam'))
+
+ 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
diff --git a/parts/django/django/contrib/comments/managers.py b/parts/django/django/contrib/comments/managers.py
new file mode 100644
index 0000000..499feee
--- /dev/null
+++ b/parts/django/django/contrib/comments/managers.py
@@ -0,0 +1,22 @@
+from django.db import models
+from django.contrib.contenttypes.models import ContentType
+from django.utils.encoding import force_unicode
+
+class CommentManager(models.Manager):
+
+ def in_moderation(self):
+ """
+ QuerySet for all comments currently in the moderation queue.
+ """
+ return self.get_query_set().filter(is_public=False, is_removed=False)
+
+ def for_model(self, model):
+ """
+ QuerySet for all comments for a particular model (either an instance or
+ a class).
+ """
+ ct = ContentType.objects.get_for_model(model)
+ qs = self.get_query_set().filter(content_type=ct)
+ if isinstance(model, models.Model):
+ qs = qs.filter(object_pk=force_unicode(model._get_pk_val()))
+ return qs
diff --git a/parts/django/django/contrib/comments/models.py b/parts/django/django/contrib/comments/models.py
new file mode 100644
index 0000000..5e128d2
--- /dev/null
+++ b/parts/django/django/contrib/comments/models.py
@@ -0,0 +1,191 @@
+import datetime
+from django.contrib.auth.models import User
+from django.contrib.comments.managers import CommentManager
+from django.contrib.contenttypes import generic
+from django.contrib.contenttypes.models import ContentType
+from django.contrib.sites.models import Site
+from django.db import models
+from django.core import urlresolvers
+from django.utils.translation import ugettext_lazy as _
+from django.conf import settings
+
+COMMENT_MAX_LENGTH = getattr(settings,'COMMENT_MAX_LENGTH',3000)
+
+class BaseCommentAbstractModel(models.Model):
+ """
+ An abstract base class that any custom comment models probably should
+ subclass.
+ """
+
+ # Content-object field
+ content_type = models.ForeignKey(ContentType,
+ verbose_name=_('content type'),
+ related_name="content_type_set_for_%(class)s")
+ object_pk = models.TextField(_('object ID'))
+ content_object = generic.GenericForeignKey(ct_field="content_type", fk_field="object_pk")
+
+ # Metadata about the comment
+ site = models.ForeignKey(Site)
+
+ class Meta:
+ abstract = True
+
+ def get_content_object_url(self):
+ """
+ Get a URL suitable for redirecting to the content object.
+ """
+ return urlresolvers.reverse(
+ "comments-url-redirect",
+ args=(self.content_type_id, self.object_pk)
+ )
+
+class Comment(BaseCommentAbstractModel):
+ """
+ A user comment about some object.
+ """
+
+ # Who posted this comment? If ``user`` is set then it was an authenticated
+ # user; otherwise at least user_name should have been set and the comment
+ # was posted by a non-authenticated user.
+ user = models.ForeignKey(User, verbose_name=_('user'),
+ blank=True, null=True, related_name="%(class)s_comments")
+ user_name = models.CharField(_("user's name"), max_length=50, blank=True)
+ user_email = models.EmailField(_("user's email address"), blank=True)
+ user_url = models.URLField(_("user's URL"), blank=True)
+
+ comment = models.TextField(_('comment'), max_length=COMMENT_MAX_LENGTH)
+
+ # Metadata about the comment
+ submit_date = models.DateTimeField(_('date/time submitted'), default=None)
+ ip_address = models.IPAddressField(_('IP address'), blank=True, null=True)
+ is_public = models.BooleanField(_('is public'), default=True,
+ help_text=_('Uncheck this box to make the comment effectively ' \
+ 'disappear from the site.'))
+ is_removed = models.BooleanField(_('is removed'), default=False,
+ help_text=_('Check this box if the comment is inappropriate. ' \
+ 'A "This comment has been removed" message will ' \
+ 'be displayed instead.'))
+
+ # Manager
+ objects = CommentManager()
+
+ class Meta:
+ db_table = "django_comments"
+ ordering = ('submit_date',)
+ permissions = [("can_moderate", "Can moderate comments")]
+ verbose_name = _('comment')
+ verbose_name_plural = _('comments')
+
+ def __unicode__(self):
+ return "%s: %s..." % (self.name, self.comment[:50])
+
+ def save(self, *args, **kwargs):
+ if self.submit_date is None:
+ self.submit_date = datetime.datetime.now()
+ super(Comment, self).save(*args, **kwargs)
+
+ def _get_userinfo(self):
+ """
+ Get a dictionary that pulls together information about the poster
+ safely for both authenticated and non-authenticated comments.
+
+ This dict will have ``name``, ``email``, and ``url`` fields.
+ """
+ if not hasattr(self, "_userinfo"):
+ self._userinfo = {
+ "name" : self.user_name,
+ "email" : self.user_email,
+ "url" : self.user_url
+ }
+ if self.user_id:
+ u = self.user
+ if u.email:
+ self._userinfo["email"] = u.email
+
+ # If the user has a full name, use that for the user name.
+ # However, a given user_name overrides the raw user.username,
+ # so only use that if this comment has no associated name.
+ if u.get_full_name():
+ self._userinfo["name"] = self.user.get_full_name()
+ elif not self.user_name:
+ self._userinfo["name"] = u.username
+ return self._userinfo
+ userinfo = property(_get_userinfo, doc=_get_userinfo.__doc__)
+
+ def _get_name(self):
+ return self.userinfo["name"]
+ def _set_name(self, val):
+ if self.user_id:
+ raise AttributeError(_("This comment was posted by an authenticated "\
+ "user and thus the name is read-only."))
+ self.user_name = val
+ name = property(_get_name, _set_name, doc="The name of the user who posted this comment")
+
+ def _get_email(self):
+ return self.userinfo["email"]
+ def _set_email(self, val):
+ if self.user_id:
+ raise AttributeError(_("This comment was posted by an authenticated "\
+ "user and thus the email is read-only."))
+ self.user_email = val
+ email = property(_get_email, _set_email, doc="The email of the user who posted this comment")
+
+ def _get_url(self):
+ return self.userinfo["url"]
+ def _set_url(self, val):
+ self.user_url = val
+ url = property(_get_url, _set_url, doc="The URL given by the user who posted this comment")
+
+ def get_absolute_url(self, anchor_pattern="#c%(id)s"):
+ return self.get_content_object_url() + (anchor_pattern % self.__dict__)
+
+ def get_as_text(self):
+ """
+ Return this comment as plain text. Useful for emails.
+ """
+ d = {
+ 'user': self.user or self.name,
+ 'date': self.submit_date,
+ 'comment': self.comment,
+ 'domain': self.site.domain,
+ 'url': self.get_absolute_url()
+ }
+ return _('Posted by %(user)s at %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s') % d
+
+class CommentFlag(models.Model):
+ """
+ Records a flag on a comment. This is intentionally flexible; right now, a
+ flag could be:
+
+ * A "removal suggestion" -- where a user suggests a comment for (potential) removal.
+
+ * A "moderator deletion" -- used when a moderator deletes a comment.
+
+ You can (ab)use this model to add other flags, if needed. However, by
+ design users are only allowed to flag a comment with a given flag once;
+ if you want rating look elsewhere.
+ """
+ user = models.ForeignKey(User, verbose_name=_('user'), related_name="comment_flags")
+ comment = models.ForeignKey(Comment, verbose_name=_('comment'), related_name="flags")
+ flag = models.CharField(_('flag'), max_length=30, db_index=True)
+ flag_date = models.DateTimeField(_('date'), default=None)
+
+ # Constants for flag types
+ SUGGEST_REMOVAL = "removal suggestion"
+ MODERATOR_DELETION = "moderator deletion"
+ MODERATOR_APPROVAL = "moderator approval"
+
+ class Meta:
+ db_table = 'django_comment_flags'
+ unique_together = [('user', 'comment', 'flag')]
+ verbose_name = _('comment flag')
+ verbose_name_plural = _('comment flags')
+
+ def __unicode__(self):
+ return "%s flag of comment ID %s by %s" % \
+ (self.flag, self.comment_id, self.user.username)
+
+ def save(self, *args, **kwargs):
+ if self.flag_date is None:
+ self.flag_date = datetime.datetime.now()
+ super(CommentFlag, self).save(*args, **kwargs)
diff --git a/parts/django/django/contrib/comments/moderation.py b/parts/django/django/contrib/comments/moderation.py
new file mode 100644
index 0000000..7f429c5
--- /dev/null
+++ b/parts/django/django/contrib/comments/moderation.py
@@ -0,0 +1,353 @@
+"""
+A generic comment-moderation system which allows configuration of
+moderation options on a per-model basis.
+
+To use, do two things:
+
+1. Create or import a subclass of ``CommentModerator`` defining the
+ options you want.
+
+2. Import ``moderator`` from this module and register one or more
+ models, passing the models and the ``CommentModerator`` options
+ class you want to use.
+
+
+Example
+-------
+
+First, we define a simple model class which might represent entries in
+a Weblog::
+
+ from django.db import models
+
+ class Entry(models.Model):
+ title = models.CharField(maxlength=250)
+ body = models.TextField()
+ pub_date = models.DateField()
+ enable_comments = models.BooleanField()
+
+Then we create a ``CommentModerator`` subclass specifying some
+moderation options::
+
+ from django.contrib.comments.moderation import CommentModerator, moderator
+
+ class EntryModerator(CommentModerator):
+ email_notification = True
+ enable_field = 'enable_comments'
+
+And finally register it for moderation::
+
+ moderator.register(Entry, EntryModerator)
+
+This sample class would apply two moderation steps to each new
+comment submitted on an Entry:
+
+* If the entry's ``enable_comments`` field is set to ``False``, the
+ comment will be rejected (immediately deleted).
+
+* If the comment is successfully posted, an email notification of the
+ comment will be sent to site staff.
+
+For a full list of built-in moderation options and other
+configurability, see the documentation for the ``CommentModerator``
+class.
+
+"""
+
+import datetime
+
+from django.conf import settings
+from django.core.mail import send_mail
+from django.contrib.comments import signals
+from django.db.models.base import ModelBase
+from django.template import Context, loader
+from django.contrib import comments
+from django.contrib.sites.models import Site
+
+class AlreadyModerated(Exception):
+ """
+ Raised when a model which is already registered for moderation is
+ attempting to be registered again.
+
+ """
+ pass
+
+class NotModerated(Exception):
+ """
+ Raised when a model which is not registered for moderation is
+ attempting to be unregistered.
+
+ """
+ pass
+
+class CommentModerator(object):
+ """
+ Encapsulates comment-moderation options for a given model.
+
+ This class is not designed to be used directly, since it doesn't
+ enable any of the available moderation options. Instead, subclass
+ it and override attributes to enable different options::
+
+ ``auto_close_field``
+ If this is set to the name of a ``DateField`` or
+ ``DateTimeField`` on the model for which comments are
+ being moderated, new comments for objects of that model
+ will be disallowed (immediately deleted) when a certain
+ number of days have passed after the date specified in
+ that field. Must be used in conjunction with
+ ``close_after``, which specifies the number of days past
+ which comments should be disallowed. Default value is
+ ``None``.
+
+ ``auto_moderate_field``
+ Like ``auto_close_field``, but instead of outright
+ deleting new comments when the requisite number of days
+ have elapsed, it will simply set the ``is_public`` field
+ of new comments to ``False`` before saving them. Must be
+ used in conjunction with ``moderate_after``, which
+ specifies the number of days past which comments should be
+ moderated. Default value is ``None``.
+
+ ``close_after``
+ If ``auto_close_field`` is used, this must specify the
+ number of days past the value of the field specified by
+ ``auto_close_field`` after which new comments for an
+ object should be disallowed. Default value is ``None``.
+
+ ``email_notification``
+ If ``True``, any new comment on an object of this model
+ which survives moderation will generate an email to site
+ staff. Default value is ``False``.
+
+ ``enable_field``
+ If this is set to the name of a ``BooleanField`` on the
+ model for which comments are being moderated, new comments
+ on objects of that model will be disallowed (immediately
+ deleted) whenever the value of that field is ``False`` on
+ the object the comment would be attached to. Default value
+ is ``None``.
+
+ ``moderate_after``
+ If ``auto_moderate_field`` is used, this must specify the number
+ of days past the value of the field specified by
+ ``auto_moderate_field`` after which new comments for an
+ object should be marked non-public. Default value is
+ ``None``.
+
+ Most common moderation needs can be covered by changing these
+ attributes, but further customization can be obtained by
+ subclassing and overriding the following methods. Each method will
+ be called with three arguments: ``comment``, which is the comment
+ being submitted, ``content_object``, which is the object the
+ comment will be attached to, and ``request``, which is the
+ ``HttpRequest`` in which the comment is being submitted::
+
+ ``allow``
+ Should return ``True`` if the comment should be allowed to
+ post on the content object, and ``False`` otherwise (in
+ which case the comment will be immediately deleted).
+
+ ``email``
+ If email notification of the new comment should be sent to
+ site staff or moderators, this method is responsible for
+ sending the email.
+
+ ``moderate``
+ Should return ``True`` if the comment should be moderated
+ (in which case its ``is_public`` field will be set to
+ ``False`` before saving), and ``False`` otherwise (in
+ which case the ``is_public`` field will not be changed).
+
+ Subclasses which want to introspect the model for which comments
+ are being moderated can do so through the attribute ``_model``,
+ which will be the model class.
+
+ """
+ auto_close_field = None
+ auto_moderate_field = None
+ close_after = None
+ email_notification = False
+ enable_field = None
+ moderate_after = None
+
+ def __init__(self, model):
+ self._model = model
+
+ def _get_delta(self, now, then):
+ """
+ Internal helper which will return a ``datetime.timedelta``
+ representing the time between ``now`` and ``then``. Assumes
+ ``now`` is a ``datetime.date`` or ``datetime.datetime`` later
+ than ``then``.
+
+ If ``now`` and ``then`` are not of the same type due to one of
+ them being a ``datetime.date`` and the other being a
+ ``datetime.datetime``, both will be coerced to
+ ``datetime.date`` before calculating the delta.
+
+ """
+ if now.__class__ is not then.__class__:
+ now = datetime.date(now.year, now.month, now.day)
+ then = datetime.date(then.year, then.month, then.day)
+ if now < then:
+ raise ValueError("Cannot determine moderation rules because date field is set to a value in the future")
+ return now - then
+
+ def allow(self, comment, content_object, request):
+ """
+ Determine whether a given comment is allowed to be posted on
+ a given object.
+
+ Return ``True`` if the comment should be allowed, ``False
+ otherwise.
+
+ """
+ if self.enable_field:
+ if not getattr(content_object, self.enable_field):
+ return False
+ if self.auto_close_field and self.close_after:
+ if self._get_delta(datetime.datetime.now(), getattr(content_object, self.auto_close_field)).days >= self.close_after:
+ return False
+ return True
+
+ def moderate(self, comment, content_object, request):
+ """
+ Determine whether a given comment on a given object should be
+ allowed to show up immediately, or should be marked non-public
+ and await approval.
+
+ Return ``True`` if the comment should be moderated (marked
+ non-public), ``False`` otherwise.
+
+ """
+ if self.auto_moderate_field and self.moderate_after:
+ if self._get_delta(datetime.datetime.now(), getattr(content_object, self.auto_moderate_field)).days >= self.moderate_after:
+ return True
+ return False
+
+ def email(self, comment, content_object, request):
+ """
+ Send email notification of a new comment to site staff when email
+ notifications have been requested.
+
+ """
+ if not self.email_notification:
+ return
+ recipient_list = [manager_tuple[1] for manager_tuple in settings.MANAGERS]
+ t = loader.get_template('comments/comment_notification_email.txt')
+ c = Context({ 'comment': comment,
+ 'content_object': content_object })
+ subject = '[%s] New comment posted on "%s"' % (Site.objects.get_current().name,
+ content_object)
+ message = t.render(c)
+ send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, recipient_list, fail_silently=True)
+
+class Moderator(object):
+ """
+ Handles moderation of a set of models.
+
+ An instance of this class will maintain a list of one or more
+ models registered for comment moderation, and their associated
+ moderation classes, and apply moderation to all incoming comments.
+
+ To register a model, obtain an instance of ``Moderator`` (this
+ module exports one as ``moderator``), and call its ``register``
+ method, passing the model class and a moderation class (which
+ should be a subclass of ``CommentModerator``). Note that both of
+ these should be the actual classes, not instances of the classes.
+
+ To cease moderation for a model, call the ``unregister`` method,
+ passing the model class.
+
+ For convenience, both ``register`` and ``unregister`` can also
+ accept a list of model classes in place of a single model; this
+ allows easier registration of multiple models with the same
+ ``CommentModerator`` class.
+
+ The actual moderation is applied in two phases: one prior to
+ saving a new comment, and the other immediately after saving. The
+ pre-save moderation may mark a comment as non-public or mark it to
+ be removed; the post-save moderation may delete a comment which
+ was disallowed (there is currently no way to prevent the comment
+ being saved once before removal) and, if the comment is still
+ around, will send any notification emails the comment generated.
+
+ """
+ def __init__(self):
+ self._registry = {}
+ self.connect()
+
+ def connect(self):
+ """
+ Hook up the moderation methods to pre- and post-save signals
+ from the comment models.
+
+ """
+ signals.comment_will_be_posted.connect(self.pre_save_moderation, sender=comments.get_model())
+ signals.comment_was_posted.connect(self.post_save_moderation, sender=comments.get_model())
+
+ def register(self, model_or_iterable, moderation_class):
+ """
+ Register a model or a list of models for comment moderation,
+ using a particular moderation class.
+
+ Raise ``AlreadyModerated`` if any of the models are already
+ registered.
+
+ """
+ if isinstance(model_or_iterable, ModelBase):
+ model_or_iterable = [model_or_iterable]
+ for model in model_or_iterable:
+ if model in self._registry:
+ raise AlreadyModerated("The model '%s' is already being moderated" % model._meta.module_name)
+ self._registry[model] = moderation_class(model)
+
+ def unregister(self, model_or_iterable):
+ """
+ Remove a model or a list of models from the list of models
+ whose comments will be moderated.
+
+ Raise ``NotModerated`` if any of the models are not currently
+ registered for moderation.
+
+ """
+ if isinstance(model_or_iterable, ModelBase):
+ model_or_iterable = [model_or_iterable]
+ for model in model_or_iterable:
+ if model not in self._registry:
+ raise NotModerated("The model '%s' is not currently being moderated" % model._meta.module_name)
+ del self._registry[model]
+
+ def pre_save_moderation(self, sender, comment, request, **kwargs):
+ """
+ Apply any necessary pre-save moderation steps to new
+ comments.
+
+ """
+ model = comment.content_type.model_class()
+ if model not in self._registry:
+ return
+ content_object = comment.content_object
+ moderation_class = self._registry[model]
+
+ # Comment will be disallowed outright (HTTP 403 response)
+ if not moderation_class.allow(comment, content_object, request):
+ return False
+
+ if moderation_class.moderate(comment, content_object, request):
+ comment.is_public = False
+
+ def post_save_moderation(self, sender, comment, request, **kwargs):
+ """
+ Apply any necessary post-save moderation steps to new
+ comments.
+
+ """
+ model = comment.content_type.model_class()
+ if model not in self._registry:
+ return
+ self._registry[model].email(comment, comment.content_object, request)
+
+# Import this instance in your own code to use in registering
+# your models for moderation.
+moderator = Moderator()
diff --git a/parts/django/django/contrib/comments/signals.py b/parts/django/django/contrib/comments/signals.py
new file mode 100644
index 0000000..fe1083b
--- /dev/null
+++ b/parts/django/django/contrib/comments/signals.py
@@ -0,0 +1,21 @@
+"""
+Signals relating to comments.
+"""
+from django.dispatch import Signal
+
+# Sent just before a comment will be posted (after it's been approved and
+# moderated; this can be used to modify the comment (in place) with posting
+# details or other such actions. If any receiver returns False the comment will be
+# discarded and a 403 (not allowed) response. This signal is sent at more or less
+# the same time (just before, actually) as the Comment object's pre-save signal,
+# except that the HTTP request is sent along with this signal.
+comment_will_be_posted = Signal(providing_args=["comment", "request"])
+
+# Sent just after a comment was posted. See above for how this differs
+# from the Comment object's post-save signal.
+comment_was_posted = Signal(providing_args=["comment", "request"])
+
+# Sent after a comment was "flagged" in some way. Check the flag to see if this
+# was a user requesting removal of a comment, a moderator approving/removing a
+# comment, or some other custom user flag.
+comment_was_flagged = Signal(providing_args=["comment", "flag", "created", "request"])
diff --git a/parts/django/django/contrib/comments/templates/comments/400-debug.html b/parts/django/django/contrib/comments/templates/comments/400-debug.html
new file mode 100644
index 0000000..29593b5
--- /dev/null
+++ b/parts/django/django/contrib/comments/templates/comments/400-debug.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <title>Comment post not allowed (400)</title>
+ <meta name="robots" content="NONE,NOARCHIVE" />
+ <style type="text/css">
+ html * { padding:0; margin:0; }
+ body * { padding:10px 20px; }
+ body * * { padding:0; }
+ body { font:small sans-serif; background:#eee; }
+ body>div { border-bottom:1px solid #ddd; }
+ h1 { font-weight:normal; margin-bottom:.4em; }
+ h1 span { font-size:60%; color:#666; font-weight:normal; }
+ table { border:none; border-collapse: collapse; width:100%; }
+ td, th { vertical-align:top; padding:2px 3px; }
+ th { width:12em; text-align:right; color:#666; padding-right:.5em; }
+ #info { background:#f6f6f6; }
+ #info ol { margin: 0.5em 4em; }
+ #info ol li { font-family: monospace; }
+ #summary { background: #ffc; }
+ #explanation { background:#eee; border-bottom: 0px none; }
+ </style>
+</head>
+<body>
+ <div id="summary">
+ <h1>Comment post not allowed <span>(400)</span></h1>
+ <table class="meta">
+ <tr>
+ <th>Why:</th>
+ <td>{{ why }}</td>
+ </tr>
+ </table>
+ </div>
+ <div id="info">
+ <p>
+ The comment you tried to post to this view wasn't saved because something
+ tampered with the security information in the comment form. The message
+ above should explain the problem, or you can check the <a
+ href="http://docs.djangoproject.com/en/dev/ref/contrib/comments/">comment
+ documentation</a> for more help.
+ </p>
+ </div>
+
+ <div id="explanation">
+ <p>
+ You're seeing this error because you have <code>DEBUG = True</code> in
+ your Django settings file. Change that to <code>False</code>, and Django
+ will display a standard 400 error page.
+ </p>
+ </div>
+</body>
+</html>
diff --git a/parts/django/django/contrib/comments/templates/comments/approve.html b/parts/django/django/contrib/comments/templates/comments/approve.html
new file mode 100644
index 0000000..78d15db
--- /dev/null
+++ b/parts/django/django/contrib/comments/templates/comments/approve.html
@@ -0,0 +1,15 @@
+{% extends "comments/base.html" %}
+{% load i18n %}
+
+{% block title %}{% trans "Approve a comment" %}{% endblock %}
+
+{% block content %}
+ <h1>{% trans "Really make this comment public?" %}</h1>
+ <blockquote>{{ comment|linebreaks }}</blockquote>
+ <form action="." method="post">{% csrf_token %}
+ {% if next %}<div><input type="hidden" name="next" value="{{ next }}" id="next" /></div>{% endif %}
+ <p class="submit">
+ <input type="submit" name="submit" value="{% trans "Approve" %}" /> or <a href="{{ comment.get_absolute_url }}">cancel</a>
+ </p>
+ </form>
+{% endblock %}
diff --git a/parts/django/django/contrib/comments/templates/comments/approved.html b/parts/django/django/contrib/comments/templates/comments/approved.html
new file mode 100644
index 0000000..d4ba245
--- /dev/null
+++ b/parts/django/django/contrib/comments/templates/comments/approved.html
@@ -0,0 +1,8 @@
+{% extends "comments/base.html" %}
+{% load i18n %}
+
+{% block title %}{% trans "Thanks for approving" %}.{% endblock %}
+
+{% block content %}
+ <h1>{% trans "Thanks for taking the time to improve the quality of discussion on our site" %}.</h1>
+{% endblock %}
diff --git a/parts/django/django/contrib/comments/templates/comments/base.html b/parts/django/django/contrib/comments/templates/comments/base.html
new file mode 100644
index 0000000..0f58e3e
--- /dev/null
+++ b/parts/django/django/contrib/comments/templates/comments/base.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <title>{% block title %}{% endblock %}</title>
+</head>
+<body>
+ {% block content %}{% endblock %}
+</body>
+</html>
diff --git a/parts/django/django/contrib/comments/templates/comments/delete.html b/parts/django/django/contrib/comments/templates/comments/delete.html
new file mode 100644
index 0000000..50c9a4d
--- /dev/null
+++ b/parts/django/django/contrib/comments/templates/comments/delete.html
@@ -0,0 +1,15 @@
+{% extends "comments/base.html" %}
+{% load i18n %}
+
+{% block title %}{% trans "Remove a comment" %}{% endblock %}
+
+{% block content %}
+<h1>{% trans "Really remove this comment?" %}</h1>
+ <blockquote>{{ comment|linebreaks }}</blockquote>
+ <form action="." method="post">{% csrf_token %}
+ {% if next %}<div><input type="hidden" name="next" value="{{ next }}" id="next" /></div>{% endif %}
+ <p class="submit">
+ <input type="submit" name="submit" value="{% trans "Remove" %}" /> or <a href="{{ comment.get_absolute_url }}">cancel</a>
+ </p>
+ </form>
+{% endblock %}
diff --git a/parts/django/django/contrib/comments/templates/comments/deleted.html b/parts/django/django/contrib/comments/templates/comments/deleted.html
new file mode 100644
index 0000000..e608481
--- /dev/null
+++ b/parts/django/django/contrib/comments/templates/comments/deleted.html
@@ -0,0 +1,8 @@
+{% extends "comments/base.html" %}
+{% load i18n %}
+
+{% block title %}{% trans "Thanks for removing" %}.{% endblock %}
+
+{% block content %}
+ <h1>{% trans "Thanks for taking the time to improve the quality of discussion on our site" %}.</h1>
+{% endblock %}
diff --git a/parts/django/django/contrib/comments/templates/comments/flag.html b/parts/django/django/contrib/comments/templates/comments/flag.html
new file mode 100644
index 0000000..ca7c77f
--- /dev/null
+++ b/parts/django/django/contrib/comments/templates/comments/flag.html
@@ -0,0 +1,15 @@
+{% extends "comments/base.html" %}
+{% load i18n %}
+
+{% block title %}{% trans "Flag this comment" %}{% endblock %}
+
+{% block content %}
+<h1>{% trans "Really flag this comment?" %}</h1>
+ <blockquote>{{ comment|linebreaks }}</blockquote>
+ <form action="." method="post">{% csrf_token %}
+ {% if next %}<div><input type="hidden" name="next" value="{{ next }}" id="next" /></div>{% endif %}
+ <p class="submit">
+ <input type="submit" name="submit" value="{% trans "Flag" %}" /> or <a href="{{ comment.get_absolute_url }}">cancel</a>
+ </p>
+ </form>
+{% endblock %}
diff --git a/parts/django/django/contrib/comments/templates/comments/flagged.html b/parts/django/django/contrib/comments/templates/comments/flagged.html
new file mode 100644
index 0000000..e558019
--- /dev/null
+++ b/parts/django/django/contrib/comments/templates/comments/flagged.html
@@ -0,0 +1,8 @@
+{% extends "comments/base.html" %}
+{% load i18n %}
+
+{% block title %}{% trans "Thanks for flagging" %}.{% endblock %}
+
+{% block content %}
+ <h1>{% trans "Thanks for taking the time to improve the quality of discussion on our site" %}.</h1>
+{% endblock %}
diff --git a/parts/django/django/contrib/comments/templates/comments/form.html b/parts/django/django/contrib/comments/templates/comments/form.html
new file mode 100644
index 0000000..2a9ad55
--- /dev/null
+++ b/parts/django/django/contrib/comments/templates/comments/form.html
@@ -0,0 +1,20 @@
+{% load comments i18n %}
+<form action="{% comment_form_target %}" method="post">{% csrf_token %}
+ {% if next %}<div><input type="hidden" name="next" value="{{ next }}" /></div>{% endif %}
+ {% for field in form %}
+ {% if field.is_hidden %}
+ <div>{{ field }}</div>
+ {% else %}
+ {% if field.errors %}{{ field.errors }}{% endif %}
+ <p
+ {% if field.errors %} class="error"{% endif %}
+ {% ifequal field.name "honeypot" %} style="display:none;"{% endifequal %}>
+ {{ field.label_tag }} {{ field }}
+ </p>
+ {% endif %}
+ {% endfor %}
+ <p class="submit">
+ <input type="submit" name="post" class="submit-post" value="{% trans "Post" %}" />
+ <input type="submit" name="preview" class="submit-preview" value="{% trans "Preview" %}" />
+ </p>
+</form>
diff --git a/parts/django/django/contrib/comments/templates/comments/list.html b/parts/django/django/contrib/comments/templates/comments/list.html
new file mode 100644
index 0000000..3d4ec1e
--- /dev/null
+++ b/parts/django/django/contrib/comments/templates/comments/list.html
@@ -0,0 +1,10 @@
+<dl id="comments">
+ {% for comment in comment_list %}
+ <dt id="c{{ comment.id }}">
+ {{ comment.submit_date }} - {{ comment.name }}
+ </dt>
+ <dd>
+ <p>{{ comment.comment }}</p>
+ </dd>
+ {% endfor %}
+</dl>
diff --git a/parts/django/django/contrib/comments/templates/comments/posted.html b/parts/django/django/contrib/comments/templates/comments/posted.html
new file mode 100644
index 0000000..76f7f6d
--- /dev/null
+++ b/parts/django/django/contrib/comments/templates/comments/posted.html
@@ -0,0 +1,8 @@
+{% extends "comments/base.html" %}
+{% load i18n %}
+
+{% block title %}{% trans "Thanks for commenting" %}.{% endblock %}
+
+{% block content %}
+ <h1>{% trans "Thank you for your comment" %}.</h1>
+{% endblock %}
diff --git a/parts/django/django/contrib/comments/templates/comments/preview.html b/parts/django/django/contrib/comments/templates/comments/preview.html
new file mode 100644
index 0000000..b1607b9
--- /dev/null
+++ b/parts/django/django/contrib/comments/templates/comments/preview.html
@@ -0,0 +1,36 @@
+{% extends "comments/base.html" %}
+{% load i18n %}
+
+{% block title %}{% trans "Preview your comment" %}{% endblock %}
+
+{% block content %}
+ {% load comments %}
+ <form action="{% comment_form_target %}" method="post">{% csrf_token %}
+ {% if next %}<div><input type="hidden" name="next" value="{{ next }}" /></div>{% endif %}
+ {% if form.errors %}
+ <h1>{% blocktrans count form.errors|length as counter %}Please correct the error below{% plural %}Please correct the errors below{% endblocktrans %}</h1>
+ {% else %}
+ <h1>{% trans "Preview your comment" %}</h1>
+ <blockquote>{{ comment|linebreaks }}</blockquote>
+ <p>
+ {% trans "and" %} <input type="submit" name="submit" class="submit-post" value="{% trans "Post your comment" %}" id="submit" /> {% trans "or make changes" %}:
+ </p>
+ {% endif %}
+ {% for field in form %}
+ {% if field.is_hidden %}
+ <div>{{ field }}</div>
+ {% else %}
+ {% if field.errors %}{{ field.errors }}{% endif %}
+ <p
+ {% if field.errors %} class="error"{% endif %}
+ {% ifequal field.name "honeypot" %} style="display:none;"{% endifequal %}>
+ {{ field.label_tag }} {{ field }}
+ </p>
+ {% endif %}
+ {% endfor %}
+ <p class="submit">
+ <input type="submit" name="submit" class="submit-post" value="{% trans "Post" %}" />
+ <input type="submit" name="preview" class="submit-preview" value="{% trans "Preview" %}" />
+ </p>
+ </form>
+{% endblock %}
diff --git a/parts/django/django/contrib/comments/templatetags/__init__.py b/parts/django/django/contrib/comments/templatetags/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/parts/django/django/contrib/comments/templatetags/__init__.py
diff --git a/parts/django/django/contrib/comments/templatetags/comments.py b/parts/django/django/contrib/comments/templatetags/comments.py
new file mode 100644
index 0000000..42691c6
--- /dev/null
+++ b/parts/django/django/contrib/comments/templatetags/comments.py
@@ -0,0 +1,333 @@
+from django import template
+from django.template.loader import render_to_string
+from django.conf import settings
+from django.contrib.contenttypes.models import ContentType
+from django.contrib import comments
+from django.utils.encoding import smart_unicode
+
+register = template.Library()
+
+class BaseCommentNode(template.Node):
+ """
+ Base helper class (abstract) for handling the get_comment_* template tags.
+ Looks a bit strange, but the subclasses below should make this a bit more
+ obvious.
+ """
+
+ #@classmethod
+ def handle_token(cls, parser, token):
+ """Class method to parse get_comment_list/count/form and return a Node."""
+ tokens = token.contents.split()
+ if tokens[1] != 'for':
+ raise template.TemplateSyntaxError("Second argument in %r tag must be 'for'" % tokens[0])
+
+ # {% get_whatever for obj as varname %}
+ if len(tokens) == 5:
+ if tokens[3] != 'as':
+ raise template.TemplateSyntaxError("Third argument in %r must be 'as'" % tokens[0])
+ return cls(
+ object_expr = parser.compile_filter(tokens[2]),
+ as_varname = tokens[4],
+ )
+
+ # {% get_whatever for app.model pk as varname %}
+ elif len(tokens) == 6:
+ if tokens[4] != 'as':
+ raise template.TemplateSyntaxError("Fourth argument in %r must be 'as'" % tokens[0])
+ return cls(
+ ctype = BaseCommentNode.lookup_content_type(tokens[2], tokens[0]),
+ object_pk_expr = parser.compile_filter(tokens[3]),
+ as_varname = tokens[5]
+ )
+
+ else:
+ raise template.TemplateSyntaxError("%r tag requires 4 or 5 arguments" % tokens[0])
+
+ handle_token = classmethod(handle_token)
+
+ #@staticmethod
+ def lookup_content_type(token, tagname):
+ try:
+ app, model = token.split('.')
+ return ContentType.objects.get(app_label=app, model=model)
+ except ValueError:
+ raise template.TemplateSyntaxError("Third argument in %r must be in the format 'app.model'" % tagname)
+ except ContentType.DoesNotExist:
+ raise template.TemplateSyntaxError("%r tag has non-existant content-type: '%s.%s'" % (tagname, app, model))
+ lookup_content_type = staticmethod(lookup_content_type)
+
+ def __init__(self, ctype=None, object_pk_expr=None, object_expr=None, as_varname=None, comment=None):
+ if ctype is None and object_expr is None:
+ raise template.TemplateSyntaxError("Comment nodes must be given either a literal object or a ctype and object pk.")
+ self.comment_model = comments.get_model()
+ self.as_varname = as_varname
+ self.ctype = ctype
+ self.object_pk_expr = object_pk_expr
+ self.object_expr = object_expr
+ self.comment = comment
+
+ def render(self, context):
+ qs = self.get_query_set(context)
+ context[self.as_varname] = self.get_context_value_from_queryset(context, qs)
+ return ''
+
+ def get_query_set(self, context):
+ ctype, object_pk = self.get_target_ctype_pk(context)
+ if not object_pk:
+ return self.comment_model.objects.none()
+
+ qs = self.comment_model.objects.filter(
+ content_type = ctype,
+ object_pk = smart_unicode(object_pk),
+ site__pk = settings.SITE_ID,
+ )
+
+ # The is_public and is_removed fields are implementation details of the
+ # built-in comment model's spam filtering system, so they might not
+ # be present on a custom comment model subclass. If they exist, we
+ # should filter on them.
+ field_names = [f.name for f in self.comment_model._meta.fields]
+ if 'is_public' in field_names:
+ qs = qs.filter(is_public=True)
+ if getattr(settings, 'COMMENTS_HIDE_REMOVED', True) and 'is_removed' in field_names:
+ qs = qs.filter(is_removed=False)
+
+ return qs
+
+ def get_target_ctype_pk(self, context):
+ if self.object_expr:
+ try:
+ obj = self.object_expr.resolve(context)
+ except template.VariableDoesNotExist:
+ return None, None
+ return ContentType.objects.get_for_model(obj), obj.pk
+ else:
+ return self.ctype, self.object_pk_expr.resolve(context, ignore_failures=True)
+
+ def get_context_value_from_queryset(self, context, qs):
+ """Subclasses should override this."""
+ raise NotImplementedError
+
+class CommentListNode(BaseCommentNode):
+ """Insert a list of comments into the context."""
+ def get_context_value_from_queryset(self, context, qs):
+ return list(qs)
+
+class CommentCountNode(BaseCommentNode):
+ """Insert a count of comments into the context."""
+ def get_context_value_from_queryset(self, context, qs):
+ return qs.count()
+
+class CommentFormNode(BaseCommentNode):
+ """Insert a form for the comment model into the context."""
+
+ def get_form(self, context):
+ ctype, object_pk = self.get_target_ctype_pk(context)
+ if object_pk:
+ return comments.get_form()(ctype.get_object_for_this_type(pk=object_pk))
+ else:
+ return None
+
+ def render(self, context):
+ context[self.as_varname] = self.get_form(context)
+ return ''
+
+class RenderCommentFormNode(CommentFormNode):
+ """Render the comment form directly"""
+
+ #@classmethod
+ def handle_token(cls, parser, token):
+ """Class method to parse render_comment_form and return a Node."""
+ tokens = token.contents.split()
+ if tokens[1] != 'for':
+ raise template.TemplateSyntaxError("Second argument in %r tag must be 'for'" % tokens[0])
+
+ # {% render_comment_form for obj %}
+ if len(tokens) == 3:
+ return cls(object_expr=parser.compile_filter(tokens[2]))
+
+ # {% render_comment_form for app.models pk %}
+ elif len(tokens) == 4:
+ return cls(
+ ctype = BaseCommentNode.lookup_content_type(tokens[2], tokens[0]),
+ object_pk_expr = parser.compile_filter(tokens[3])
+ )
+ handle_token = classmethod(handle_token)
+
+ def render(self, context):
+ ctype, object_pk = self.get_target_ctype_pk(context)
+ if object_pk:
+ template_search_list = [
+ "comments/%s/%s/form.html" % (ctype.app_label, ctype.model),
+ "comments/%s/form.html" % ctype.app_label,
+ "comments/form.html"
+ ]
+ context.push()
+ formstr = render_to_string(template_search_list, {"form" : self.get_form(context)}, context)
+ context.pop()
+ return formstr
+ else:
+ return ''
+
+class RenderCommentListNode(CommentListNode):
+ """Render the comment list directly"""
+
+ #@classmethod
+ def handle_token(cls, parser, token):
+ """Class method to parse render_comment_list and return a Node."""
+ tokens = token.contents.split()
+ if tokens[1] != 'for':
+ raise template.TemplateSyntaxError("Second argument in %r tag must be 'for'" % tokens[0])
+
+ # {% render_comment_list for obj %}
+ if len(tokens) == 3:
+ return cls(object_expr=parser.compile_filter(tokens[2]))
+
+ # {% render_comment_list for app.models pk %}
+ elif len(tokens) == 4:
+ return cls(
+ ctype = BaseCommentNode.lookup_content_type(tokens[2], tokens[0]),
+ object_pk_expr = parser.compile_filter(tokens[3])
+ )
+ handle_token = classmethod(handle_token)
+
+ def render(self, context):
+ ctype, object_pk = self.get_target_ctype_pk(context)
+ if object_pk:
+ template_search_list = [
+ "comments/%s/%s/list.html" % (ctype.app_label, ctype.model),
+ "comments/%s/list.html" % ctype.app_label,
+ "comments/list.html"
+ ]
+ qs = self.get_query_set(context)
+ context.push()
+ liststr = render_to_string(template_search_list, {
+ "comment_list" : self.get_context_value_from_queryset(context, qs)
+ }, context)
+ context.pop()
+ return liststr
+ else:
+ return ''
+
+# We could just register each classmethod directly, but then we'd lose out on
+# the automagic docstrings-into-admin-docs tricks. So each node gets a cute
+# wrapper function that just exists to hold the docstring.
+
+#@register.tag
+def get_comment_count(parser, token):
+ """
+ Gets the comment count for the given params and populates the template
+ context with a variable containing that value, whose name is defined by the
+ 'as' clause.
+
+ Syntax::
+
+ {% get_comment_count for [object] as [varname] %}
+ {% get_comment_count for [app].[model] [object_id] as [varname] %}
+
+ Example usage::
+
+ {% get_comment_count for event as comment_count %}
+ {% get_comment_count for calendar.event event.id as comment_count %}
+ {% get_comment_count for calendar.event 17 as comment_count %}
+
+ """
+ return CommentCountNode.handle_token(parser, token)
+
+#@register.tag
+def get_comment_list(parser, token):
+ """
+ Gets the list of comments for the given params and populates the template
+ context with a variable containing that value, whose name is defined by the
+ 'as' clause.
+
+ Syntax::
+
+ {% get_comment_list for [object] as [varname] %}
+ {% get_comment_list for [app].[model] [object_id] as [varname] %}
+
+ Example usage::
+
+ {% get_comment_list for event as comment_list %}
+ {% for comment in comment_list %}
+ ...
+ {% endfor %}
+
+ """
+ return CommentListNode.handle_token(parser, token)
+
+#@register.tag
+def render_comment_list(parser, token):
+ """
+ Render the comment list (as returned by ``{% get_comment_list %}``)
+ through the ``comments/list.html`` template
+
+ Syntax::
+
+ {% render_comment_list for [object] %}
+ {% render_comment_list for [app].[model] [object_id] %}
+
+ Example usage::
+
+ {% render_comment_list for event %}
+
+ """
+ return RenderCommentListNode.handle_token(parser, token)
+
+#@register.tag
+def get_comment_form(parser, token):
+ """
+ Get a (new) form object to post a new comment.
+
+ Syntax::
+
+ {% get_comment_form for [object] as [varname] %}
+ {% get_comment_form for [app].[model] [object_id] as [varname] %}
+ """
+ return CommentFormNode.handle_token(parser, token)
+
+#@register.tag
+def render_comment_form(parser, token):
+ """
+ Render the comment form (as returned by ``{% render_comment_form %}``) through
+ the ``comments/form.html`` template.
+
+ Syntax::
+
+ {% render_comment_form for [object] %}
+ {% render_comment_form for [app].[model] [object_id] %}
+ """
+ return RenderCommentFormNode.handle_token(parser, token)
+
+#@register.simple_tag
+def comment_form_target():
+ """
+ Get the target URL for the comment form.
+
+ Example::
+
+ <form action="{% comment_form_target %}" method="post">
+ """
+ return comments.get_form_target()
+
+#@register.simple_tag
+def get_comment_permalink(comment, anchor_pattern=None):
+ """
+ Get the permalink for a comment, optionally specifying the format of the
+ named anchor to be appended to the end of the URL.
+
+ Example::
+ {{ get_comment_permalink comment "#c%(id)s-by-%(user_name)s" }}
+ """
+
+ if anchor_pattern:
+ return comment.get_absolute_url(anchor_pattern)
+ return comment.get_absolute_url()
+
+register.tag(get_comment_count)
+register.tag(get_comment_list)
+register.tag(get_comment_form)
+register.tag(render_comment_form)
+register.simple_tag(comment_form_target)
+register.simple_tag(get_comment_permalink)
+register.tag(render_comment_list)
diff --git a/parts/django/django/contrib/comments/urls.py b/parts/django/django/contrib/comments/urls.py
new file mode 100644
index 0000000..d903779
--- /dev/null
+++ b/parts/django/django/contrib/comments/urls.py
@@ -0,0 +1,16 @@
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns('django.contrib.comments.views',
+ url(r'^post/$', 'comments.post_comment', name='comments-post-comment'),
+ url(r'^posted/$', 'comments.comment_done', name='comments-comment-done'),
+ url(r'^flag/(\d+)/$', 'moderation.flag', name='comments-flag'),
+ url(r'^flagged/$', 'moderation.flag_done', name='comments-flag-done'),
+ url(r'^delete/(\d+)/$', 'moderation.delete', name='comments-delete'),
+ url(r'^deleted/$', 'moderation.delete_done', name='comments-delete-done'),
+ url(r'^approve/(\d+)/$', 'moderation.approve', name='comments-approve'),
+ url(r'^approved/$', 'moderation.approve_done', name='comments-approve-done'),
+)
+
+urlpatterns += patterns('',
+ url(r'^cr/(\d+)/(.+)/$', 'django.contrib.contenttypes.views.shortcut', name='comments-url-redirect'),
+)
diff --git a/parts/django/django/contrib/comments/views/__init__.py b/parts/django/django/contrib/comments/views/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/parts/django/django/contrib/comments/views/__init__.py
diff --git a/parts/django/django/contrib/comments/views/comments.py b/parts/django/django/contrib/comments/views/comments.py
new file mode 100644
index 0000000..c2b553f
--- /dev/null
+++ b/parts/django/django/contrib/comments/views/comments.py
@@ -0,0 +1,136 @@
+from django import http
+from django.conf import settings
+from utils import next_redirect, confirmation_view
+from django.core.exceptions import ObjectDoesNotExist, ValidationError
+from django.db import models
+from django.shortcuts import render_to_response
+from django.template import RequestContext
+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
+from django.views.decorators.csrf import csrf_protect
+
+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})
+
+@csrf_protect
+@require_POST
+def post_comment(request, next=None, using=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
+
+ # Check to see if the POST data overrides the view's next argument.
+ next = data.get("next", next)
+
+ # 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.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), e:
+ return CommentPostBadRequest(
+ "Attempting go get content-type %r and object PK %r exists 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 = 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:
+ 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.module_name),
+ "comments/%s_preview.html" % model._meta.app_label,
+ # Now the usual directory based template heirarchy.
+ "comments/%s/%s/preview.html" % (model._meta.app_label, model._meta.module_name),
+ "comments/%s/preview.html" % model._meta.app_label,
+ "comments/preview.html",
+ ]
+ return render_to_response(
+ template_list, {
+ "comment" : form.data.get("comment", ""),
+ "form" : form,
+ "next": next,
+ },
+ RequestContext(request, {})
+ )
+
+ # 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
+
+ # 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 == False:
+ 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(data, next, comment_done, c=comment._get_pk_val())
+
+comment_done = confirmation_view(
+ template = "comments/posted.html",
+ doc = """Display a "comment was posted" success page."""
+)
+
diff --git a/parts/django/django/contrib/comments/views/moderation.py b/parts/django/django/contrib/comments/views/moderation.py
new file mode 100644
index 0000000..73304ba
--- /dev/null
+++ b/parts/django/django/contrib/comments/views/moderation.py
@@ -0,0 +1,159 @@
+from django import template
+from django.conf import settings
+from django.shortcuts import get_object_or_404, render_to_response
+from django.contrib.auth.decorators import login_required, permission_required
+from utils import next_redirect, confirmation_view
+from django.contrib import comments
+from django.contrib.comments import signals
+from django.views.decorators.csrf import csrf_protect
+
+@csrf_protect
+@login_required
+def flag(request, comment_id, next=None):
+ """
+ Flags a comment. Confirmation on GET, action on POST.
+
+ Templates: `comments/flag.html`,
+ Context:
+ comment
+ the flagged `comments.comment` object
+ """
+ comment = get_object_or_404(comments.get_model(), pk=comment_id, site__pk=settings.SITE_ID)
+
+ # Flag on POST
+ if request.method == 'POST':
+ perform_flag(request, comment)
+ return next_redirect(request.POST.copy(), next, flag_done, c=comment.pk)
+
+ # Render a form on GET
+ else:
+ return render_to_response('comments/flag.html',
+ {'comment': comment, "next": next},
+ template.RequestContext(request)
+ )
+
+@csrf_protect
+@permission_required("comments.can_moderate")
+def delete(request, comment_id, next=None):
+ """
+ Deletes a comment. Confirmation on GET, action on POST. Requires the "can
+ moderate comments" permission.
+
+ Templates: `comments/delete.html`,
+ Context:
+ comment
+ the flagged `comments.comment` object
+ """
+ comment = get_object_or_404(comments.get_model(), pk=comment_id, site__pk=settings.SITE_ID)
+
+ # Delete on POST
+ if request.method == 'POST':
+ # Flag the comment as deleted instead of actually deleting it.
+ perform_delete(request, comment)
+ return next_redirect(request.POST.copy(), next, delete_done, c=comment.pk)
+
+ # Render a form on GET
+ else:
+ return render_to_response('comments/delete.html',
+ {'comment': comment, "next": next},
+ template.RequestContext(request)
+ )
+
+@csrf_protect
+@permission_required("comments.can_moderate")
+def approve(request, comment_id, next=None):
+ """
+ Approve a comment (that is, mark it as public and non-removed). Confirmation
+ on GET, action on POST. Requires the "can moderate comments" permission.
+
+ Templates: `comments/approve.html`,
+ Context:
+ comment
+ the `comments.comment` object for approval
+ """
+ comment = get_object_or_404(comments.get_model(), pk=comment_id, site__pk=settings.SITE_ID)
+
+ # Delete on POST
+ if request.method == 'POST':
+ # Flag the comment as approved.
+ perform_approve(request, comment)
+ return next_redirect(request.POST.copy(), next, approve_done, c=comment.pk)
+
+ # Render a form on GET
+ else:
+ return render_to_response('comments/approve.html',
+ {'comment': comment, "next": next},
+ template.RequestContext(request)
+ )
+
+# The following functions actually perform the various flag/aprove/delete
+# actions. They've been broken out into seperate functions to that they
+# may be called from admin actions.
+
+def perform_flag(request, comment):
+ """
+ Actually perform the flagging of a comment from a request.
+ """
+ flag, created = comments.models.CommentFlag.objects.get_or_create(
+ comment = comment,
+ user = request.user,
+ flag = comments.models.CommentFlag.SUGGEST_REMOVAL
+ )
+ signals.comment_was_flagged.send(
+ sender = comment.__class__,
+ comment = comment,
+ flag = flag,
+ created = created,
+ request = request,
+ )
+
+def perform_delete(request, comment):
+ flag, created = comments.models.CommentFlag.objects.get_or_create(
+ comment = comment,
+ user = request.user,
+ flag = comments.models.CommentFlag.MODERATOR_DELETION
+ )
+ comment.is_removed = True
+ comment.save()
+ signals.comment_was_flagged.send(
+ sender = comment.__class__,
+ comment = comment,
+ flag = flag,
+ created = created,
+ request = request,
+ )
+
+
+def perform_approve(request, comment):
+ flag, created = comments.models.CommentFlag.objects.get_or_create(
+ comment = comment,
+ user = request.user,
+ flag = comments.models.CommentFlag.MODERATOR_APPROVAL,
+ )
+
+ comment.is_removed = False
+ comment.is_public = True
+ comment.save()
+
+ signals.comment_was_flagged.send(
+ sender = comment.__class__,
+ comment = comment,
+ flag = flag,
+ created = created,
+ request = request,
+ )
+
+# Confirmation views.
+
+flag_done = confirmation_view(
+ template = "comments/flagged.html",
+ doc = 'Displays a "comment was flagged" success page.'
+)
+delete_done = confirmation_view(
+ template = "comments/deleted.html",
+ doc = 'Displays a "comment was deleted" success page.'
+)
+approve_done = confirmation_view(
+ template = "comments/approved.html",
+ doc = 'Displays a "comment was approved" success page.'
+)
diff --git a/parts/django/django/contrib/comments/views/utils.py b/parts/django/django/contrib/comments/views/utils.py
new file mode 100644
index 0000000..8b729d2
--- /dev/null
+++ b/parts/django/django/contrib/comments/views/utils.py
@@ -0,0 +1,58 @@
+"""
+A few bits of helper functions for comment views.
+"""
+
+import urllib
+import textwrap
+from django.http import HttpResponseRedirect
+from django.core import urlresolvers
+from django.shortcuts import render_to_response
+from django.template import RequestContext
+from django.core.exceptions import ObjectDoesNotExist
+from django.contrib import comments
+
+def next_redirect(data, default, default_view, **get_kwargs):
+ """
+ Handle the "where should I go next?" part of comment views.
+
+ The next value could be a kwarg to the function (``default``), or a
+ ``?next=...`` GET arg, or the URL of a given view (``default_view``). See
+ the view modules for examples.
+
+ Returns an ``HttpResponseRedirect``.
+ """
+ next = data.get("next", default)
+ if next is None:
+ next = urlresolvers.reverse(default_view)
+ if get_kwargs:
+ joiner = ('?' in next) and '&' or '?'
+ next += joiner + urllib.urlencode(get_kwargs)
+ return HttpResponseRedirect(next)
+
+def confirmation_view(template, doc="Display a confirmation view."):
+ """
+ Confirmation view generator for the "comment was
+ posted/flagged/deleted/approved" views.
+ """
+ def confirmed(request):
+ comment = None
+ if 'c' in request.GET:
+ try:
+ comment = comments.get_model().objects.get(pk=request.GET['c'])
+ except (ObjectDoesNotExist, ValueError):
+ pass
+ return render_to_response(template,
+ {'comment': comment},
+ context_instance=RequestContext(request)
+ )
+
+ confirmed.__doc__ = textwrap.dedent("""\
+ %s
+
+ Templates: `%s``
+ Context:
+ comment
+ The posted comment
+ """ % (doc, template)
+ )
+ return confirmed