summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/lib/django_comments/__init__.py92
-rw-r--r--app/lib/django_comments/admin.py87
-rw-r--r--app/lib/django_comments/feeds.py32
-rw-r--r--app/lib/django_comments/forms.py197
-rw-r--r--app/lib/django_comments/managers.py22
-rw-r--r--app/lib/django_comments/models.py201
-rw-r--r--app/lib/django_comments/moderation.py357
-rw-r--r--app/lib/django_comments/signals.py21
-rw-r--r--app/lib/django_comments/templatetags/__init__.py0
-rw-r--r--app/lib/django_comments/templatetags/comments.py334
-rw-r--r--app/lib/django_comments/urls.py16
-rw-r--r--app/lib/django_comments/views/__init__.py0
-rw-r--r--app/lib/django_comments/views/comments.py136
-rw-r--r--app/lib/django_comments/views/moderation.py165
-rw-r--r--app/lib/django_comments/views/utils.py71
-rw-r--r--app/links/sync_links.py2
16 files changed, 1733 insertions, 0 deletions
diff --git a/app/lib/django_comments/__init__.py b/app/lib/django_comments/__init__.py
new file mode 100644
index 0000000..a78b21a
--- /dev/null
+++ b/app/lib/django_comments/__init__.py
@@ -0,0 +1,92 @@
+from django.conf import settings
+from django.core import urlresolvers
+from django.core.exceptions import ImproperlyConfigured
+from django.utils.importlib import import_module
+
+from django_comments.models import Comment
+from django_comments.forms import CommentForm
+
+DEFAULT_COMMENTS_APP = 'django_comments'
+
+def get_comment_app():
+ """
+ Get the comment app (i.e. "django_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 as e:
+ raise ImproperlyConfigured("The COMMENTS_APP setting refers to "\
+ "a non-existing package. (%s)" % e)
+
+ 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_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_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_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_comments.views.moderation.approve",
+ args=(comment.id,))
diff --git a/app/lib/django_comments/admin.py b/app/lib/django_comments/admin.py
new file mode 100644
index 0000000..8a5c063
--- /dev/null
+++ b/app/lib/django_comments/admin.py
@@ -0,0 +1,87 @@
+from __future__ import unicode_literals
+
+from django.contrib import admin
+from django.contrib.auth import get_user_model
+from django.utils.translation import ugettext_lazy as _, ungettext
+
+from django_comments.models import Comment
+from django_comments import get_model
+from django_comments.views.moderation import perform_flag, perform_approve, perform_delete
+
+
+class UsernameSearch(object):
+ """The User object may not be auth.User, so we need to provide
+ a mechanism for issuing the equivalent of a .filter(user__username=...)
+ search in CommentAdmin.
+ """
+ def __str__(self):
+ return 'user__%s' % get_user_model().USERNAME_FIELD
+
+
+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', UsernameSearch(), '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('django_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('1 comment was successfully %(action)s.',
+ '%(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/app/lib/django_comments/feeds.py b/app/lib/django_comments/feeds.py
new file mode 100644
index 0000000..0b8ce5b
--- /dev/null
+++ b/app/lib/django_comments/feeds.py
@@ -0,0 +1,32 @@
+from django.contrib.syndication.views import Feed
+from django.contrib.sites.models import get_current_site
+from django.utils.translation import ugettext as _
+
+import django_comments
+
+class LatestCommentFeed(Feed):
+ """Feed of latest comments on the current site."""
+
+ def __call__(self, request, *args, **kwargs):
+ self.site = get_current_site(request)
+ return super(LatestCommentFeed, self).__call__(request, *args, **kwargs)
+
+ def title(self):
+ return _("%(site_name)s comments") % dict(site_name=self.site.name)
+
+ def link(self):
+ return "http://%s/" % (self.site.domain)
+
+ def description(self):
+ return _("Latest comments on %(site_name)s") % dict(site_name=self.site.name)
+
+ def items(self):
+ qs = django_comments.get_model().objects.filter(
+ site__pk = self.site.pk,
+ is_public = True,
+ is_removed = False,
+ )
+ return qs.order_by('-submit_date')[:40]
+
+ def item_pubdate(self, item):
+ return item.submit_date
diff --git a/app/lib/django_comments/forms.py b/app/lib/django_comments/forms.py
new file mode 100644
index 0000000..6a3082f
--- /dev/null
+++ b/app/lib/django_comments/forms.py
@@ -0,0 +1,197 @@
+import time
+from django import forms
+from django.forms.util import ErrorDict
+from django.conf import settings
+from django.contrib.contenttypes.models import ContentType
+from django.utils.crypto import salted_hmac, constant_time_compare
+from django.utils.encoding import force_text
+from django.utils.text import get_text_list
+from django.utils import timezone
+from django.utils.translation import ungettext, ugettext, ugettext_lazy as _
+
+from django_comments.models import Comment
+
+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 not constant_time_compare(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 HMAC security hash from the provided info.
+ """
+ info = (content_type, object_pk, timestamp)
+ key_salt = "django.contrib.forms.CommentSecurityForm"
+ value = "-".join(info)
+ return salted_hmac(key_salt, value).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_text(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 = timezone.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:
+ raise forms.ValidationError(ungettext(
+ "Watch your mouth! The word %s is not allowed here.",
+ "Watch your mouth! The words %s are not allowed here.",
+ len(bad_words)) % get_text_list(
+ ['"%s%s%s"' % (i[0], '-'*(len(i)-2), i[-1])
+ for i in bad_words], ugettext('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/app/lib/django_comments/managers.py b/app/lib/django_comments/managers.py
new file mode 100644
index 0000000..bc0fc5f
--- /dev/null
+++ b/app/lib/django_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_text
+
+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_text(model._get_pk_val()))
+ return qs
diff --git a/app/lib/django_comments/models.py b/app/lib/django_comments/models.py
new file mode 100644
index 0000000..08bad3c
--- /dev/null
+++ b/app/lib/django_comments/models.py
@@ -0,0 +1,201 @@
+from django.conf import settings
+from django.contrib.contenttypes import generic
+from django.contrib.contenttypes.models import ContentType
+from django.contrib.sites.models import Site
+from django.core import urlresolvers
+from django.db import models
+from django.utils.translation import ugettext_lazy as _
+from django.utils import timezone
+from django.utils.encoding import python_2_unicode_compatible
+
+from django_comments.managers import CommentManager
+
+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)
+ )
+
+
+@python_2_unicode_compatible
+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(settings.AUTH_USER_MODEL, 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.GenericIPAddressField(_('IP address'), unpack_ipv4=True, 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 __str__(self):
+ return "%s: %s..." % (self.name, self.comment[:50])
+
+ def save(self, *args, **kwargs):
+ if self.submit_date is None:
+ self.submit_date = timezone.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"):
+ userinfo = {
+ "name": self.user_name,
+ "email": self.user_email,
+ "url": self.user_url
+ }
+ if self.user_id:
+ u = self.user
+ if u.email:
+ 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():
+ userinfo["name"] = self.user.get_full_name()
+ elif not self.user_name:
+ userinfo["name"] = u.get_username()
+ self._userinfo = userinfo
+ 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
+
+
+@python_2_unicode_compatible
+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(settings.AUTH_USER_MODEL, 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 __str__(self):
+ return "%s flag of comment ID %s by %s" % \
+ (self.flag, self.comment_id, self.user.get_username())
+
+ def save(self, *args, **kwargs):
+ if self.flag_date is None:
+ self.flag_date = timezone.now()
+ super(CommentFlag, self).save(*args, **kwargs)
diff --git a/app/lib/django_comments/moderation.py b/app/lib/django_comments/moderation.py
new file mode 100644
index 0000000..ebb29da
--- /dev/null
+++ b/app/lib/django_comments/moderation.py
@@ -0,0 +1,357 @@
+"""
+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_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.db.models.base import ModelBase
+from django.template import Context, loader
+from django.contrib.sites.shortcuts import get_current_site
+from django.utils import timezone
+
+import django_comments
+from django_comments import signals
+
+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 is not None:
+ close_after_date = getattr(content_object, self.auto_close_field)
+ if close_after_date is not None and self._get_delta(timezone.now(), close_after_date).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 is not None:
+ moderate_after_date = getattr(content_object, self.auto_moderate_field)
+ if moderate_after_date is not None and self._get_delta(timezone.now(), moderate_after_date).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"' % (get_current_site(request).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=django_comments.get_model())
+ signals.comment_was_posted.connect(self.post_save_moderation, sender=django_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/app/lib/django_comments/signals.py b/app/lib/django_comments/signals.py
new file mode 100644
index 0000000..079afaf
--- /dev/null
+++ b/app/lib/django_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 400 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/app/lib/django_comments/templatetags/__init__.py b/app/lib/django_comments/templatetags/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/lib/django_comments/templatetags/__init__.py
diff --git a/app/lib/django_comments/templatetags/comments.py b/app/lib/django_comments/templatetags/comments.py
new file mode 100644
index 0000000..5ff156b
--- /dev/null
+++ b/app/lib/django_comments/templatetags/comments.py
@@ -0,0 +1,334 @@
+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.utils.encoding import smart_text
+
+import django_comments
+
+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.split_contents()
+ 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])
+
+ @staticmethod
+ def lookup_content_type(token, tagname):
+ try:
+ app, model = token.split('.')
+ return ContentType.objects.get_by_natural_key(app, 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))
+
+ 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 = django_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_text(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):
+ obj = self.get_object(context)
+ if obj:
+ return django_comments.get_form()(obj)
+ else:
+ return None
+
+ def get_object(self, context):
+ if self.object_expr:
+ try:
+ return self.object_expr.resolve(context)
+ except template.VariableDoesNotExist:
+ return None
+ else:
+ object_pk = self.object_pk_expr.resolve(context,
+ ignore_failures=True)
+ return self.ctype.get_object_for_this_type(pk=object_pk)
+
+ 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.split_contents()
+ 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])
+ )
+
+ 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.split_contents()
+ 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])
+ )
+
+ 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 django_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()
+
diff --git a/app/lib/django_comments/urls.py b/app/lib/django_comments/urls.py
new file mode 100644
index 0000000..e02bab0
--- /dev/null
+++ b/app/lib/django_comments/urls.py
@@ -0,0 +1,16 @@
+from django.conf.urls import patterns, url
+
+urlpatterns = patterns('django_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/app/lib/django_comments/views/__init__.py b/app/lib/django_comments/views/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/lib/django_comments/views/__init__.py
diff --git a/app/lib/django_comments/views/comments.py b/app/lib/django_comments/views/comments.py
new file mode 100644
index 0000000..92084b6
--- /dev/null
+++ b/app/lib/django_comments/views/comments.py
@@ -0,0 +1,136 @@
+from __future__ import absolute_import
+
+from django import http
+from django.conf import settings
+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.csrf import csrf_protect
+from django.views.decorators.http import require_POST
+
+import django_comments
+from django_comments import signals
+from django_comments.views.utils import next_redirect, confirmation_view
+
+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})
+
+
+@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.get_username()
+ if not data.get('email', ''):
+ data["email"] = request.user.email
+
+ # 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) as 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 = django_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.model_name),
+ "comments/%s_preview.html" % model._meta.app_label,
+ # Now the usual directory based template hierarchy.
+ "comments/%s/%s/preview.html" % (model._meta.app_label, model._meta.model_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": data.get("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(request, fallback=next or 'comments-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/app/lib/django_comments/views/moderation.py b/app/lib/django_comments/views/moderation.py
new file mode 100644
index 0000000..b35626a
--- /dev/null
+++ b/app/lib/django_comments/views/moderation.py
@@ -0,0 +1,165 @@
+from __future__ import absolute_import
+
+from django import template
+from django.conf import settings
+from django.contrib.auth.decorators import login_required, permission_required
+from django.shortcuts import get_object_or_404, render_to_response
+from django.views.decorators.csrf import csrf_protect
+
+import django_comments
+from django_comments import signals
+from django_comments.views.utils import next_redirect, confirmation_view
+
+@csrf_protect
+@login_required
+def flag(request, comment_id, next=None):
+ """
+ Flags a comment. Confirmation on GET, action on POST.
+
+ Templates: :template:`comments/flag.html`,
+ Context:
+ comment
+ the flagged `comments.comment` object
+ """
+ comment = get_object_or_404(django_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, fallback=next or 'comments-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("django_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: :template:`comments/delete.html`,
+ Context:
+ comment
+ the flagged `comments.comment` object
+ """
+ comment = get_object_or_404(django_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, fallback=next or 'comments-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("django_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: :template:`comments/approve.html`,
+ Context:
+ comment
+ the `comments.comment` object for approval
+ """
+ comment = get_object_or_404(django_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, fallback=next or 'comments-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 separate 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 = django_comments.models.CommentFlag.objects.get_or_create(
+ comment = comment,
+ user = request.user,
+ flag = django_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 = django_comments.models.CommentFlag.objects.get_or_create(
+ comment = comment,
+ user = request.user,
+ flag = django_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 = django_comments.models.CommentFlag.objects.get_or_create(
+ comment = comment,
+ user = request.user,
+ flag = django_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/app/lib/django_comments/views/utils.py b/app/lib/django_comments/views/utils.py
new file mode 100644
index 0000000..314ab8c
--- /dev/null
+++ b/app/lib/django_comments/views/utils.py
@@ -0,0 +1,71 @@
+"""
+A few bits of helper functions for comment views.
+"""
+
+import textwrap
+try:
+ from urllib.parse import urlencode
+except ImportError: # Python 2
+ from urllib import urlencode
+
+from django.http import HttpResponseRedirect
+from django.shortcuts import render_to_response, resolve_url
+from django.template import RequestContext
+from django.core.exceptions import ObjectDoesNotExist
+from django.utils.http import is_safe_url
+
+import django_comments
+
+def next_redirect(request, fallback, **get_kwargs):
+ """
+ Handle the "where should I go next?" part of comment views.
+
+ The next value could be a
+ ``?next=...`` GET arg or the URL of a given view (``fallback``). See
+ the view modules for examples.
+
+ Returns an ``HttpResponseRedirect``.
+ """
+ next = request.POST.get('next')
+ if not is_safe_url(url=next, host=request.get_host()):
+ next = resolve_url(fallback)
+
+ if get_kwargs:
+ if '#' in next:
+ tmp = next.rsplit('#', 1)
+ next = tmp[0]
+ anchor = '#' + tmp[1]
+ else:
+ anchor = ''
+
+ joiner = ('?' in next) and '&' or '?'
+ next += joiner + urlencode(get_kwargs) + anchor
+ 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 = django_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: :template:`%s``
+ Context:
+ comment
+ The posted comment
+ """ % (doc, template)
+ )
+ return confirmed
diff --git a/app/links/sync_links.py b/app/links/sync_links.py
index 7a0cd9c..a9fa1bf 100644
--- a/app/links/sync_links.py
+++ b/app/links/sync_links.py
@@ -1,6 +1,7 @@
import sys
import os
from os.path import dirname, abspath
+import django
PROJECT_ROOT = abspath(dirname(dirname(dirname(__file__)))) + '/'
#PROJECT_ROOT = abspath(dirname(dirname(__file__)))
sys.path.append(PROJECT_ROOT)
@@ -9,5 +10,6 @@ sys.path.append(PROJECT_ROOT + '/app/lib')
sys.path.append(PROJECT_ROOT + '/config')
sys.path.append('/home/luxagraf/apps/venv/bin/python2.7/')
os.environ['DJANGO_SETTINGS_MODULE'] = 'config.settings'
+django.setup()
from links import retriever
retriever.sync_pinboard_links()