Code

Ticket #772: comments.py

File comments.py, 18.2 KB (added by vlado@…, 8 years ago)
Line 
1"Custom template tags for user comments"
2
3from django.core import template
4from django.core.exceptions import ObjectDoesNotExist
5from django.models.comments import comments, freecomments
6from django.models.core import contenttypes
7import re
8
9COMMENT_FORM = '''
10{% if display_form %}
11<form {% if photos_optional or photos_required %}enctype="multipart/form-data" {% endif %}action="/comments/post/" method="post">
12
13{% if user.is_anonymous %}
14<p>Username: <input type="text" name="username" id="id_username" /><br />Password: <input type="password" name="password" id="id_password" /> (<a href="/accounts/password_reset/">Forgotten your password?</a>)</p>
15{% else %}
16<p>Username: <strong>{{ user.username }}</strong> (<a href="/accounts/logout/">Log out</a>)</p>
17{% endif %}
18
19{% if ratings_optional or ratings_required %}
20<p>Ratings ({% if ratings_required %}Required{% else %}Optional{% endif %}):</p>
21<table>
22<tr><th>&nbsp;</th>{% for value in rating_range %}<th>{{ value }}</th>{% endfor %}</tr>
23{% for rating in rating_choices %}
24<tr><th>{{ rating }}</th>{% for value in rating_range %}<th><input type="radio" name="rating{{ forloop.parentloop.counter }}" value="{{ value }}" /></th>{% endfor %}</tr>
25{% endfor %}
26</table>
27<input type="hidden" name="rating_options" value="{{ rating_options }}" />
28{% endif %}
29
30{% if photos_optional or photos_required %}
31<p>Post a photo ({% if photos_required %}Required{% else %}Optional{% endif %}): <input type="file" name="photo" /></p>
32<input type="hidden" name="photo_options" value="{{ photo_options }}" />
33{% endif %}
34
35<p>Comment:<br /><textarea name="comment" id="id_comment" rows="10" cols="60"></textarea></p>
36
37<input type="hidden" name="options" value="{{ options }}" />
38<input type="hidden" name="target" value="{{ target }}" />
39<input type="hidden" name="gonzo" value="{{ hash }}" />
40<p><input type="submit" name="preview" value="Preview comment" /></p>
41</form>
42{% endif %}
43'''
44
45FREE_COMMENT_FORM = '''
46{% if display_form %}
47<form action="/comments/postfree/" method="post">
48<p>{% trans 'Your name' %}: <input type="text" id="id_person_name" name="person_name" /></p>
49<p>{% trans 'Comment' %}:<br /><textarea name="comment" id="id_comment" rows="10" cols="60"></textarea></p>
50<input type="hidden" name="options" value="{{ options }}" />
51<input type="hidden" name="target" value="{{ target }}" />
52<input type="hidden" name="gonzo" value="{{ hash }}" />
53<p><input type="submit" name="preview" value="{% trans 'Preview comment' %}" /></p>
54</form>
55{% endif %}
56'''
57
58class CommentFormNode(template.Node):
59    def __init__(self, content_type, obj_id_lookup_var, obj_id, free,
60        photos_optional=False, photos_required=False, photo_options='',
61        ratings_optional=False, ratings_required=False, rating_options='',
62        is_public=True):
63        self.content_type = content_type
64        self.obj_id_lookup_var, self.obj_id, self.free = obj_id_lookup_var, obj_id, free
65        self.photos_optional, self.photos_required = photos_optional, photos_required
66        self.ratings_optional, self.ratings_required = ratings_optional, ratings_required
67        self.photo_options, self.rating_options = photo_options, rating_options
68        self.is_public = is_public
69
70    def render(self, context):
71        from django.utils.text import normalize_newlines
72        import base64
73        context.push()
74        if self.obj_id_lookup_var is not None:
75            try:
76                self.obj_id = template.resolve_variable(self.obj_id_lookup_var, context)
77            except template.VariableDoesNotExist:
78                return ''
79            # Validate that this object ID is valid for this content-type.
80            # We only have to do this validation if obj_id_lookup_var is provided,
81            # because do_comment_form() validates hard-coded object IDs.
82            try:
83                self.content_type.get_object_for_this_type(pk=self.obj_id)
84            except ObjectDoesNotExist:
85                context['display_form'] = False
86            else:
87                context['display_form'] = True
88        else:
89            context['display_form'] = True
90        context['target'] = '%s:%s' % (self.content_type.id, self.obj_id)
91        options = []
92        for var, abbr in (('photos_required', comments.PHOTOS_REQUIRED),
93                          ('photos_optional', comments.PHOTOS_OPTIONAL),
94                          ('ratings_required', comments.RATINGS_REQUIRED),
95                          ('ratings_optional', comments.RATINGS_OPTIONAL),
96                          ('is_public', comments.IS_PUBLIC)):
97            context[var] = getattr(self, var)
98            if getattr(self, var):
99                options.append(abbr)
100        context['options'] = ','.join(options)
101        if self.free:
102            context['hash'] = comments.get_security_hash(context['options'], '', '', context['target'])
103            default_form = FREE_COMMENT_FORM
104        else:
105            context['photo_options'] = self.photo_options
106            context['rating_options'] = normalize_newlines(base64.encodestring(self.rating_options).strip())
107            if self.rating_options:
108                context['rating_range'], context['rating_choices'] = comments.get_rating_options(self.rating_options)
109            context['hash'] = comments.get_security_hash(context['options'], context['photo_options'], context['rating_options'], context['target'])
110            default_form = COMMENT_FORM
111        output = template.Template(default_form).render(context)
112        context.pop()
113        return output
114
115class CommentCountNode(template.Node):
116    def __init__(self, package, module, context_var_name, obj_id, var_name, free):
117        self.package, self.module = package, module
118        self.context_var_name, self.obj_id = context_var_name, obj_id
119        self.var_name, self.free = var_name, free
120
121    def render(self, context):
122        from django.conf.settings import SITE_ID
123        get_count_function = self.free and freecomments.get_count or comments.get_count
124        if self.context_var_name is not None:
125            self.obj_id = template.resolve_variable(self.context_var_name, context)
126        comment_count = get_count_function(object_id__exact=self.obj_id,
127            content_type__package__label__exact=self.package,
128            content_type__python_module_name__exact=self.module, site__id__exact=SITE_ID)
129        context[self.var_name] = comment_count
130        return ''
131
132class CommentListNode(template.Node):
133    def __init__(self, package, module, context_var_name, obj_id, var_name, free, ordering, extra_kwargs=None):
134        self.package, self.module = package, module
135        self.context_var_name, self.obj_id = context_var_name, obj_id
136        self.var_name, self.free = var_name, free
137        self.ordering = ordering
138        self.extra_kwargs = extra_kwargs or {}
139
140    def render(self, context):
141        from django.conf.settings import COMMENTS_BANNED_USERS_GROUP, SITE_ID
142        get_list_function = self.free and freecomments.get_list or comments.get_list_with_karma
143        if self.context_var_name is not None:
144            try:
145                self.obj_id = template.resolve_variable(self.context_var_name, context)
146            except template.VariableDoesNotExist:
147                return ''
148        kwargs = {
149            'object_id__exact': self.obj_id,
150            'content_type__package__label__exact': self.package,
151            'content_type__python_module_name__exact': self.module,
152            'site__id__exact': SITE_ID,
153            'select_related': True,
154            'order_by': (self.ordering + 'submit_date',),
155        }
156        kwargs.update(self.extra_kwargs)
157        if not self.free and COMMENTS_BANNED_USERS_GROUP:
158            kwargs['select'] = {'is_hidden': 'user_id IN (SELECT user_id FROM auth_users_groups WHERE group_id = %s)' % COMMENTS_BANNED_USERS_GROUP}
159        comment_list = get_list_function(**kwargs)
160
161        if not self.free:
162            if context.has_key('user') and not context['user'].is_anonymous():
163                user_id = context['user'].id
164                context['user_can_moderate_comments'] = comments.user_is_moderator(context['user'])
165            else:
166                user_id = None
167                context['user_can_moderate_comments'] = False
168            # Only display comments by banned users to those users themselves.
169            if COMMENTS_BANNED_USERS_GROUP:
170                comment_list = [c for c in comment_list if not c.is_hidden or (user_id == c.user_id)]
171
172        context[self.var_name] = comment_list
173        return ''
174
175class DoCommentForm:
176    """
177    Displays a comment form for the given params.
178
179    Syntax::
180
181        {% comment_form for [pkg].[py_module_name] [context_var_containing_obj_id] with [list of options] %}
182
183    Example usage::
184
185        {% 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 %}
186
187    ``[context_var_containing_obj_id]`` can be a hard-coded integer or a variable containing the ID.
188    """
189    def __init__(self, free):
190        self.free = free
191
192    def __call__(self, parser, token):
193        tokens = token.contents.split()
194        if len(tokens) < 4:
195            raise template.TemplateSyntaxError, "%r tag requires at least 3 arguments" % tokens[0]
196        if tokens[1] != 'for':
197            raise template.TemplateSyntaxError, "Second argument in %r tag must be 'for'" % tokens[0]
198        try:
199            package, module = tokens[2].split('.')
200        except ValueError: # unpack list of wrong size
201            raise template.TemplateSyntaxError, "Third argument in %r tag must be in the format 'package.module'" % tokens[0]
202        try:
203            content_type = contenttypes.get_object(package__label__exact=package, python_module_name__exact=module)
204        except contenttypes.ContentTypeDoesNotExist:
205            raise template.TemplateSyntaxError, "%r tag has invalid content-type '%s.%s'" % (tokens[0], package, module)
206        obj_id_lookup_var, obj_id = None, None
207        if tokens[3].isdigit():
208            obj_id = tokens[3]
209            try: # ensure the object ID is valid
210                content_type.get_object_for_this_type(pk=obj_id)
211            except ObjectDoesNotExist:
212                raise template.TemplateSyntaxError, "%r tag refers to %s object with ID %s, which doesn't exist" % (tokens[0], content_type.name, obj_id)
213        else:
214            obj_id_lookup_var = tokens[3]
215        kwargs = {}
216        if len(tokens) > 4:
217            if tokens[4] != 'with':
218                raise template.TemplateSyntaxError, "Fourth argument in %r tag must be 'with'" % tokens[0]
219            for option, args in zip(tokens[5::2], tokens[6::2]):
220                if option in ('photos_optional', 'photos_required') and not self.free:
221                    # VALIDATION ##############################################
222                    option_list = args.split(',')
223                    if len(option_list) % 3 != 0:
224                        raise template.TemplateSyntaxError, "Incorrect number of comma-separated arguments to %r tag" % tokens[0]
225                    for opt in option_list[::3]:
226                        if not opt.isalnum():
227                            raise template.TemplateSyntaxError, "Invalid photo directory name in %r tag: '%s'" % (tokens[0], opt)
228                    for opt in option_list[1::3] + option_list[2::3]:
229                        if not opt.isdigit() or not (comments.MIN_PHOTO_DIMENSION <= int(opt) <= comments.MAX_PHOTO_DIMENSION):
230                            raise template.TemplateSyntaxError, "Invalid photo dimension in %r tag: '%s'. Only values between %s and %s are allowed." % (tokens[0], opt, comments.MIN_PHOTO_DIMENSION, comments.MAX_PHOTO_DIMENSION)
231                    # VALIDATION ENDS #########################################
232                    kwargs[option] = True
233                    kwargs['photo_options'] = args
234                elif option in ('ratings_optional', 'ratings_required') and not self.free:
235                    # VALIDATION ##############################################
236                    if 2 < len(args.split('|')) > 9:
237                        raise template.TemplateSyntaxError, "Incorrect number of '%s' options in %r tag. Use between 2 and 8." % (option, tokens[0])
238                    if re.match('^scale:\d+\-\d+\:$', args.split('|')[0]):
239                        raise template.TemplateSyntaxError, "Invalid 'scale' in %r tag's '%s' options" % (tokens[0], option)
240                    # VALIDATION ENDS #########################################
241                    kwargs[option] = True
242                    kwargs['rating_options'] = args
243                elif option in ('is_public'):
244                    kwargs[option] = (args == 'true')
245                else:
246                    raise template.TemplateSyntaxError, "%r tag got invalid parameter '%s'" % (tokens[0], option)
247        return CommentFormNode(content_type, obj_id_lookup_var, obj_id, self.free, **kwargs)
248
249class DoCommentCount:
250    """
251    Gets comment count for the given params and populates the template context
252    with a variable containing that value, whose name is defined by the 'as'
253    clause.
254
255    Syntax::
256
257        {% get_comment_count for [pkg].[py_module_name] [context_var_containing_obj_id] as [varname]  %}
258
259    Example usage::
260
261        {% get_comment_count for lcom.eventtimes event.id as comment_count %}
262
263    Note: ``[context_var_containing_obj_id]`` can also be a hard-coded integer, like this::
264
265        {% get_comment_count for lcom.eventtimes 23 as comment_count %}
266    """
267    def __init__(self, free):
268        self.free = free
269
270    def __call__(self, parser, token):
271        tokens = token.contents.split()
272        # Now tokens is a list like this:
273        # ['get_comment_list', 'for', 'lcom.eventtimes', 'event.id', 'as', 'comment_list']
274        if len(tokens) != 6:
275            raise template.TemplateSyntaxError, "%r tag requires 5 arguments" % tokens[0]
276        if tokens[1] != 'for':
277            raise template.TemplateSyntaxError, "Second argument in %r tag must be 'for'" % tokens[0]
278        try:
279            package, module = tokens[2].split('.')
280        except ValueError: # unpack list of wrong size
281            raise template.TemplateSyntaxError, "Third argument in %r tag must be in the format 'package.module'" % tokens[0]
282        try:
283            content_type = contenttypes.get_object(package__label__exact=package, python_module_name__exact=module)
284        except contenttypes.ContentTypeDoesNotExist:
285            raise template.TemplateSyntaxError, "%r tag has invalid content-type '%s.%s'" % (tokens[0], package, module)
286        var_name, obj_id = None, None
287        if tokens[3].isdigit():
288            obj_id = tokens[3]
289            try: # ensure the object ID is valid
290                content_type.get_object_for_this_type(pk=obj_id)
291            except ObjectDoesNotExist:
292                raise template.TemplateSyntaxError, "%r tag refers to %s object with ID %s, which doesn't exist" % (tokens[0], content_type.name, obj_id)
293        else:
294            var_name = tokens[3]
295        if tokens[4] != 'as':
296            raise template.TemplateSyntaxError, "Fourth argument in %r must be 'as'" % tokens[0]
297        return CommentCountNode(package, module, var_name, obj_id, tokens[5], self.free)
298
299class DoGetCommentList:
300    """
301    Gets comments for the given params and populates the template context with a
302    special comment_package variable, whose name is defined by the ``as``
303    clause.
304
305    Syntax::
306
307        {% get_comment_list for [pkg].[py_module_name] [context_var_containing_obj_id] as [varname] (reversed) %}
308
309    Example usage::
310
311        {% get_comment_list for lcom.eventtimes event.id as comment_list %}
312
313    Note: ``[context_var_containing_obj_id]`` can also be a hard-coded integer, like this::
314
315        {% get_comment_list for lcom.eventtimes 23 as comment_list %}
316
317    To get a list of comments in reverse order -- that is, most recent first --
318    pass ``reversed`` as the last param::
319
320        {% get_comment_list for lcom.eventtimes event.id as comment_list reversed %}
321    """
322    def __init__(self, free):
323        self.free = free
324
325    def __call__(self, parser, token):
326        tokens = token.contents.split()
327        # Now tokens is a list like this:
328        # ['get_comment_list', 'for', 'lcom.eventtimes', 'event.id', 'as', 'comment_list']
329        if not len(tokens) in (6, 7):
330            raise template.TemplateSyntaxError, "%r tag requires 5 or 6 arguments" % tokens[0]
331        if tokens[1] != 'for':
332            raise template.TemplateSyntaxError, "Second argument in %r tag must be 'for'" % tokens[0]
333        try:
334            package, module = tokens[2].split('.')
335        except ValueError: # unpack list of wrong size
336            raise template.TemplateSyntaxError, "Third argument in %r tag must be in the format 'package.module'" % tokens[0]
337        try:
338            content_type = contenttypes.get_object(package__label__exact=package, python_module_name__exact=module)
339        except contenttypes.ContentTypeDoesNotExist:
340            raise template.TemplateSyntaxError, "%r tag has invalid content-type '%s.%s'" % (tokens[0], package, module)
341        var_name, obj_id = None, None
342        if tokens[3].isdigit():
343            obj_id = tokens[3]
344            try: # ensure the object ID is valid
345                content_type.get_object_for_this_type(pk=obj_id)
346            except ObjectDoesNotExist:
347                raise template.TemplateSyntaxError, "%r tag refers to %s object with ID %s, which doesn't exist" % (tokens[0], content_type.name, obj_id)
348        else:
349            var_name = tokens[3]
350        if tokens[4] != 'as':
351            raise template.TemplateSyntaxError, "Fourth argument in %r must be 'as'" % tokens[0]
352        if len(tokens) == 7:
353            if tokens[6] != 'reversed':
354                raise template.TemplateSyntaxError, "Final argument in %r must be 'reversed' if given" % tokens[0]
355            ordering = "-"
356        else:
357            ordering = ""
358        return CommentListNode(package, module, var_name, obj_id, tokens[5], self.free, ordering)
359
360# registration comments
361template.register_tag('get_comment_list', DoGetCommentList(False))
362template.register_tag('comment_form', DoCommentForm(False))
363template.register_tag('get_comment_count', DoCommentCount(False))
364# free comments
365template.register_tag('get_free_comment_list', DoGetCommentList(True))
366template.register_tag('free_comment_form', DoCommentForm(True))
367template.register_tag('get_free_comment_count', DoCommentCount(True))