Django

Code

Changeset 8557

Show
Ignore:
Timestamp:
08/25/08 17:14:22 (3 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', PHO