Ticket #772: comments.py

File comments.py, 18.2 KB (added by vlado@…, 18 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))
Back to Top