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