1 | "Custom template tags for user comments"
|
---|
2 |
|
---|
3 | from django.core import template
|
---|
4 | from django.core.exceptions import ObjectDoesNotExist
|
---|
5 | from django.models.comments import comments, freecomments
|
---|
6 | from django.models.core import contenttypes
|
---|
7 | import re
|
---|
8 |
|
---|
9 | COMMENT_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> </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 |
|
---|
45 | FREE_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 |
|
---|
58 | class 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 |
|
---|
115 | class 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 |
|
---|
132 | class 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 |
|
---|
175 | class 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 |
|
---|
249 | class 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 |
|
---|
299 | class 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
|
---|
361 | template.register_tag('get_comment_list', DoGetCommentList(False))
|
---|
362 | template.register_tag('comment_form', DoCommentForm(False))
|
---|
363 | template.register_tag('get_comment_count', DoCommentCount(False))
|
---|
364 | # free comments
|
---|
365 | template.register_tag('get_free_comment_list', DoGetCommentList(True))
|
---|
366 | template.register_tag('free_comment_form', DoCommentForm(True))
|
---|
367 | template.register_tag('get_free_comment_count', DoCommentCount(True))
|
---|