Django

Code

Changeset 8557

Show
Ignore:
Timestamp:
08/25/08 17:14:22 (10 months ago)
Author:
jacob
Message:

Refactored Django's comment system.

Much of this work was done by Thejaswi Puthraya as part of Google's Summer of Code project; much thanks to him for the work, and to them for the program.

This is a backwards-incompatible change; see the upgrading guide in docs/ref/contrib/comments/upgrade.txt for instructions if you were using the old comments system.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • django/trunk/AUTHORS

    r8539 r8557  
    323323    Matthias Pronk <django@masida.nl> 
    324324    Jyrki Pulliainen <jyrki.pulliainen@gmail.com> 
     325    Thejaswi Puthraya <thejaswi.puthraya@gmail.com> 
    325326    Johann Queuniet <johann.queuniet@adh.naellia.eu> 
    326327    Jan Rademaker 
  • django/trunk/django/contrib/comments/admin.py

    r7967 r8557  
    11from django.contrib import admin 
    2 from django.contrib.comments.models import Comment, FreeComment 
     2from django.conf import settings 
     3from django.contrib.comments.models import Comment 
     4from django.utils.translation import ugettext_lazy as _ 
    35 
     6class CommentsAdmin(admin.ModelAdmin): 
     7    fieldsets = ( 
     8        (None, 
     9           {'fields': ('content_type', 'object_pk', 'site')} 
     10        ), 
     11        (_('Content'), 
     12           {'fields': ('user', 'user_name', 'user_email', 'user_url', 'comment')} 
     13        ), 
     14        (_('Metadata'), 
     15           {'fields': ('submit_date', 'ip_address', 'is_public', 'is_removed')} 
     16        ), 
     17     ) 
    418 
    5 class CommentAdmin(admin.ModelAdmin): 
    6     fieldsets = ( 
    7         (None, {'fields': ('content_type', 'object_id', 'site')}), 
    8         ('Content', {'fields': ('user', 'headline', 'comment')}), 
    9         ('Ratings', {'fields': ('rating1', 'rating2', 'rating3', 'rating4', 'rating5', 'rating6', 'rating7', 'rating8', 'valid_rating')}), 
    10         ('Meta', {'fields': ('is_public', 'is_removed', 'ip_address')}), 
    11     ) 
    12     list_display = ('user', 'submit_date', 'content_type', 'get_content_object') 
    13     list_filter = ('submit_date',) 
     19    list_display = ('name', 'content_type', 'object_pk', 'ip_address', 'is_public', 'is_removed') 
     20    list_filter = ('submit_date', 'site', 'is_public', 'is_removed') 
    1421    date_hierarchy = 'submit_date' 
    15     search_fields = ('comment', 'user__username') 
    16     raw_id_fields = ('user',) 
     22    search_fields = ('comment', 'user__username', 'user_name', 'user_email', 'user_url', 'ip_address') 
    1723 
    18 class FreeCommentAdmin(admin.ModelAdmin): 
    19     fieldsets = ( 
    20         (None, {'fields': ('content_type', 'object_id', 'site')}), 
    21         ('Content', {'fields': ('person_name', 'comment')}), 
    22         ('Meta', {'fields': ('is_public', 'ip_address', 'approved')}), 
    23     ) 
    24     list_display = ('person_name', 'submit_date', 'content_type', 'get_content_object') 
    25     list_filter = ('submit_date',) 
    26     date_hierarchy = 'submit_date' 
    27     search_fields = ('comment', 'person_name') 
    28  
    29 admin.site.register(Comment, CommentAdmin) 
    30 admin.site.register(FreeComment, FreeCommentAdmin) 
     24admin.site.register(Comment, CommentsAdmin) 
  • django/trunk/django/contrib/comments/feeds.py

    r6529 r8557  
    11from django.conf import settings 
    2 from django.contrib.comments.models import Comment, FreeComment 
    32from django.contrib.syndication.feeds import Feed 
    43from django.contrib.sites.models import Site 
     4from django.contrib import comments 
    55 
    6 class LatestFreeCommentsFeed(Feed): 
    7     """Feed of latest free comments on the current site.""" 
    8  
    9     comments_class = FreeComment 
     6class LatestCommentFeed(Feed): 
     7    """Feed of latest comments on the current site.""" 
    108 
    119    def title(self): 
     
    2422        return u"Latest comments on %s" % self._site.name 
    2523 
    26     def get_query_set(self): 
    27         return self.comments_class.objects.filter(site__pk=settings.SITE_ID, is_public=True) 
    28  
    2924    def items(self): 
    30         return self.get_query_set()[:40] 
    31  
    32 class LatestCommentsFeed(LatestFreeCommentsFeed): 
    33     """Feed of latest comments on the current site.""" 
    34  
    35     comments_class = Comment 
    36  
    37     def get_query_set(self): 
    38         qs = super(LatestCommentsFeed, self).get_query_set() 
    39         qs = qs.filter(is_removed=False) 
    40         if settings.COMMENTS_BANNED_USERS_GROUP: 
     25        qs = comments.get_model().objects.filter( 
     26            site__pk = settings.SITE_ID, 
     27            is_public = True, 
     28            is_removed = False, 
     29        ) 
     30        if getattr(settings, 'COMMENTS_BANNED_USERS_GROUP', None): 
    4131            where = ['user_id NOT IN (SELECT user_id FROM auth_users_group WHERE group_id = %s)'] 
    4232            params = [settings.COMMENTS_BANNED_USERS_GROUP] 
    4333            qs = qs.extra(where=where, params=params) 
    44         return qs 
     34        return qs[:40] 
     35         
     36    def item_pubdate(self, item): 
     37        return item.submit_date 
  • django/trunk/django/contrib/comments/__init__.py

    r4265 r8557  
     1from django.conf import settings 
     2from django.core import urlresolvers 
     3from django.core.exceptions import ImproperlyConfigured 
     4 
     5# Attributes required in the top-level app for COMMENTS_APP 
     6REQUIRED_COMMENTS_APP_ATTRIBUTES = ["get_model", "get_form", "get_form_target"] 
     7 
     8def get_comment_app(): 
     9    """ 
     10    Get the comment app (i.e. "django.contrib.comments") as defined in the settings 
     11    """ 
     12    # Make sure the app's in INSTALLED_APPS 
     13    comments_app = getattr(settings, 'COMMENTS_APP', 'django.contrib.comments') 
     14    if comments_app not in settings.INSTALLED_APPS: 
     15        raise ImproperlyConfigured("The COMMENTS_APP (%r) "\ 
     16                                   "must be in INSTALLED_APPS" % settings.COMMENTS_APP) 
     17 
     18    # Try to import the package 
     19    try: 
     20        package = __import__(settings.COMMENTS_APP, '', '', ['']) 
     21    except ImportError: 
     22        raise ImproperlyConfigured("The COMMENTS_APP setting refers to "\ 
     23                                   "a non-existing package.") 
     24 
     25    # Make sure some specific attributes exist inside that package. 
     26    for attribute in REQUIRED_COMMENTS_APP_ATTRIBUTES: 
     27        if not hasattr(package, attribute): 
     28            raise ImproperlyConfigured("The COMMENTS_APP package %r does not "\ 
     29                                       "define the (required) %r function" % \ 
     30                                            (package, attribute)) 
     31 
     32    return package 
     33 
     34def get_model(): 
     35    from django.contrib.comments.models import Comment 
     36    return Comment 
     37 
     38def get_form(): 
     39    from django.contrib.comments.forms import CommentForm 
     40    return CommentForm 
     41 
     42def get_form_target(): 
     43    return urlresolvers.reverse("django.contrib.comments.views.comments.post_comment") 
     44 
     45def get_flag_url(comment): 
     46    """ 
     47    Get the URL for the "flag this comment" view. 
     48    """ 
     49    if settings.COMMENTS_APP != __name__ and hasattr(get_comment_app(), "get_flag_url"): 
     50        return get_comment_app().get_flag_url(comment) 
     51    else: 
     52        return urlresolvers.reverse("django.contrib.comments.views.moderation.flag", args=(comment.id,)) 
     53 
     54def get_delete_url(comment): 
     55    """ 
     56    Get the URL for the "delete this comment" view. 
     57    """ 
     58    if settings.COMMENTS_APP != __name__ and hasattr(get_comment_app(), "get_delete_url"): 
     59        return get_comment_app().get_flag_url(get_delete_url) 
     60    else: 
     61        return urlresolvers.reverse("django.contrib.comments.views.moderation.delete", args=(comment.id,)) 
     62 
     63def get_approve_url(comment): 
     64    """ 
     65    Get the URL for the "approve this comment from moderation" view. 
     66    """ 
     67    if settings.COMMENTS_APP != __name__ and hasattr(get_comment_app(), "get_approve_url"): 
     68        return get_comment_app().get_approve_url(comment) 
     69    else: 
     70        return urlresolvers.reverse("django.contrib.comments.views.moderation.approve", args=(comment.id,)) 
  • django/trunk/django/contrib/comments/models.py

    r8193 r8557  
    11import datetime 
    2  
    3 from django.db import models 
     2from django.contrib.auth.models import User 
     3from django.contrib.comments.managers import CommentManager 
     4from django.contrib.contenttypes import generic 
    45from django.contrib.contenttypes.models import ContentType 
    56from django.contrib.sites.models import Site 
    6 from django.contrib.auth.models import User 
     7from django.db import models 
     8from django.core import urlresolvers, validators 
    79from django.utils.translation import ugettext_lazy as _ 
    810from django.conf import settings 
    911 
    10 MIN_PHOTO_DIMENSION = 5 
    11 MAX_PHOTO_DIMENSION = 1000 
     12COMMENT_MAX_LENGTH = getattr(settings,'COMMENT_MAX_LENGTH',3000) 
    1213 
    13 # Option codes for comment-form hidden fields. 
    14 PHOTOS_REQUIRED = 'pr' 
    15 PHOTOS_OPTIONAL = 'pa' 
    16 RATINGS_REQUIRED = 'rr' 
    17 RATINGS_OPTIONAL = 'ra' 
    18 IS_PUBLIC = 'ip' 
     14class BaseCommentAbstractModel(models.Model): 
     15    """ 
     16    An abstract base class that any custom comment models probably should 
     17    subclass. 
     18    """ 
     19     
     20    # Content-object field 
     21    content_type   = models.ForeignKey(ContentType) 
     22    object_pk      = models.TextField(_('object ID')) 
     23    content_object = generic.GenericForeignKey(ct_field="content_type", fk_field="object_pk") 
    1924 
    20 # What users get if they don't have any karma. 
    21 DEFAULT_KARMA = 5 
    22 KARMA_NEEDED_BEFORE_DISPLAYED = 3 
     25    # Metadata about the comment 
     26    site        = models.ForeignKey(Site) 
    2327 
     28    class Meta: 
     29        abstract = True 
    2430 
    25 class CommentManager(models.Manager): 
    26     def get_security_hash(self, options, photo_options, rating_options, target): 
     31    def get_content_object_url(self): 
    2732        """ 
    28         Returns the MD5 hash of the given options (a comma-separated string such as 
    29         'pa,ra') and target (something like 'lcom.eventtimes:5157'). Used to 
    30         validate that submitted form options have not been tampered-with. 
     33        Get a URL suitable for redirecting to the content object. Uses the 
     34        ``django.views.defaults.shortcut`` view, which thus must be installed. 
    3135        """ 
    32         from django.utils.hashcompat import md5_constructor 
    33         return md5_constructor(options + photo_options + rating_options + target + settings.SECRET_KEY).hexdigest() 
     36        return urlresolvers.reverse( 
     37            "django.views.defaults.shortcut", 
     38            args=(self.content_type_id, self.object_pk) 
     39        ) 
    3440 
    35     def get_rating_options(self, rating_string): 
    36         """ 
    37         Given a rating_string, this returns a tuple of (rating_range, options). 
    38         >>> s = "scale:1-10|First_category|Second_category" 
    39         >>> Comment.objects.get_rating_options(s) 
    40         ([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], ['First category', 'Second category']) 
    41         """ 
    42         rating_range, options = rating_string.split('|', 1) 
    43         rating_range = range(int(rating_range[6:].split('-')[0]), int(rating_range[6:].split('-')[1])+1) 
    44         choices = [c.replace('_', ' ') for c in options.split('|')] 
    45         return rating_range, choices 
     41class Comment(BaseCommentAbstractModel): 
     42    """ 
     43    A user comment about some object. 
     44    """ 
    4645 
    47     def get_list_with_karma(self, **kwargs): 
    48         """ 
    49         Returns a list of Comment objects matching the given lookup terms, with 
    50         _karma_total_good and _karma_total_bad filled. 
    51         """ 
    52         extra_kwargs = {} 
    53         extra_kwargs.setdefault('select', {}) 
    54         extra_kwargs['select']['_karma_total_good'] = 'SELECT COUNT(*) FROM comments_karmascore, comments_comment WHERE comments_karmascore.comment_id=comments_comment.id AND score=1' 
    55         extra_kwargs['select']['_karma_total_bad'] = 'SELECT COUNT(*) FROM comments_karmascore, comments_comment WHERE comments_karmascore.comment_id=comments_comment.id AND score=-1' 
    56         return self.filter(**kwargs).extra(**extra_kwargs) 
     46    # Who posted this comment? If ``user`` is set then it was an authenticated 
     47    # user; otherwise at least person_name should have been set and the comment 
     48    # was posted by a non-authenticated user. 
     49    user        = models.ForeignKey(User, blank=True, null=True, related_name="%(class)s_comments") 
     50    user_name   = models.CharField(_("user's name"), max_length=50, blank=True) 
     51    user_email  = models.EmailField(_("user's email address"), blank=True) 
     52    user_url    = models.URLField(_("user's URL"), blank=True) 
    5753 
    58     def user_is_moderator(self, user): 
    59         if user.is_superuser: 
    60             return True 
    61         for g in user.groups.all(): 
    62             if g.id == settings.COMMENTS_MODERATORS_GROUP: 
    63                 return True 
    64         return False 
     54    comment = models.TextField(_('comment'), max_length=COMMENT_MAX_LENGTH) 
    6555 
     56    # Metadata about the comment 
     57    submit_date = models.DateTimeField(_('date/time submitted'), default=None) 
     58    ip_address  = models.IPAddressField(_('IP address'), blank=True, null=True) 
     59    is_public   = models.BooleanField(_('is public'), default=True, 
     60                    help_text=_('Uncheck this box to make the comment effectively ' \ 
     61                                'disappear from the site.')) 
     62    is_removed  = models.BooleanField(_('is removed'), default=False, 
     63                    help_text=_('Check this box if the comment is inappropriate. ' \ 
     64                                'A "This comment has been removed" message will ' \ 
     65                                'be displayed instead.')) 
    6666 
    67 class Comment(models.Model): 
    68     """A comment by a registered user.""" 
    69     user = models.ForeignKey(User) 
    70     content_type = models.ForeignKey(ContentType) 
    71     object_id = models.IntegerField(_('object ID')) 
    72     headline = models.CharField(_('headline'), max_length=255, blank=True) 
    73     comment = models.TextField(_('comment'), max_length=3000) 
    74     rating1 = models.PositiveSmallIntegerField(_('rating #1'), blank=True, null=True) 
    75     rating2 = models.PositiveSmallIntegerField(_('rating #2'), blank=True, null=True) 
    76     rating3 = models.PositiveSmallIntegerField(_('rating #3'), blank=True, null=True) 
    77     rating4 = models.PositiveSmallIntegerField(_('rating #4'), blank=True, null=True) 
    78     rating5 = models.PositiveSmallIntegerField(_('rating #5'), blank=True, null=True) 
    79     rating6 = models.PositiveSmallIntegerField(_('rating #6'), blank=True, null=True) 
    80     rating7 = models.PositiveSmallIntegerField(_('rating #7'), blank=True, null=True) 
    81     rating8 = models.PositiveSmallIntegerField(_('rating #8'), blank=True, null=True) 
    82     # This field designates whether to use this row's ratings in aggregate 
    83     # functions (summaries). We need this because people are allowed to post 
    84     # multiple reviews on the same thing, but the system will only use the 
    85     # latest one (with valid_rating=True) in tallying the reviews. 
    86     valid_rating = models.BooleanField(_('is valid rating')) 
    87     submit_date = models.DateTimeField(_('date/time submitted'), auto_now_add=True) 
    88     is_public = models.BooleanField(_('is public')) 
    89     ip_address = models.IPAddressField(_('IP address'), blank=True, null=True) 
    90     is_removed = models.BooleanField(_('is removed'), help_text=_('Check this box if the comment is inappropriate. A "This comment has been removed" message will be displayed instead.')) 
    91     site = models.ForeignKey(Site) 
     67    # Manager 
    9268    objects = CommentManager() 
    9369 
    9470    class Meta: 
    95         verbose_name = _('comment') 
    96         verbose_name_plural = _('comments'
    97         ordering = ('-submit_date',) 
     71        db_table = "django_comments" 
     72        ordering = ('submit_date',
     73        permissions = [("can_moderate", "Can moderate comments")] 
    9874 
    9975    def __unicode__(self): 
    100         return "%s: %s..." % (self.user.username, self.comment[:100]) 
     76        return "%s: %s..." % (self.name, self.comment[:50]) 
    10177 
    102     def get_absolute_url(self): 
    103         try: 
    104             return self.get_content_object().get_absolute_url() + "#c" + str(self.id) 
    105         except AttributeError: 
    106             return "" 
     78    def save(self): 
     79        if self.submit_date is None: 
     80            self.submit_date = datetime.datetime.now() 
     81        super(Comment, self).save() 
    10782 
    108     def get_crossdomain_url(self): 
    109         return "/r/%d/%d/" % (self.content_type_id, self.object_id) 
     83    def _get_userinfo(self): 
     84        """ 
     85        Get a dictionary that pulls together information about the poster 
     86        safely for both authenticated and non-authenticated comments. 
    11087 
    111     def get_flag_url(self): 
    112         return "/comments/flag/%s/" % self.id 
     88        This dict will have ``name``, ``email``, and ``url`` fields. 
     89        """ 
     90        if not hasattr(self, "_userinfo"): 
     91            self._userinfo = { 
     92                "name"  : self.user_name, 
     93                "email" : self.user_email, 
     94                "url"   : self.user_url 
     95            } 
     96            if self.user_id: 
     97                u = self.user 
     98                if u.email: 
     99                    self._userinfo["email"] = u.email 
    113100 
    114     def get_deletion_url(self): 
    115         return "/comments/delete/%s/" % self.id 
     101                # If the user has a full name, use that for the user name. 
     102                # However, a given user_name overrides the raw user.username, 
     103                # so only use that if this comment has no associated name. 
     104                if u.get_full_name(): 
     105                    self._userinfo["name"] = self.user.get_full_name() 
     106                elif not self.user_name: 
     107                    self._userinfo["name"] = u.username 
     108        return self._userinfo 
     109    userinfo = property(_get_userinfo, doc=_get_userinfo.__doc__) 
    116110 
    117     def get_content_object(self): 
    118         """ 
    119         Returns the object that this comment is a comment on. Returns None if 
    120         the object no longer exists. 
    121         """ 
    122         from django.core.exceptions import ObjectDoesNotExist 
    123         try: 
    124             return self.content_type.get_object_for_this_type(pk=self.object_id) 
    125         except ObjectDoesNotExist: 
    126             return None 
     111    def _get_name(self): 
     112        return self.userinfo["name"] 
     113    def _set_name(self, val): 
     114        if self.user_id: 
     115            raise AttributeError(_("This comment was posted by an authenticated "\ 
     116                                   "user and thus the name is read-only.")) 
     117        self.user_name = val 
     118    name = property(_get_name, _set_name, doc="The name of the user who posted this comment") 
    127119 
    128     get_content_object.short_description = _('Content object') 
     120    def _get_email(self): 
     121        return self.userinfo["email"] 
     122    def _set_email(self, val): 
     123        if self.user_id: 
     124            raise AttributeError(_("This comment was posted by an authenticated "\ 
     125                                   "user and thus the email is read-only.")) 
     126        self.user_email = val 
     127    email = property(_get_email, _set_email, doc="The email of the user who posted this comment") 
    129128 
    130     def _fill_karma_cache(self): 
    131         """Helper function that populates good/bad karma caches.""" 
    132         good, bad = 0, 0 
    133         for k in self.karmascore_set: 
    134             if k.score == -1: 
    135                 bad +=1 
    136             elif k.score == 1: 
    137                 good +=1 
    138         self._karma_total_good, self._karma_total_bad = good, bad 
     129    def _get_url(self): 
     130        return self.userinfo["url"] 
     131    def _set_url(self, val): 
     132        self.user_url = val 
     133    url = property(_get_url, _set_url, doc="The URL given by the user who posted this comment") 
    139134 
    140     def get_good_karma_total(self): 
    141         if not hasattr(self, "_karma_total_good"): 
    142             self._fill_karma_cache() 
    143         return self._karma_total_good 
    144  
    145     def get_bad_karma_total(self): 
    146         if not hasattr(self, "_karma_total_bad"): 
    147             self._fill_karma_cache() 
    148         return self._karma_total_bad 
    149  
    150     def get_karma_total(self): 
    151         if not hasattr(self, "_karma_total_good") or not hasattr(self, "_karma_total_bad"): 
    152             self._fill_karma_cache() 
    153         return self._karma_total_good + self._karma_total_bad 
     135    def get_absolute_url(self, anchor_pattern="#c%(id)s"): 
     136        return self.get_content_object_url() + (anchor_pattern % self.__dict__) 
    154137 
    155138    def get_as_text(self): 
    156         return _('Posted by %(user)s at %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s') % \ 
    157             {'user': self.user.username, 'date': self.submit_date, 
    158             'comment': self.comment, 'domain': self.site.domain, 'url': self.get_absolute_url()} 
     139        """ 
     140        Return this comment as plain text.  Useful for emails. 
     141        """ 
     142        d = { 
     143            'user': self.user, 
     144            'date': self.submit_date, 
     145            'comment': self.comment, 
     146            'domain': self.site.domain, 
     147            'url': self.get_absolute_url() 
     148        } 
     149        return _('Posted by %(user)s at %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s') % d 
    159150 
     151class CommentFlag(models.Model): 
     152    """ 
     153    Records a flag on a comment. This is intentionally flexible; right now, a 
     154    flag could be: 
    160155 
    161 class FreeComment(models.Model): 
    162     """A comment by a non-registered user.""" 
    163     content_type = models.ForeignKey(ContentType) 
    164     object_id = models.IntegerField(_('object ID')) 
    165     comment = models.TextField(_('comment'), max_length=3000) 
    166     person_name = models.CharField(_("person's name"), max_length=50) 
    167     submit_date = models.DateTimeField(_('date/time submitted'), auto_now_add=True) 
    168     is_public = models.BooleanField(_('is public')) 
    169     ip_address = models.IPAddressField(_('ip address')) 
    170     # TODO: Change this to is_removed, like Comment 
    171     approved = models.BooleanField(_('approved by staff')) 
    172     site = models.ForeignKey(Site) 
     156        * A "removal suggestion" -- where a user suggests a comment for (potential) removal. 
     157 
     158        * A "moderator deletion" -- used when a moderator deletes a comment. 
     159 
     160    You can (ab)use this model to add other flags, if needed. However, by 
     161    design users are only allowed to flag a comment with a given flag once; 
     162    if you want rating look elsewhere. 
     163    """ 
     164    user      = models.ForeignKey(User, related_name="comment_flags") 
     165    comment   = models.ForeignKey(Comment, related_name="flags") 
     166    flag      = models.CharField(max_length=30, db_index=True) 
     167    flag_date = models.DateTimeField(default=None) 
     168 
     169    # Constants for flag types 
     170    SUGGEST_REMOVAL = "removal suggestion" 
     171    MODERATOR_DELETION = "moderator deletion" 
     172    MODERATOR_APPROVAL = "moderator approval" 
    173173 
    174174    class Meta: 
    175         verbose_name = _('free comment') 
    176         verbose_name_plural = _('free comments') 
    177         ordering = ('-submit_date',) 
     175        db_table = 'django_comment_flags' 
     176        unique_together = [('user', 'comment', 'flag')] 
    178177 
    179178    def __unicode__(self): 
    180         return "%s: %s..." % (self.person_name, self.comment[:100]) 
     179        return "%s flag of comment ID %s by %s" % \ 
     180            (self.flag, self.comment_id, self.user.username) 
    181181 
    182     def get_absolute_url(self): 
    183         try: 
    184             return self.get_content_object().get_absolute_url() + "#c" + str(self.id) 
    185         except AttributeError: 
    186             return "" 
    187  
    188     def get_content_object(self): 
    189         """ 
    190         Returns the object that this comment is a comment on. Returns None if 
    191         the object no longer exists. 
    192         """ 
    193         from django.core.exceptions import ObjectDoesNotExist 
    194         try: 
    195             return self.content_type.get_object_for_this_type(pk=self.object_id) 
    196         except ObjectDoesNotExist: 
    197             return None 
    198  
    199     get_content_object.short_description = _('Content object') 
    200  
    201  
    202 class KarmaScoreManager(models.Manager): 
    203     def vote(self, user_id, comment_id, score): 
    204         try: 
    205             karma = self.get(comment__pk=comment_id, user__pk=user_id) 
    206         except self.model.DoesNotExist: 
    207             karma = self.model(None, user_id=user_id, comment_id=comment_id, score=score, scored_date=datetime.datetime.now()) 
    208             karma.save() 
    209         else: 
    210             karma.score = score 
    211             karma.scored_date = datetime.datetime.now() 
    212             karma.save() 
    213  
    214     def get_pretty_score(self, score): 
    215         """ 
    216         Given a score between -1 and 1 (inclusive), returns the same score on a 
    217         scale between 1 and 10 (inclusive), as an integer. 
    218         """ 
    219         if score is None: 
    220             return DEFAULT_KARMA 
    221         return int(round((4.5 * score) + 5.5)) 
    222  
    223  
    224 class KarmaScore(models.Model): 
    225     user = models.ForeignKey(User) 
    226     comment = models.ForeignKey(Comment) 
    227     score = models.SmallIntegerField(_('score'), db_index=True) 
    228     scored_date = models.DateTimeField(_('score date'), auto_now=True) 
    229     objects = KarmaScoreManager() 
    230  
    231     class Meta: 
    232         verbose_name = _('karma score') 
    233         verbose_name_plural = _('karma scores') 
    234         unique_together = (('user', 'comment'),) 
    235  
    236     def __unicode__(self): 
    237         return _("%(score)d rating by %(user)s") % {'score': self.score, 'user': self.user} 
    238  
    239  
    240 class UserFlagManager(models.Manager): 
    241     def flag(self, comment, user): 
    242         """ 
    243         Flags the given comment by the given user. If the comment has already 
    244         been flagged by the user, or it was a comment posted by the user, 
    245         nothing happens. 
    246         """ 
    247         if int(comment.user_id) == int(user.id): 
    248             return # A user can't flag his own comment. Fail silently. 
    249         try: 
    250             f = self.get(user__pk=user.id, comment__pk=comment.id) 
    251         except self.model.DoesNotExist: 
    252             from django.core.mail import mail_managers 
    253             f = self.model(None, user.id, comment.id, None) 
    254             message = _('This comment was flagged by %(user)s:\n\n%(text)s') % {'user': user.username, 'text': comment.get_as_text()} 
    255             mail_managers('Comment flagged', message, fail_silently=True) 
    256             f.save() 
    257  
    258  
    259 class UserFlag(models.Model): 
    260     user = models.ForeignKey(User) 
    261     comment = models.ForeignKey(Comment) 
    262     flag_date = models.DateTimeField(_('flag date'), auto_now_add=True) 
    263     objects = UserFlagManager() 
    264  
    265     class Meta: 
    266         verbose_name = _('user flag') 
    267         verbose_name_plural = _('user flags') 
    268         unique_together = (('user', 'comment'),) 
    269  
    270     def __unicode__(self): 
    271         return _("Flag by %r") % self.user 
    272  
    273  
    274 class ModeratorDeletion(models.Model): 
    275     user = models.ForeignKey(User, verbose_name='moderator') 
    276     comment = models.ForeignKey(Comment) 
    277     deletion_date = models.DateTimeField(_('deletion date'), auto_now_add=True) 
    278  
    279     class Meta: 
    280         verbose_name = _('moderator deletion') 
    281         verbose_name_plural = _('moderator deletions') 
    282         unique_together = (('user', 'comment'),) 
    283  
    284     def __unicode__(self): 
    285         return _("Moderator deletion by %r") % self.user 
    286          
     182    def save(self): 
     183        if self.flag_date is None: 
     184            self.flag_date = datetime.datetime.now() 
     185        super(CommentFlag, self).save() 
  • django/trunk/django/contrib/comments/templates/comments/form.html

    r7294 r8557  
    1 {% load i18n %} 
    2 {% if display_form %} 
    3 <form {% if photos_optional or photos_required %}enctype="multipart/form-data" {% endif %}action="/comments/post/" method="post"> 
    4  
    5 {% if user.is_authenticated %} 
    6 <p>{% trans "Username:" %} <strong>{{ user.username }}</strong> (<a href="{{ logout_url }}">{% trans "Log out" %}</a>)</p> 
    7 {% else %} 
    8 <p><label for="id_username">{% trans "Username:" %}</label> <input type="text" name="username" id="id_username" /><br />{% trans "Password:" %} <input type="password" name="password" id="id_password" /> (<a href="/accounts/password_reset/">{% trans "Forgotten your password?" %}</a>)</p> 
    9 {% endif %} 
    10  
    11 {% if ratings_optional or ratings_required %} 
    12 <p>{% trans "Ratings" %} ({% if ratings_required %}{% trans "Required" %}{% else %}{% trans "Optional" %}{% endif %}):</p> 
    13 <table> 
    14 <tr><th>&nbsp;</th>{% for value in rating_range %}<th>{{ value }}</th>{% endfor %}</tr> 
    15 {% for rating in rating_choices %} 
    16 <tr><th>{{ rating }}</th>{% for value in rating_range %}<th><input type="radio" name="rating{{ forloop.parentloop.counter }}" value="{{ value }}" /></th>{% endfor %}</tr> 
    17 {% endfor %} 
    18 </table> 
    19 <input type="hidden" name="rating_options" value="{{ rating_options }}" /> 
    20 {% endif %} 
    21  
    22 {% if photos_optional or photos_required %} 
    23 <p><label for="id_photo">{% trans "Post a photo" %}</label> ({% if photos_required %}{% trans "Required" %}{% else %}{% trans "Optional" %}{% endif %}): 
    24 <input type="file" name="photo" id="id_photo" /></p> 
    25 <input type="hidden" name="photo_options" value="{{ photo_options }}" /> 
    26 {% endif %} 
    27  
    28 <p><label for="id_comment">{% trans "Comment:" %}</label><br /> 
    29 <textarea name="comment" id="id_comment" rows="10" cols="60"></textarea></p> 
    30  
    31 <p> 
    32 <input type="hidden" name="options" value="{{ options }}" /> 
    33 <input type="hidden" name="target" value="{{ target }}" /> 
    34 <input type="hidden" name="gonzo" value="{{ hash }}" /> 
    35 <input type="submit" name="preview" value="{% trans "Preview comment" %}" /> 
    36 </p> 
     1{% load comments %} 
     2<form action="{% comment_form_target %}" method="POST"> 
     3  {% for field in form %} 
     4    {% if field.is_hidden %} 
     5      {{ field }} 
     6    {% else %} 
     7      <p 
     8        {% if field.errors %} class="error"{% endif %} 
     9        {% ifequal field.name "honeypot" %} style="display:none;"{% endifequal %}> 
     10        {% if field.errors %}{{ field.errors }}{% endif %} 
     11        {{ field.label_tag }} {{ field }} 
     12      </p> 
     13    {% endif %} 
     14  {% endfor %} 
     15  <p class="submit"> 
     16    <input type="submit" name="submit" class="submit-post" value="Post"> 
     17    <input type="submit" name="submit" class="submit-preview" value="Preview"> 
     18  </p> 
    3719</form> 
    38 {% endif %} 
  • django/trunk/django/contrib/comments/templatetags/comments.py

    r6399 r8557  
    1 from django.contrib.comments.models import Comment, FreeComment 
    2 from django.contrib.comments.models import PHOTOS_REQUIRED, PHOTOS_OPTIONAL, RATINGS_REQUIRED, RATINGS_OPTIONAL, IS_PUBLIC 
    3 from django.contrib.comments.models import MIN_PHOTO_DIMENSION, MAX_PHOTO_DIMENSION 
    41from django import template 
    5 from django.template import loader 
    6 from django.core.exceptions import ObjectDoesNotExist 
     2from django.template.loader import render_to_string 
     3from django.conf import settings 
    74from django.contrib.contenttypes.models import ContentType 
    8 from django.utils.encoding import smart_str 
    9 import re 
     5from django.contrib import comments 
    106 
    117register = template.Library() 
    128 
    13 COMMENT_FORM = 'comments/form.html' 
    14 FREE_COMMENT_FORM = 'comments/freeform.html' 
    15  
    16 class CommentFormNode(template.Node): 
    17     def __init__(self, content_type, obj_id_lookup_var, obj_id, free, 
    18         photos_optional=False, photos_required=False, photo_options='', 
    19         ratings_optional=False, ratings_required=False, rating_options='', 
    20         is_public=True): 
    21         self.content_type = content_type 
    22         if obj_id_lookup_var is not None: 
    23             obj_id_lookup_var = template.Variable(obj_id_lookup_var) 
    24         self.obj_id_lookup_var, self.obj_id, self.free = obj_id_lookup_var, obj_id, free 
    25         self.photos_optional, self.photos_required = photos_optional, photos_required 
    26         self.ratings_optional, self.ratings_required = ratings_optional, ratings_required 
    27         self.photo_options, self.rating_options = photo_options, rating_options 
    28         self.is_public = is_public 
     9class BaseCommentNode(template.Node): 
     10    """ 
     11    Base helper class (abstract) for handling the get_comment_* template tags. 
     12    Looks a bit strange, but the subclasses below should make this a bit more 
     13    obvious. 
     14    """ 
     15 
     16    #@classmethod 
     17    def handle_token(cls, parser, token): 
     18        """Class method to parse get_comment_list/count/form and return a Node.""" 
     19        tokens = token.contents.split() 
     20        if tokens[1] != 'for': 
     21            raise template.TemplateSyntaxError("Second argument in %r tag must be 'for'" % tokens[0]) 
     22 
     23        # {% get_whatever for obj as varname %} 
     24        if len(tokens) == 5: 
     25            if tokens[3] != 'as': 
     26                raise template.TemplateSyntaxError("Third argument in %r must be 'as'" % tokens[0]) 
     27            return cls( 
     28                object_expr = parser.compile_filter(tokens[2]), 
     29                as_varname = tokens[4], 
     30            ) 
     31 
     32        # {% get_whatever for app.model pk as varname %} 
     33        elif len(tokens) == 6: 
     34            if tokens[4] != 'as': 
     35                raise template.TemplateSyntaxError("Fourth argument in %r must be 'as'" % tokens[0]) 
     36            return cls( 
     37                ctype = BaseCommentNode.lookup_content_type(tokens[2], tokens[0]), 
     38                object_pk_expr = parser.compile_filter(tokens[3]), 
     39                as_varname = tokens[5] 
     40            ) 
     41 
     42        else: 
     43            raise template.TemplateSyntaxError("%r tag requires 4 or 5 arguments" % tokens[0]) 
     44 
     45    handle_token = classmethod(handle_token) 
     46 
     47    #@staticmethod 
     48    def lookup_content_type(token, tagname): 
     49        try: 
     50            app, model = token.split('.') 
     51            return ContentType.objects.get(app_label=app, model=model) 
     52        except ValueError: 
     53            raise template.TemplateSyntaxError("Third argument in %r must be in the format 'app.model'" % tagname) 
     54        except ContentType.DoesNotExist: 
     55            raise template.TemplateSyntaxError("%r tag has non-existant content-type: '%s.%s'" % (tagname, app, model)) 
     56    lookup_content_type = staticmethod(lookup_content_type) 
     57 
     58    def __init__(self, ctype=None, object_pk_expr=None, object_expr=None, as_varname=None, comment=None): 
     59        if ctype is None and object_expr is None: 
     60            raise template.TemplateSyntaxError("Comment nodes must be given either a literal object or a ctype and object pk.") 
     61        self.comment_model = comments.get_model() 
     62        self.as_varname = as_varname 
     63        self.ctype = ctype 
     64        self.object_pk_expr = object_pk_expr 
     65        self.object_expr = object_expr 
     66        self.comment = comment 
    2967 
    3068    def render(self, context): 
    31         from django.conf import settings 
    32         from django.utils.text import normalize_newlines 
    33         import base64 
    34         context.push() 
    35         if self.obj_id_lookup_var is not None: 
     69        qs = self.get_query_set(context) 
     70        context[self.as_varname] = self.get_context_value_from_queryset(context, qs) 
     71        return '' 
     72 
     73    def get_query_set(self, context): 
     74        ctype, object_pk = self.get_target_ctype_pk(context) 
     75        if not object_pk: 
     76            return self.comment_model.objects.none() 
     77 
     78        qs = self.comment_model.objects.filter( 
     79            content_type = ctype, 
     80            object_pk    = object_pk, 
     81            site__pk     = settings.SITE_ID, 
     82            is_public    = True, 
     83        ) 
     84        if settings.COMMENTS_HIDE_REMOVED: 
     85            qs = qs.filter(is_removed=False) 
     86 
     87        return qs 
     88 
     89    def get_target_ctype_pk(self, context): 
     90        if self.object_expr: 
    3691            try: 
    37                 self.obj_id = self.obj_id_lookup_var.resolve(context) 
     92                obj = self.object_expr.resolve(context) 
    3893            except template.VariableDoesNotExist: 
    39                 return '' 
    40             # Validate that this object ID is valid for this content-type. 
    41             # We only have to do this validation if obj_id_lookup_var is provided, 
    42             # because do_comment_form() validates hard-coded object IDs. 
    43             try: 
    44                 self.content_type.get_object_for_this_type(pk=self.obj_id) 
    45             except ObjectDoesNotExist: 
    46                 context['display_form'] = False 
    47             else: 
    48                 context['display_form'] = True 
    49         else: 
    50             context['display_form'] = True 
    51         context['target'] = '%s:%s' % (self.content_type.id, self.obj_id) 
    52         options = [] 
    53         for var, abbr in (('photos_required', PHOTOS_REQUIRED), 
    54                           ('photos_optional', PHOTOS_OPTIONAL), 
    55                           ('ratings_required', RATINGS_REQUIRED), 
    56                           ('ratings_optional', RATINGS_OPTIONAL), 
    57                           ('is_public', IS_PUBLIC)): 
    58             context[var] = getattr(self, var) 
    59             if getattr(self, var): 
    60                 options.append(abbr) 
    61         context['options'] = ','.join(options) 
    62         if self.free: 
    63             context['hash'] = Comment.objects.get_security_hash(context['options'], '', '', context['target']) 
    64             default_form = loader.get_template(FREE_COMMENT_FORM) 
    65         else: 
    66             context['photo_options'] = self.photo_options 
    67             context['rating_options'] = normalize_newlines(base64.encodestring(self.rating_options).strip()) 
    68             if self.rating_options: 
    69                 context['rating_range'], context['rating_choices'] = Comment.objects.get_rating_options(self.rating_options) 
    70             context['hash'] = Comment.objects.get_security_hash(context['options'], context['photo_options'], context['rating_options'], context['target']) 
    71             context['logout_url'] = settings.LOGOUT_URL 
    72             default_form = loader.get_template(COMMENT_FORM) 
    73         output = default_form.render(context) 
    74         context.pop() 
    75         return output 
    76  
    77 class CommentCountNode(template.Node): 
    78     def __init__(self, package, module, context_var_name, obj_id, var_name, free): 
    79         self.package, self.module = package, module 
    80         if context_var_name is not None: 
    81             context_var_name = template.Variable(context_var_name) 
    82         self.context_var_name, self.obj_id = context_var_name, obj_id 
    83         self.var_name, self.free = var_name, free 
     94                return None, None 
     95            return ContentType.objects.get_for_model(obj), obj.pk 
     96        else: 
     97            return self.ctype, self.object_pk_expr.resolve(context, ignore_failures=True) 
     98 
     99    def get_context_value_from_queryset(self, context, qs): 
     100        """Subclasses should override this.""" 
     101        raise NotImplementedError 
     102 
     103class CommentListNode(BaseCommentNode): 
     104    """Insert a list of comments into the context.""" 
     105    def get_context_value_from_queryset(self, context, qs): 
     106        return list(qs) 
     107 
     108class CommentCountNode(BaseCommentNode): 
     109    """Insert a count of comments into the context.""" 
     110    def get_context_value_from_queryset(self, context, qs): 
     111        return qs.count() 
     112 
     113class CommentFormNode(BaseCommentNode): 
     114    """Insert a form for the comment model into the context.""" 
     115 
     116    def get_form(self, context): 
     117        ctype, object_pk = self.get_target_ctype_pk(context) 
     118        if object_pk: 
     119            return comments.get_form()(ctype.get_object_for_this_type(pk=object_pk)) 
     120        else: 
     121            return None 
    84122 
    85123    def render(self, context): 
    86         from django.conf import settings 
    87         manager = self.free and FreeComment.objects or Comment.objects 
    88         if self.context_var_name is not None: 
    89             self.obj_id = self.context_var_name.resolve(context) 
    90         comment_count = manager.filter(object_id__exact=self.obj_id, 
    91             content_type__app_label__exact=self.package, 
    92             content_type__model__exact=self.module, site__id__exact=settings.SITE_ID).count() 
    93         context[self.var_name] = comment_count 
     124        context[self.as_varname] = self.get_form(context) 
    94125        return '' 
    95126 
    96 class CommentListNode(template.Node): 
    97     def __init__(self, package, module, context_var_name, obj_id, var_name, free, ordering, extra_kwargs=None): 
    98         self.package, self.module = package, module 
    99         if context_var_name is not None: 
    100             context_var_name = template.Variable(context_var_name) 
    101         self.context_var_name, self.obj_id = context_var_name, obj_id 
    102         self.var_name, self.free = var_name, free 
    103         self.ordering = ordering 
    104         self.extra_kwargs = extra_kwargs or {} 
     127class RenderCommentFormNode(CommentFormNode): 
     128    """Render the comment form directly""" 
     129 
     130    #@classmethod 
     131    def handle_token(cls, parser, token): 
     132        """Class method to parse render_comment_form and return a Node.""" 
     133        tokens = token.contents.split() 
     134        if tokens[1] != 'for': 
     135            raise template.TemplateSyntaxError("Second argument in %r tag must be 'for'" % tokens[0]) 
     136 
     137        # {% render_comment_form for obj %} 
     138        if len(tokens) == 3: 
     139            return cls(object_expr=parser.compile_filter(tokens[2])) 
     140 
     141        # {% render_comment_form for app.models pk %} 
     142        elif len(tokens) == 4: 
     143            return cls( 
     144                ctype = BaseCommentNode.lookup_content_type(tokens[2], tokens[0]), 
     145                object_pk_expr = parser.compile_filter(tokens[3]) 
     146            ) 
     147    handle_token = classmethod(handle_token) 
    105148 
    106149    def render(self, context): 
    107         from django.conf import settings 
    108         get_list_function = self.free and FreeComment.objects.filter or Comment.objects.get_list_with_karma 
    109         if self.context_var_name is not None: 
    110             try: 
    111                 self.obj_id = self.context_var_name.resolve(context) 
    112             except template.VariableDoesNotExist: 
    113                 return '' 
    114         kwargs = { 
    115             'object_id__exact': self.obj_id, 
    116             'content_type__app_label__exact': self.package, 
    117             'content_type__model__exact': self.module, 
    118             'site__id__exact': settings.SITE_ID, 
    119         } 
    120         kwargs.update(self.extra_kwargs) 
    121         comment_list = get_list_function(**kwargs).order_by(self.ordering + 'submit_date').select_related() 
    122         if not self.free and settings.COMMENTS_BANNED_USERS_GROUP: 
    123             comment_list = comment_list.extra(select={'is_hidden': 'user_id IN (SELECT user_id FROM auth_user_groups WHERE group_id = %s)' % settings.COMMENTS_BANNED_USERS_GROUP}) 
    124  
    125         if not self.free: 
    126             if 'user' in context and context['user'].is_authenticated(): 
    127                 user_id = context['user'].id 
    128                 context['user_can_moderate_comments'] = Comment.objects.user_is_moderator(context['user']) 
    129             else: 
    130                 user_id = None 
    131                 context['user_can_moderate_comments'] = False 
    132             # Only display comments by banned users to those users themselves. 
    133             if settings.COMMENTS_BANNED_USERS_GROUP: 
    134                 comment_list = [c for c in comment_list if not c.is_hidden or (user_id == c.user_id)] 
    135  
    136         context[self.var_name] = comment_list 
    137         return '' 
    138  
    139 class DoCommentForm: 
    140     """ 
    141     Displays a comment form for the given params. 
    142  
    143     Syntax:: 
    144  
    145         {% comment_form for [pkg].[py_module_name] [context_var_containing_obj_id] with [list of options] %} 
     150        ctype, object_pk = self.get_target_ctype_pk(context) 
     151        if object_pk: 
     152            template_search_list = [ 
     153                "comments/%s/%s/form.html" % (ctype.app_label, ctype.model), 
     154                "comments/%s/form.html" % ctype.app_label, 
     155                "comments/form.html" 
     156            ] 
     157            context.push() 
     158            formstr = render_to_string(template_search_list, {"form" : self.get_form(context)}, context) 
     159            context.pop() 
     160            return formstr 
     161        else: 
     162            return '' 
     163 
     164# We could just register each classmethod directly, but then we'd lose out on 
     165# the automagic docstrings-into-admin-docs tricks. So each node gets a cute 
     166# wrapper function that just exists to hold the docstring. 
     167 
     168#@register.tag 
     169def get_comment_count(parser, token): 
     170    """ 
     171    Gets the comment count for the given params and populates the template 
     172    context with a variable containing that value, whose name is defined by the 
     173    'as' clause. 
     174 
     175    Syntax:: 
     176 
     177        {% get_comment_count for [object] as [varname]  %} 
     178        {% get_comment_count for [app].[model] [object_id] as [varname]  %} 
    146179 
    147180    Example usage:: 
    148181 
    149         {% comment_form for lcom.eventtimes event.id with is_public yes photos_optional thumbs,200,400 ratings_optional scale:1-5|first_option|second_option %} 
    150  
    151     ``[context_var_containing_obj_id]`` can be a hard-coded integer or a variable containing the ID. 
    152     """ 
    153     def __init__(self, free): 
    154         self.free = free 
    155  
    156     def __call__(self, parser, token): 
    157         tokens = token.contents.split() 
    158         if len(tokens) < 4: 
    159             raise template.TemplateSyntaxError, "%r tag requires at least 3 arguments" % tokens[0] 
    160         if tokens[1] != 'for': 
    161             raise template.TemplateSyntaxError, "Second argument in %r tag must be 'for'" % tokens[0] 
    162         try: 
    163             package, module = tokens[2].split('.') 
    164         except ValueError: # unpack list of wrong size 
    165             raise template.TemplateSyntaxError, "Third argument in %r tag must be in the format 'package.module'" % tokens[0] 
    166         try: 
    167             content_type = ContentType.objects.get(app_label__exact=package, model__exact=module) 
    168         except ContentType.DoesNotExist: 
    169             raise template.TemplateSyntaxError, "%r tag has invalid content-type '%s.%s'" % (tokens[0], package, module) 
    170         obj_id_lookup_var, obj_id = None, None 
    171         if tokens[3].isdigit(): 
    172             obj_id = tokens[3] 
    173             try: # ensure the object ID is valid 
    174                 content_type.get_object_for_this_type(pk=obj_id) 
    175             except ObjectDoesNotExist: 
    176                 raise template.TemplateSyntaxError, "%r tag refers to %s object with ID %s, which doesn't exist" % (tokens[0], content_type.name, obj_id) 
    177         else: 
    178             obj_id_lookup_var = tokens[3] 
    179         kwargs = {} 
    180         if len(tokens) > 4: 
    181             if tokens[4] != 'with': 
    182                 raise template.TemplateSyntaxError, "Fourth argument in %r tag must be 'with'" % tokens[0] 
    183             for option, args in zip(tokens[5::2], tokens[6::2]): 
    184                 option = smart_str(option) 
    185                 if option in ('photos_optional', 'photos_required') and not self.free: 
    186                     # VALIDATION ############################################## 
    187                     option_list = args.split(',') 
    188                     if len(option_list) % 3 != 0: 
    189                         raise template.TemplateSyntaxError, "Incorrect number of comma-separated arguments to %r tag" % tokens[0] 
    190                     for opt in option_list[::3]: 
    191                         if not opt.isalnum(): 
    192                             raise template.TemplateSyntaxError, "Invalid photo directory name in %r tag: '%s'" % (tokens[0], opt) 
    193                     for opt in option_list[1::3] + option_list[2::3]: 
    194                         if not opt.isdigit() or not (MIN_PHOTO_DIMENSION <= int(opt) <= MAX_PHOTO_DIMENSION): 
    195                             raise template.TemplateSyntaxError, "Invalid photo dimension in %r tag: '%s'. Only values between %s and %s are allowed." % (tokens[0], opt, MIN_PHOTO_DIMENSION, MAX_PHOTO_DIMENSION) 
    196                     # VALIDATION ENDS ######################################### 
    197                     kwargs[option] = True 
    198                     kwargs['photo_options'] = args 
    199                 elif option in ('ratings_optional', 'ratings_required') and not self.free: 
    200                     # VALIDATION ############################################## 
    201                     if 2 < len(args.split('|')) > 9: 
    202                         raise template.TemplateSyntaxError, "Incorrect number of '%s' options in %r tag. Use between 2 and 8." % (option, tokens[0]) 
    203                     if re.match('^scale:\d+\-\d+\:$', args.split('|')[0]): 
    204                         raise template.TemplateSyntaxError, "Invalid 'scale' in %r tag's '%s' options" % (tokens[0], option) 
    205                     # VALIDATION ENDS ######################################### 
    206                     kwargs[option] = True 
    207                     kwargs['rating_options'] = args 
    208                 elif option in ('is_public'): 
    209                     kwargs[option] = (args == 'true') 
    210                 else: 
    211                     raise template.TemplateSyntaxError, "%r tag got invalid parameter '%s'" % (tokens[0], option) 
    212         return CommentFormNode(content_type, obj_id_lookup_var, obj_id, self.free, **kwargs) 
    213  
    214 class DoCommentCount: 
    215     """ 
    216     Gets comment count for the given params and populates the template context 
    217     with a variable containing that value, whose name is defined by the 'as' 
    218     clause. 
    219  
    220     Syntax:: 
    221  
    222         {% get_comment_count for [pkg].[py_module_name] [context_var_containing_obj_id] as [varname]  %} 
     182        {% get_comment_count for event as comment_count %} 
     183        {% get_comment_count for calendar.event event.id as comment_count %} 
     184        {% get_comment_count for calendar.event 17 as comment_count %} 
     185 
     186    """ 
     187    return CommentCountNode.handle_token(parser, token) 
     188 
     189#@register.tag 
     190def get_comment_list(parser, token): 
     191    """ 
     192    Gets the list of comments for the given params and populates the template 
     193    context with a variable containing that value, whose name is defined by the 
     194    'as' clause. 
     195 
     196    Syntax:: 
     197 
     198        {% get_comment_list for [object] as [varname]  %} 
     199        {% get_comment_list for [app].[model] [object_id] as [varname]  %} 
    223200 
    224201    Example usage:: 
    225202 
    226         {% get_comment_count for lcom.eventtimes event.id as comment_count %} 
    227  
    228     Note: ``[context_var_containing_obj_id]`` can also be a hard-coded integer, like this:: 
    229  
    230         {% get_comment_count for lcom.eventtimes 23 as comment_count %} 
    231     """ 
    232     def __init__(self, free): 
    233         self.free = free 
    234  
    235     def __call__(self, parser, token): 
    236         tokens = token.contents.split() 
    237         # Now tokens is a list like this: 
    238         # ['get_comment_list', 'for', 'lcom.eventtimes', 'event.id', 'as', 'comment_list'] 
    239         if len(tokens) != 6: 
    240             raise template.TemplateSyntaxError, "%r tag requires 5 arguments" % tokens[0] 
    241         if tokens[1] != 'for': 
    242             raise template.TemplateSyntaxError, "Second argument in %r tag must be 'for'" % tokens[0] 
    243         try: 
    244             package, module = tokens[2].split('.') 
    245         except ValueError: # unpack list of wrong size 
    246             raise template.TemplateSyntaxError, "Third argument in %r tag must be in the format 'package.module'" % tokens[0] 
    247         try: 
    248             content_type = ContentType.objects.get(app_label__exact=package, model__exact=module) 
    249         except ContentType.DoesNotExist: 
    250             raise template.TemplateSyntaxError, "%r tag has invalid content-type '%s.%s'" % (tokens[0], package, module) 
    251         var_name, obj_id = None, None 
    252         if tokens[3].isdigit(): 
    253             obj_id = tokens[3] 
    254             try: # ensure the object ID is valid 
    255                 content_type.get_object_for_this_type(pk=obj_id) 
    256             except ObjectDoesNotExist: 
    257                 raise template.TemplateSyntaxError, "%r tag refers to %s object with ID %s, which doesn't exist" % (tokens[0], content_type.name, obj_id) 
    258         else: 
    259             var_name = tokens[3] 
    260         if tokens[4] != 'as': 
    261             raise template.TemplateSyntaxError, "Fourth argument in %r must be 'as'" % tokens[0] 
    262         return CommentCountNode(package, module, var_name, obj_id, tokens[5], self.free) 
    263  
    264 class DoGetCommentList: 
    265     """ 
    266     Gets comments for the given params and populates the template context with a 
    267     special comment_package variable, whose name is defined by the ``as`` 
    268     clause. 
    269  
    270     Syntax:: 
    271  
    272         {% get_comment_list for [pkg].[py_module_name] [context_var_containing_obj_id] as [varname] (reversed) %} 
    273  
    274     Example usage:: 
    275  
    276         {% get_comment_list for lcom.eventtimes event.id as comment_list %} 
    277  
    278     Note: ``[context_var_containing_obj_id]`` can also be a hard-coded integer, like this:: 
    279  
    280         {% get_comment_list for lcom.eventtimes 23 as comment_list %} 
    281  
    282     To get a list of comments in reverse order -- that is, most recent first -- 
    283     pass ``reversed`` as the last param:: 
    284  
    285         {% get_comment_list for lcom.eventtimes event.id as comment_list reversed %} 
    286     """ 
    287     def __init__(self, free): 
    288         self.free = free 
    289  
    290     def __call__(self, parser, token): 
    291         tokens = token.contents.split() 
    292         # Now tokens is a list like this: 
    293         # ['get_comment_list', 'for', 'lcom.eventtimes', 'event.id', 'as', 'comment_list'] 
    294         if not len(tokens) in (6, 7): 
    295             raise template.TemplateSyntaxError, "%r tag requires 5 or 6 arguments" % tokens[0] 
    296         if tokens[1] != 'for': 
    297             raise template.TemplateSyntaxError, "Second argument in %r tag must be 'for'" % tokens[0] 
    298         try: 
    299             package, module = tokens[2].split('.') 
    300         except ValueError: # unpack list of wrong size 
    301             raise template.TemplateSyntaxError, "Third argument in %r tag must be in the format 'package.module'" % tokens[0] 
    302         try: 
    303             content_type = ContentType.objects.get(app_label__exact=package,model__exact=module) 
    304         except ContentType.DoesNotExist: 
    305             raise template.TemplateSyntaxError, "%r tag has invalid content-type '%s.%s'" % (tokens[0], package, module) 
    306         var_name, obj_id = None, None 
    307         if tokens[3].isdigit(): 
    308             obj_id = tokens[3] 
    309             try: # ensure the object ID is valid 
    310                 content_type.get_object_for_this_type(pk=obj_id) 
    311             except ObjectDoesNotExist: 
    312                 raise template.TemplateSyntaxError, "%r tag refers to %s object with ID %s, which doesn't exist" % (tokens[0], content_type.name, obj_id) 
    313         else: 
    314             var_name = tokens[3] 
    315         if tokens[4] != 'as': 
    316             raise template.TemplateSyntaxError, "Fourth argument in %r must be 'as'" % tokens[0] 
    317         if len(tokens) == 7: 
    318             if tokens[6] != 'reversed': 
    319                 raise template.TemplateSyntaxError, "Final argument in %r must be 'reversed' if given" % tokens[0] 
    320             ordering = "-" 
    321         else: 
    322             ordering = "" 
    323         return CommentListNode(package, module, var_name, obj_id, tokens[5], self.free, ordering) 
    324  
    325 # registration comments 
    326 register.tag('get_comment_list', DoGetCommentList(False)) 
    327 register.tag('comment_form', DoCommentForm(False)) 
    328 register.tag('get_comment_count', DoCommentCount(False)) 
    329 # free comments 
    330 register.tag('get_free_comment_list', DoGetCommentList(True)) 
    331 register.tag('free_comment_form', DoCommentForm(True)) 
    332 register.tag('get_free_comment_count', DoCommentCount(True)) 
     203        {% get_comment_list for event as comment_list %} 
     204        {% for comment in comment_list %} 
     205            ... 
     206        {% endfor %} 
     207 
     208    """ 
     209    return CommentListNode.handle_token(parser, token) 
     210 
     211#@register.tag 
     212def get_comment_form(parser, token): 
     213    """ 
     214    Get a (new) form object to post a new comment. 
     215 
     216    Syntax:: 
     217 
     218        {% get_comment_form for [object] as [varname] %} 
     219        {% get_comment_form for [app].[model] [object_id] as [varname] %} 
     220    """ 
     221    return CommentFormNode.handle_token(parser, token) 
     222 
     223#@register.tag 
     224def render_comment_form(parser, token): 
     225    """ 
     226    Render the comment form (as returned by ``{% render_comment_form %}``) through 
     227    the ``comments/form.html`` template. 
     228 
     229    Syntax:: 
     230 
     231        {% render_comment_form for [object] %} 
     232        {% render_comment_form for [app].[model] [object_id] %} 
     233    """ 
     234    return RenderCommentFormNode.handle_token(parser, token) 
     235 
     236#@register.simple_tag 
     237def comment_form_target(): 
     238    """ 
     239    Get the target URL for the comment form. 
     240 
     241    Example:: 
     242 
     243        <form action="{% comment_form_target %}" method="POST"> 
     244    """ 
     245    return comments.get_form_target() 
     246 
     247register.tag(get_comment_count) 
     248register.tag(get_comment_list) 
     249register.tag(get_comment_form) 
     250register.tag(render_comment_form) 
     251register.simple_tag(comment_form_target) 
  • django/trunk/django/contrib/comments/views/comments.py

    r7967 r8557  
    1 import base64 
    2 import datetime 
    3  
    4 from django.core import validators 
    5 from django import oldforms 
    6 from django.core.mail import mail_admins, mail_managers 
    7 from django.http import Http404 
     1from django import http 
     2from django.conf import settings 
     3from utils import next_redirect, confirmation_view 
    84from django.core.exceptions import ObjectDoesNotExist 
     5from django.db import models 
    96from django.shortcuts import render_to_response 
    107from django.template import RequestContext 
    11 from django.contrib.comments.models import Comment, FreeComment, RATINGS_REQUIRED, RATINGS_OPTIONAL, IS_PUBLIC 
    12 from django.contrib.contenttypes.models import ContentType 
    13 from django.contrib.auth import authenticate 
    14 from django.http import HttpResponseRedirect 
    15 from django.utils.text import normalize_newlines 
    16 from django.conf import settings 
    17 from django.utils.translation import ungettext, ugettext as _ 
    18 from django.utils.encoding import smart_unicode 
     8from django.template.loader import render_to_string 
     9from django.utils.html import escape 
     10from django.contrib import comments 
     11from django.contrib.comments import signals 
    1912 
    20 COMMENTS_PER_PAGE = 20 
     13class CommentPostBadRequest(http.HttpResponseBadRequest): 
     14    """ 
     15    Response returned when a comment post is invalid. If ``DEBUG`` is on a 
     16    nice-ish error message will be displayed (for debugging purposes), but in 
     17    production mode a simple opaque 400 page will be displayed. 
     18    """ 
     19    def __init__(self, why): 
     20        super(CommentPostBadRequest, self).__init__() 
     21        if settings.DEBUG: 
     22            self.content = render_to_string("comments/400-debug.html", {"why": why}) 
    2123 
    22 # TODO: This is a copy of the manipulator-based form that used to live in 
    23 # contrib.auth.forms.  It should be replaced with the newforms version that 
    24 # has now been added to contrib.auth.forms when the comments app gets updated 
    25 # for newforms. 
     24def post_comment(request, next=None): 
     25    """ 
     26    Post a comment. 
    2627 
    27 class AuthenticationForm(oldforms.Manipulator): 
     28    HTTP POST is required. If ``POST['submit'] == "preview"`` or if there are 
     29    errors a preview template, ``comments/preview.html``, will be rendered. 
    2830    """ 
    29     Base class for authenticating users. Extend this to get a form that accepts 
    30     username/password logins. 
    31     """ 
    32     def __init__(self, request=None): 
    33         """ 
    34         If request is passed in, the manipulator will validate that cookies are 
    35         enabled. Note that the request (a HttpRequest object) must have set a 
    36         cookie with the key TEST_COOKIE_NAME and value TEST_COOKIE_VALUE before 
    37         running this validator. 
    38         """ 
    39         self.request = request 
    40         self.fields = [ 
    41             oldforms.TextField(field_name="username", length=15, max_length=30, is_required=True, 
    42                 validator_list=[self.isValidUser, self.hasCookiesEnabled]), 
    43             oldforms.PasswordField(field_name="password", length=15, max_length=30, is_required=True), 
     31 
     32    # Require POST 
     33    if request.method != 'POST': 
     34        return http.HttpResponseNotAllowed(["POST"]) 
     35 
     36    # Fill out some initial data fields from an authenticated user, if present 
     37    data = request.POST.copy() 
     38    if request.user.is_authenticated(): 
     39        if "name" not in data: 
     40            data["name"] = request.user.get_full_name() 
     41        if "email" not in data: 
     42            data["email"] = request.user.email 
     43 
     44    # Look up the object we're trying to comment about 
     45    ctype = data.get("content_type") 
     46    object_pk = data.get("object_pk") 
     47    if ctype is None or object_pk is None: 
     48        return CommentPostBadRequest("Missing content_type or object_pk field.") 
     49    try: 
     50        model = models.get_model(*ctype.split(".", 1)) 
     51        target = model._default_manager.get(pk=object_pk) 
     52    except TypeError: 
     53        return CommentPostBadRequest( 
     54            "Invalid content_type value: %r" % escape(ctype)) 
     55    except AttributeError: 
     56        return CommentPostBadRequest( 
     57            "The given content-type %r does not resolve to a valid model." % \ 
     58                escape(ctype)) 
     59    except ObjectDoesNotExist: 
     60        return CommentPostBadRequest( 
     61            "No object matching content-type %r and object PK %r exists." % \ 
     62                (escape(ctype), escape(object_pk))) 
     63 
     64    # Do we want to preview the comment? 
     65    preview = data.get("submit", "").lower() == "preview" or \ 
     66              data.get("preview", None) is not None 
     67 
     68    # Construct the comment form 
     69    form = comments.get_form()(target, data=data) 
     70 
     71    # Check security information 
     72    if form.security_errors(): 
     73        return CommentPostBadRequest( 
     74            "The comment form failed security verification: %s" % \ 
     75                escape(str(form.security_errors()))) 
     76 
     77    # If there are errors or if we requested a preview show the comment 
     78    if form.errors or preview: 
     79        template_list = [ 
     80            "comments/%s_%s_preview.html" % tuple(str(model._meta).split(".")), 
     81            "comments/%s_preview.html" % model._meta.app_label, 
     82            "comments/preview.html", 
    4483        ] 
    45         self.user_cache = None 
    46  
    47     def hasCookiesEnabled(self, field_data, all_data): 
    48         if self.request and not self.request.session.test_cookie_worked(): 
    49             raise validators.ValidationError, _("Your Web browser doesn't appear to have cookies enabled. Cookies are required for logging in.") 
    50  
    51     def isValidUser(self, field_data, all_data): 
    52         username = field_data 
    53         password = all_data.get('password', None) 
    54         self.user_cache = authenticate(username=username, password=password) 
    55         if self.user_cache is None: 
    56             raise validators.ValidationError, _("Please enter a correct username and password. Note that both fields are case-sensitive.") 
    57         elif not self.user_cache.is_active: 
    58             raise validators.ValidationError, _("This account is inactive.") 
    59  
    60     def get_user_id(self): 
    61         if self.user_cache: 
    62             return self.user_cache.id 
    63         return None 
    64  
    65     def get_user(self): 
    66         return self.user_cache 
    67  
    68 class PublicCommentManipulator(AuthenticationForm): 
    69     "Manipulator that handles public registered comments" 
    70     def __init__(self, user, ratings_required, ratings_range, num_rating_choices): 
    71         AuthenticationForm.__init__(self) 
    72         self.ratings_range, self.num_rating_choices = ratings_range, num_rating_choices 
    73         choices = [(c, c) for c in ratings_range] 
    74         def get_validator_list(rating_num): 
    75             if rating_num <= num_rating_choices: 
    76                 return [validators.RequiredIfOtherFieldsGiven(['rating%d' % i for i in range(1, 9) if i != rating_num], _("This rating is required because you've entered at least one other rating."))] 
    77             else: 
    78                 return [] 
    79         self.fields.extend([ 
    80             oldforms.LargeTextField(field_name="comment", max_length=3000, is_required=True, 
    81                 validator_list=[self.hasNoProfanities]), 
    82             oldforms.RadioSelectField(field_name="rating1", choices=choices, 
    83                 is_required=ratings_required and num_rating_choices > 0, 
    84                 validator_list=get_validator_list(1), 
    85             ), 
    86             oldforms.RadioSelectField(field_name="rating2", choices=choices, 
    87                 is_required=ratings_required and num_rating_choices > 1, 
    88                 validator_list=get_validator_list(2), 
    89             ), 
    90             oldforms.RadioSelectField(field_name="rating3", choices=choices, 
    91                 is_required=ratings_required and num_rating_choices > 2, 
    92                 validator_list=get_validator_list(3), 
    93             ), 
    94             oldforms.RadioSelectField(field_name="rating4", choices=choices, 
    95                 is_required=ratings_required and num_rating_choices > 3, 
    96                 validator_list=get_validator_list(4), 
    97             ), 
    98             oldforms.RadioSelectField(field_name="rating5", choices=choices, 
    99                 is_required=ratings_required and num_rating_choices > 4, 
    100                 validator_list=get_validator_list(5), 
    101             ), 
    102             oldforms.RadioSelectField(field_name="rating6", choices=choices, 
    103                 is_required=ratings_required and num_rating_choices > 5, 
    104                 validator_list=get_validator_list(6), 
    105             ), 
    106             oldforms.RadioSelectField(field_name="rating7", choices=choices, 
    107                 is_required=ratings_required and num_rating_choices > 6, 
    108                 validator_list=get_validator_list(7), 
    109             ), 
    110             oldforms.RadioSelectField(field_name="rating8", choices=choices, 
    111                 is_required=ratings_required and num_rating_choices > 7, 
    112                 validator_list=get_validator_list(8), 
    113             ), 
    114         ]) 
    115         if user.is_authenticated(): 
    116             self["username"].is_required = False 
    117             self["username"].validator_list = [] 
    118             self["password"].is_required = False 
    119             self["password"].validator_list = [] 
    120             self.user_cache = user 
    121  
    122     def hasNoProfanities(self, field_data, all_data): 
    123         if settings.COMMENTS_ALLOW_PROFANITIES: 
    124             return 
    125         return validators.hasNoProfanities(field_data, all_data) 
    126  
    127     def get_comment(self, new_data): 
    128         "Helper function" 
    129         return Comment(None, self.get_user_id(), new_data["content_type_id"], 
    130             new_data["object_id"], new_data.get("headline", "").strip(), 
    131             new_data["comment"].strip(), new_data.get("rating1", None), 
    132             new_data.get("rating2", None), new_data.get("rating3", None), 
    133             new_data.get("rating4", None), new_data.get("rating5", None), 
    134             new_data.get("rating6", None), new_data.get("rating7", None), 
    135             new_data.get("rating8", None), new_data.get("rating1", None) is not None, 
    136             datetime.datetime.now(), new_data["is_public"], new_data["ip_address"], False, settings.SITE_ID) 
    137  
    138     def save(self, new_data): 
    139         today = datetime.date.today() 
    140         c = self.get_comment(new_data) 
    141         for old in Comment.objects.filter(content_type__id__exact=new_data["content_type_id"], 
    142             object_id__exact=new_data["object_id"], user__id__exact=self.get_user_id()): 
    143             # Check that this comment isn't duplicate. (Sometimes people post 
    144             # comments twice by mistake.) If it is, fail silently by pretending 
    145             # the comment was posted successfully. 
    146             if old.submit_date.date() == today and old.comment == c.comment \ 
    147                 and old.rating1 == c.rating1 and old.rating2 == c.rating2 \ 
    148                 and old.rating3 == c.rating3 and old.rating4 == c.rating4 \ 
    149                 and old.rating5 == c.rating5 and old.rating6 == c.rating6 \ 
    150                 and old.rating7 == c.rating7 and old.rating8 == c.rating8: 
    151                 return old 
    152             # If the user is leaving a rating, invalidate all old ratings. 
    153             if c.rating1 is not None: 
    154                 old.valid_rating = False 
    155                 old.save() 
    156         c.save() 
    157         # If the commentor has posted fewer than COMMENTS_FIRST_FEW comments, 
    158         # send the comment to the managers. 
    159         if self.user_cache.comment_set.count() <= settings.COMMENTS_FIRST_FEW: 
    160             message = ungettext('This comment was posted by a user who has posted fewer than %(count)s comment:\n\n%(text)s', 
    161                 'This comment was posted by a user who has posted fewer than %(count)s comments:\n\n%(text)s', settings.COMMENTS_FIRST_FEW) % \ 
    162                 {'count': settings.COMMENTS_FIRST_FEW, 'text': c.get_as_text()} 
    163             mail_managers("Comment posted by rookie user", message) 
    164         if settings.COMMENTS_SKETCHY_USERS_GROUP and settings.COMMENTS_SKETCHY_USERS_GROUP in [g.id for g in self.user_cache.groups.all()]: 
    165             message = _('This comment was posted by a sketchy user:\n\n%(text)s') % {'text': c.get_as_text()} 
    166             mail_managers("Comment posted by sketchy user (%s)" % self.user_cache.username, c.get_as_text()) 
    167         return c 
    168  
    169 class PublicFreeCommentManipulator(oldforms.Manipulator): 
    170     "Manipulator that handles public free (unregistered) comments" 
    171     def __init__(self): 
    172         self.fields = ( 
    173             oldforms.TextField(field_name="person_name", max_length=50, is_required=True, 
    174                 validator_list=[self.hasNoProfanities]), 
    175             oldforms.LargeTextField(field_name="comment", max_length=3000, is_required=True, 
    176                 validator_list=[self.hasNoProfanities]), 
     84        return render_to_response( 
     85            template_list, { 
     86                "comment" : form.data.get("comment", ""), 
     87                "form" : form, 
     88            },  
     89            RequestContext(request, {}) 
    17790        ) 
    17891 
    179     def hasNoProfanities(self, field_data, all_data): 
    180         if settings.COMMENTS_ALLOW_PROFANITIES: 
    181             return 
    182         return validators.hasNoProfanities(field_data, all_data) 
     92    # Otherwise create the comment 
     93    comment = form.get_comment_object() 
     94    comment.ip_address = request.META.get("REMOTE_ADDR", None) 
     95    if request.user.is_authenticated(): 
     96        comment.user = request.user 
    18397 
    184     def get_comment(self, new_data): 
    185         "Helper function" 
    186         return FreeComment(None, new_data["content_type_id"], 
    187             new_data["object_id"], new_data["comment"].strip(), 
    188             new_data["person_name"].strip(), datetime.datetime.now(), new_data["is_public"], 
    189             new_data["ip_address"], False, settings.SITE_ID) 
     98    # Signal that the comment is about to be saved 
     99    responses = signals.comment_will_be_posted.send(comment) 
    190100 
    191     def save(self, new_data): 
    192         today = datetime.date.today() 
    193         c = self.get_comment(new_data) 
    194         # Check that this comment isn't duplicate. (Sometimes people post 
    195         # comments twice by mistake.) If it is, fail silently by pretending 
    196         # the comment was posted successfully. 
    197         for old_comment in FreeComment.objects.filter(content_type__id__exact=new_data["content_type_id"], 
    198             object_id__exact=new_data["object_id"], person_name__exact=new_data["person_name"], 
    199             submit_date__year=today.year, submit_date__month=today.month, 
    200             submit_date__day=today.day): 
    201             if old_comment.comment == c.comment: 
    202                 return old_comment 
    203         c.save() 
    204         return c 
     101    for (receiver, response) in responses: 
     102        if response == False: 
     103            return CommentPostBadRequest( 
     104                "comment_will_be_posted receiver %r killed the comment" % receiver.__name__) 
    205105 
    206 def post_comment(request, extra_context=None, context_processors=None): 
    207     """ 
    208     Post a comment 
     106    # Save the comment and signal that it was saved 
     107    comment.save() 
     108    signals.comment_was_posted.send(comment) 
    209109 
    210     Redirects to the `comments.comments.comment_was_posted` view upon success. 
     110    return next_redirect(data, next, comment_done, c=comment._get_pk_val()) 
    211111 
    212     Templates: `comment_preview` 
    213     Context: 
    214         comment 
    215             the comment being posted 
    216         comment_form 
    217             the comment form 
    218         options 
    219             comment options 
    220         target 
    221             comment target 
    222         hash 
    223             security hash (must be included in a posted form to succesfully 
    224             post a comment). 
    225         rating_options 
    226             comment ratings options 
    227         ratings_optional 
    228             are ratings optional? 
    229         ratings_required 
    230             are ratings required? 
    231         rating_range 
    232             range of ratings 
    233         rating_choices 
    234             choice of ratings 
    235     """ 
    236     if extra_context is None: extra_context = {} 
    237     if not request.POST: 
    238         raise Http404, _("Only POSTs are allowed") 
    239     try: 
    240         options, target, security_hash = request.POST['options'], request.POST['target'], request.POST['gonzo'] 
    241     except KeyError: 
    242         raise Http404, _("One or more of the required fields wasn't submitted") 
    243     photo_options = request.POST.get('photo_options', '') 
    244     rating_options = normalize_newlines(request.POST.get('rating_options', '')) 
    245     if Comment.objects.get_security_hash(options, photo_options, rating_options, target) != security_hash: 
    246         raise Http404, _("Somebody tampered with the comment form (security violation)") 
    247     # Now we can be assured the data is valid. 
    248     if rating_options: 
    249         rating_range, rating_choices = Comment.objects.get_rating_options(base64.decodestring(rating_options)) 
    250     else: 
    251         rating_range, rating_choices = [], [] 
    252     content_type_id, object_id = target.split(':') # target is something like '52:5157' 
    253     try: 
    254         obj = ContentType.objects.get(pk=content_type_id).get_object_for_this_type(pk=object_id) 
    255     except ObjectDoesNotExist: 
    256         raise Http404, _("The comment form had an invalid 'target' parameter -- the object ID was invalid") 
    257     option_list = options.split(',') # options is something like 'pa,ra' 
    258     new_data = request.POST.copy() 
    259     new_data['content_type_id'] = content_type_id 
    260     new_data['object_id'] = object_id 
    261     new_data['ip_address'] = request.META.get('REMOTE_ADDR') 
    262     new_data['is_public'] = IS_PUBLIC in option_list 
    263     manipulator = PublicCommentManipulator(request.user, 
    264         ratings_required=RATINGS_REQUIRED in option_list, 
    265         ratings_range=rating_range, 
    266         num_rating_choices=len(rating_choices)) 
    267     errors = manipulator.get_validation_errors(new_data) 
    268     # If user gave correct username/password and wasn't already logged in, log them in 
    269     # so they don't have to enter a username/password again. 
    270     if manipulator.get_user() and not manipulator.get_user().is_authenticated() and 'password' in new_data and manipulator.get_user().check_password(new_data['password']): 
    271         from django.contrib.auth import login 
    272         login(request, manipulator.get_user()) 
    273     if errors or 'preview' in request.POST: 
    274         class CommentFormWrapper(oldforms.FormWrapper): 
    275             def __init__(self, manipulator, new_data, errors, rating_choices): 
    276                 oldforms.FormWrapper.__init__(self, manipulator, new_data, errors) 
    277                 self.rating_choices = rating_choices 
    278             def ratings(self): 
    279                 field_list = [self['rating%d' % (i+1)] for i in range(len(rating_choices))] 
    280                 for i, f in enumerate(field_list): 
    281                     f.choice = rating_choices[i] 
    282                 return field_list 
    283         comment = errors and '' or manipulator.get_comment(new_data) 
    284         comment_form = CommentFormWrapper(manipulator, new_data, errors, rating_choices) 
    285         return render_to_response('comments/preview.html', { 
    286             'comment': comment, 
    287             'comment_form': comment_form, 
    288             'options': options, 
    289             'target': target, 
    290             'hash': security_hash, 
    291             'rating_options': rating_options, 
    292             'ratings_optional': RATINGS_OPTIONAL in option_list, 
    293             'ratings_required': RATINGS_REQUIRED in option_list, 
    294             'rating_range': rating_range, 
    295             'rating_choices': rating_choices, 
    296         }, context_instance=RequestContext(request, extra_context, context_processors)) 
    297     elif 'post' in request.POST: 
    298         # If the IP is banned, mail the admins, do NOT save the comment, and 
    299         # serve up the "Thanks for posting" page as if the comment WAS posted. 
    300         if request.META['REMOTE_ADDR'] in settings.BANNED_IPS: 
    301             mail_admins("Banned IP attempted to post comment", smart_unicode(request.POST) + "\n\n" + str(request.META)) 
    302         else: 
    303             manipulator.do_html2python(new_data) 
    304             comment = manipulator.save(new_data) 
    305         return HttpResponseRedirect("../posted/?c=%s:%s" % (content_type_id, object_id)) 
    306     else: 
    307         raise Http404, _("The comment form didn't provide either 'preview' or 'post'") 
     112comment_done = confirmation_view( 
     113    template = "comments/posted.html", 
     114    doc = """Display a "comment was posted" success page.""" 
     115
    308116 
    309 def post_free_comment(request, extra_context=None, context_processors=None): 
    310     """ 
    311     Post a free comment (not requiring a log in) 
    312  
    313     Redirects to `comments.comments.comment_was_posted` view on success. 
    314  
    315     Templates: `comment_free_preview` 
    316     Context: 
    317         comment 
    318             comment being posted 
    319         comment_form 
    320             comment form object 
    321         options 
    322             comment options 
    323         target 
    324             comment target 
    325         hash 
    326             security hash (must be included in a posted form to succesfully 
    327             post a comment). 
    328     """ 
    329     if extra_context is None: extra_context = {} 
    330     if not request.POST: 
    331         raise Http404, _("Only POSTs are allowed") 
    332     try: 
    333         options, target, security_hash = request.POST['options'], request.POST['target'], request.POST['gonzo'] 
    334     except KeyError: 
    335         raise Http404, _("One or more of the required fields wasn't submitted") 
    336     if Comment.objects.get_security_hash(options, '', '', target) != security_hash: 
    337         raise Http404, _("Somebody tampered with the comment form (security violation)") 
    338     content_type_id, object_id = target.split(':') # target is something like '52:5157' 
    339     content_type = ContentType.objects.get(pk=content_type_id) 
    340     try: 
    341         obj = content_type.get_object_for_this_type(pk=object_id) 
    342     except ObjectDoesNotExist: 
    343         raise Http404, _("The comment form had an invalid 'target' parameter -- the object ID was invalid") 
    344     option_list = options.split(',') 
    345     new_data = request.POST.copy() 
    346     new_data['content_type_id'] = content_type_id 
    347     new_data['object_id'] = object_id 
    348     new_data['ip_address'] = request.META['REMOTE_ADDR'] 
    349     new_data['is_public'] = IS_PUBLIC in option_list 
    350     manipulator = PublicFreeCommentManipulator() 
    351     errors = manipulator.get_validation_errors(new_data) 
    352     if errors or 'preview' in request.POST: 
    353         comment = errors and '' or manipulator.get_comment(new_data) 
    354         return render_to_response('comments/free_preview.html', { 
    355             'comment': comment, 
    356             'comment_form': oldforms.FormWrapper(manipulator, new_data, errors), 
    357             'options': options, 
    358             'target': target, 
    359             'hash': security_hash, 
    360         }, context_instance=RequestContext(request, extra_context, context_processors)) 
    361     elif 'post' in request.POST: 
    362         # If the IP is banned, mail the admins, do NOT save the comment, and 
    363         # serve up the "Thanks for posting" page as if the comment WAS posted. 
    364         if request.META['REMOTE_ADDR'] in settings.BANNED_IPS: 
    365             from django.core.mail import mail_admins 
    366             mail_admins("Practical joker", smart_unicode(request.POST) + "\n\n" + str(request.META)) 
    367         else: 
    368             manipulator.do_html2python(new_data) 
    369             comment = manipulator.save(new_data) 
    370         return HttpResponseRedirect("../posted/?c=%s:%s" % (content_type_id, object_id)) 
    371     else: 
    372         raise Http404, _("The comment form didn't provide either 'preview' or 'post'") 
    373  
    374 def comment_was_posted(request, extra_context=None, context_processors=None): 
    375     """ 
    376     Display "comment was posted" success page 
    377  
    378     Templates: `comment_posted` 
    379     Context: 
    380         object 
    381             The object the comment was posted on 
    382     """ 
    383     if extra_context is None: extra_context = {} 
    384     obj = None 
    385     if 'c' in request.GET: 
    386         content_type_id, object_id = request.GET['c'].split(':') 
    387         try: 
    388             content_type = ContentType.objects.get(pk=content_type_id) 
    389             obj = content_type.get_object_for_this_type(pk=object_id) 
    390         except ObjectDoesNotExist: 
    391             pass 
    392     return render_to_response('comments/posted.html', {'object': obj}, 
    393         context_instance=RequestContext(request, extra_context, context_processors)) 
  • django/trunk/docs/index.txt

    r8506 r8557  
    7373--------- 
    7474 
    75 :ref:`topics-auth` ... :ref:`topics-cache` ... :ref:`topics-email` ... 
    76 :ref:`topics-files` ... :ref:`topics-i18n` ... :ref:`topics-install` ... 
    77 :ref:`topics-pagination` ... :ref:`topics-serialization` ... 
    78 :ref:`topics-settings` ... :ref:`topics-testing` 
     75    * :ref:`topics-auth` 
     76    * :ref:`topics-cache` 
     77    * :ref:`topics-email` 
     78    * :ref:`topics-files` 
     79    * :ref:`topics-i18n` 
     80    * :ref:`topics-install` 
     81    * :ref:`topics-pagination` 
     82    * :ref:`topics-serialization` 
     83    * :ref:`topics-settings` 
     84    * :ref:`topics-testing` 
    7985     
    8086Add-on ("contrib") applications 
     
    96102--------- 
    97103 
    98 :ref:`ref-contrib-contenttypes` ... :ref:`ref-contrib-csrf` ... 
    99 :ref:`ref-contrib-databrowse` ... :ref:`ref-contrib-flatpages` ... 
    100 :ref:`ref-contrib-humanize` ... :ref:`ref-contrib-redirects` ... 
    101 :ref:`ref-contrib-sitemaps` ... :ref:`ref-contrib-sites` ... 
    102 :ref:`ref-contrib-webdesign` 
     104    * :ref:`ref-contrib-comments-index` 
     105    * :ref:`ref-contrib-contenttypes` 
     106    * :ref:`ref-contrib-csrf` 
     107    * :ref:`ref-contrib-databrowse` 
     108    * :ref:`ref-contrib-flatpages` 
     109    * :ref:`ref-contrib-humanize` 
     110    * :ref:`ref-contrib-redirects` 
     111    * :ref:`ref-contrib-sitemaps` 
     112    * :ref:`ref-contrib-sites` 
     113    * :ref:`ref-contrib-webdesign` 
    103114 
    104115Solving specific problems 
     
    121132--------- 
    122133 
    123 :ref:`Authenticating in Apache <howto-apache-auth>` ... 
    124 :ref:`howto-custom-file-storage` ... :ref:`howto-custom-management-commands` ... 
    125 :ref:`howto-custom-model-fields` ... :ref:`howto-error-reporting` ... 
    126 :ref:`howto-initial-data` ... :ref:`howto-static-files` 
    127      
     134    * :ref:`Authenticating in Apache <howto-apache-auth>` 
     135    * :ref:`howto-custom-file-storage` 
     136    * :ref:`howto-custom-management-commands` 
     137    * :ref:`howto-custom-model-fields` 
     138    * :ref:`howto-error-reporting` 
     139    * :ref:`howto-initial-data` 
     140    * :ref:`howto-static-files` 
     141 
    128142Reference 
    129143========= 
     
    144158--------- 
    145159 
    146 :ref:`ref-databases` ... :ref:`ref-django-admin` ... :ref:`ref-files-index` ... 
    147 :ref:`ref-generic-views` ... :ref:`ref-middleware` ... 
    148 :ref:`ref-templates-index` ... :ref:`ref-unicode` 
     160    * :ref:`ref-databases` 
     161    * :ref:`ref-django-admin` 
     162    * :ref:`ref-files-index` 
     163    * :ref:`ref-generic-views` 
     164    * :ref:`ref-middleware` 
     165    * :ref:`ref-templates-index` 
     166    * :ref:`ref-unicode` 
    149167     
    150168And all the rest 
  • django/trunk/docs/ref/contrib/index.txt

    r8506 r8557  
    2727   admin 
    2828   auth 
     29   comments/index 
    2930   contenttypes 
    3031   csrf 
     
    5960======== 
    6061 
    61 A simple yet flexible comments system. This is not yet documented. 
     62**New in Django development version.** 
     63 
     64A simple yet flexible comments system. See :ref:`ref-contrib-comments-index`. 
    6265 
    6366contenttypes 
  • django/trunk/docs/_static/djangodocs.css

    r8506 r8557  
    6363ul { padding-left:30px; } 
    6464ol { padding-left:30px; } 
    65 ol.arabic { list-style-type: decimal; } 
     65ol.arabic li { list-style-type: decimal; } 
    6666ul li { list-style-type:square; margin-bottom:.4em; } 
    6767ol li { margin-bottom: .4em; } 
  • django/trunk/docs/topics/templates.txt

    r8506 r8557  
    608608variable and object available to you in a given template. 
    609609 
     610.. _loading-custom-template-libraries: 
     611 
    610612Custom tag and filter libraries 
    611613===============================