diff options
Diffstat (limited to 'parts/django/django/contrib/comments')
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 |