|
1 from django.forms.fields import email_re |
|
2 from django.http import HttpResponse, HttpResponseRedirect |
|
3 from django.core.exceptions import ObjectDoesNotExist |
|
4 from django.forms import ValidationError |
|
5 from django.utils import simplejson |
|
6 from django.utils.translation import ugettext as _ |
|
7 from tagging.models import Tag |
|
8 from tagging.utils import parse_tag_input, LOGARITHMIC, calculate_cloud |
|
9 from tagging.models import TaggedItem |
|
10 from cm.models import * |
|
11 from cm.utils.timezone import request_tz_convert |
|
12 from itertools import groupby |
|
13 from time import mktime, sleep |
|
14 from cm.converters.pandoc_converters import pandoc_convert |
|
15 from cm.security import get_viewable_comments, list_viewable_comments, has_perm, has_perm_on_text, has_perm_on_comment, has_own_perm |
|
16 from cm.activity import register_activity |
|
17 from cm.utils.date import datetime_to_user_str, datetime_to_epoch |
|
18 from cm.cm_settings import AUTO_CONTRIB_REGISTER |
|
19 from settings import CLIENT_DATE_FMT |
|
20 import re |
|
21 import time |
|
22 import operator |
|
23 |
|
24 |
|
25 selection_place_error_msg = _(u'A selection is required. Select in the text the part your comment applies to.') |
|
26 comment_states = ('approved', 'unapproved', 'pending') |
|
27 |
|
28 def is_valid_email(email): |
|
29 if email_re.match(email) : |
|
30 return True |
|
31 return False |
|
32 |
|
33 # |
|
34 def jsonize(obj, request): |
|
35 return simplejson.dumps(obj, cls=RequestComplexEncoder, request=request) |
|
36 |
|
37 class RequestComplexEncoder(simplejson.JSONEncoder): |
|
38 def __init__(self, request, **kw): |
|
39 self.request = request |
|
40 simplejson.JSONEncoder.__init__(self, **kw) |
|
41 |
|
42 |
|
43 def default(self, obj): |
|
44 if isinstance(obj, Comment) : |
|
45 comment = obj |
|
46 #replies = list(comment.comment_set.order_by('created')) |
|
47 text=comment.text_version.text |
|
48 replies = get_viewable_comments(self.request, comment.comment_set.all(), text) |
|
49 |
|
50 # can_view == true because of get_viewable_comments filter |
|
51 can_moderate = has_perm(self.request, 'can_edit_comment', text) |
|
52 can_edit = has_perm(self.request, 'can_edit_comment', text) or has_own_perm(self.request, 'can_edit_comment_own', text, comment) |
|
53 can_delete = has_perm(self.request, 'can_delete_comment', text) or has_own_perm(self.request, 'can_delete_comment_own', text, comment) |
|
54 |
|
55 return {'id' : comment.id, |
|
56 'key' : comment.key, |
|
57 'created_user_str' : datetime_to_user_str(request_tz_convert(comment.created, self.request)), |
|
58 'modified_user_str' : datetime_to_user_str(request_tz_convert(comment.modified, self.request)), |
|
59 # 'created_str' : datetime_to_str(comment.created), # TODO change to a simple number as modified if possible |
|
60 'created' : datetime_to_epoch(comment.created), # TODO change to a simple number as modified if possible |
|
61 'modified' : datetime_to_epoch(comment.modified), |
|
62 # 'modified' : time.mktime(comment.modified.timetuple()), |
|
63 # 'created' : datetime_to_js_date_str(comment.created), |
|
64 'reply_to_id' : comment.reply_to_id, |
|
65 'replies' : replies, |
|
66 'name' : comment.get_name(), |
|
67 'email' : comment.get_email(), |
|
68 'logged_author' : (comment.user != None), |
|
69 'title':comment.title, |
|
70 'content':comment.content, |
|
71 'content_html':comment.content_html, |
|
72 'tags': ', '.join(parse_tag_input(comment.tags)), |
|
73 'format': comment.format, |
|
74 'start_wrapper' : comment.start_wrapper, |
|
75 'end_wrapper' : comment.end_wrapper, |
|
76 'start_offset' : comment.start_offset, |
|
77 'end_offset' : comment.end_offset, |
|
78 'state' : comment.state, |
|
79 'permalink' : reverse('text-view-show-comment', args=[text.key, comment.key]), |
|
80 # permission |
|
81 'can_edit' : can_edit, |
|
82 'can_delete' : can_delete, |
|
83 'can_moderate' : can_moderate, |
|
84 } |
|
85 if isinstance(obj, Tag) : |
|
86 tag = obj |
|
87 # RBE each time issuing a db request to find comments related to this tag !!! TODO |
|
88 return { 'ids' : [t.id for t in tag.items.all()], 'name' : tag.name, 'font_size' : tag.font_size} |
|
89 |
|
90 return simplejson.JSONEncoder.default(self, obj) |
|
91 |
|
92 def experiment() : |
|
93 sleep(5) ; |
|
94 return {"key":"value"} |
|
95 |
|
96 def read_comment_args(request): |
|
97 name = request.POST.get('name', None) |
|
98 email = request.POST.get('email', None) |
|
99 if name != None : |
|
100 name = name.lower().strip() |
|
101 if email != None : |
|
102 email = email.lower().strip() |
|
103 |
|
104 title = request.POST['title'].strip() |
|
105 content = request.POST['content'].strip() |
|
106 |
|
107 tags = request.POST['tags'] |
|
108 |
|
109 reply_to_id = request.POST.get('reply_to_id', None) |
|
110 |
|
111 format = request.POST['format'] |
|
112 |
|
113 start_wrapper = request.POST.get('start_wrapper', None) |
|
114 end_wrapper = request.POST.get('end_wrapper', None) |
|
115 start_offset = request.POST.get('start_offset', None) |
|
116 end_offset = request.POST.get('end_offset', None) |
|
117 |
|
118 if start_wrapper : |
|
119 start_wrapper = int(start_wrapper.strip()) |
|
120 if end_wrapper : |
|
121 end_wrapper = int(end_wrapper.strip()) |
|
122 if start_offset : |
|
123 start_offset = int(start_offset.strip()) |
|
124 if end_offset : |
|
125 end_offset = int(end_offset.strip()) |
|
126 |
|
127 return name, email, title, content, tags, reply_to_id, format, start_wrapper, end_wrapper, start_offset, end_offset |
|
128 |
|
129 def validate_comment_args(name, email, title, content, tags): |
|
130 errors = {} |
|
131 if name != None : |
|
132 if name == "" : |
|
133 errors['name'] = _(u'name is required') |
|
134 if email != None : |
|
135 if email == "" : |
|
136 errors['email'] = _(u'email is required') |
|
137 if not is_valid_email(email) : |
|
138 errors['email'] = _('invalid email') |
|
139 if title == "" : |
|
140 errors['title'] = _(u'title is required') |
|
141 if content == "" : |
|
142 errors['content'] = _(u'content is required') |
|
143 |
|
144 tag_errors = validate_tags(tags) |
|
145 if tag_errors != "" : |
|
146 errors['tags'] = tag_errors |
|
147 |
|
148 return errors |
|
149 |
|
150 @has_perm_on_comment("can_delete_comment") |
|
151 def remove_comment(request, key, comment_key): |
|
152 ret={} |
|
153 try: |
|
154 text = Text.objects.get(key=key) |
|
155 comment = Comment.objects.get(key = comment_key) |
|
156 comment.delete() |
|
157 ret['msg'] = _(u'comment removed') |
|
158 register_activity(request, "comment_removed", text=text, comment=comment) |
|
159 except ObjectDoesNotExist: |
|
160 pass |
|
161 return ret ; |
|
162 |
|
163 @has_perm_on_comment("can_edit_comment") |
|
164 def edit_comment(request, key, comment_key): |
|
165 state = request.POST.get('state', None) |
|
166 change_state = state and state in comment_states |
|
167 |
|
168 errors = {} |
|
169 if not change_state : # moderation action |
|
170 change_scope = request.POST.get('change_scope', None) |
|
171 |
|
172 name, email, title, content, tags, reply_to_id, format, start_wrapper, end_wrapper, start_offset, end_offset = read_comment_args(request) |
|
173 |
|
174 errors = validate_comment_args(name, email, title, content, tags) |
|
175 |
|
176 if (change_scope) and start_wrapper=="" : |
|
177 errors['selection_place'] = selection_place_error_msg |
|
178 |
|
179 content_html = pandoc_convert(content, format, "html", full=False) |
|
180 |
|
181 ret = {} |
|
182 if errors != {} : |
|
183 ret['errors'] = errors |
|
184 else : |
|
185 # INSERT |
|
186 # TODO check version still exist ... |
|
187 #comment = Comment.objects.get(id=edit_comment_id) |
|
188 comment = Comment.objects.get(key=comment_key) |
|
189 if change_state : # moderation action |
|
190 comment.state = state |
|
191 else : |
|
192 comment.name = name |
|
193 comment.email = email |
|
194 comment.title = title |
|
195 comment.content = content |
|
196 comment.content_html = content_html |
|
197 comment.tags = tags |
|
198 |
|
199 if change_scope : |
|
200 comment.start_wrapper = start_wrapper |
|
201 comment.start_offset = start_offset |
|
202 comment.end_wrapper = end_wrapper |
|
203 comment.end_offset = end_offset |
|
204 |
|
205 comment.save() |
|
206 |
|
207 ret['comment'] = comment |
|
208 ret['msg'] = _(u'comment saved') |
|
209 return ret |
|
210 |
|
211 @has_perm_on_text("can_create_comment") |
|
212 def add_comment(request, key): |
|
213 # if edit_comment_id : # |
|
214 # if self.request.user.is_anonymous() : # accessing via an admin url ? |
|
215 # and comment.user == self.request.user |
|
216 user = None if request.user.is_anonymous() else request.user |
|
217 name, email, title, content, tags, reply_to_id, format, start_wrapper, end_wrapper, start_offset, end_offset = read_comment_args(request) |
|
218 errors = {} |
|
219 errors = validate_comment_args(name, email, title, content, tags) |
|
220 |
|
221 if start_wrapper == "" : |
|
222 errors['selection_place'] = selection_place_error_msg |
|
223 |
|
224 #TODO validate pandoc conversion |
|
225 content_html = pandoc_convert(content, format, "html", full=False) |
|
226 |
|
227 ret = {} |
|
228 if errors != {} : |
|
229 ret['errors'] = errors |
|
230 else : |
|
231 # INSERT |
|
232 # TODO check version still exist ... |
|
233 reply_to = None |
|
234 if reply_to_id : |
|
235 reply_to = Comment.objects.get(id=reply_to_id) |
|
236 |
|
237 text = Text.objects.get(key=key) |
|
238 text_version = text.get_latest_version() |
|
239 |
|
240 comment_state = 'approved' if text_version.mod_posteriori else 'pending' |
|
241 comment = Comment.objects.create(state=comment_state, text_version=text_version, user=user, name=name, email=email, title=title, content=content, content_html=content_html, tags = tags, start_wrapper = start_wrapper, end_wrapper = end_wrapper, start_offset = start_offset, end_offset = end_offset, reply_to=reply_to) |
|
242 |
|
243 if text_version.mod_posteriori or has_perm(request, 'can_view_unapproved_comment', text=text) : |
|
244 ret['comment'] = comment |
|
245 ret['msg'] = _(u"comment saved") |
|
246 else : |
|
247 ret['msg'] = _(u"comment saved, it is being held for moderation") |
|
248 |
|
249 if AUTO_CONTRIB_REGISTER: |
|
250 Notification.objects.set_notification_to_own_discussions(text=text, email_or_user=user or email) |
|
251 register_activity(request, "comment_created", text, comment) |
|
252 return ret |
|
253 |
|
254 #we need to call comments_thread from here this function will be very expensive |
|
255 # TODO: stupid get rid of text argument |
|
256 def get_filter_datas(request, text_version, text): |
|
257 from django.db.models import Count |
|
258 from datetime import datetime, timedelta |
|
259 |
|
260 allowed_ids = [c.id for c in comments_thread(request, text_version, text)] |
|
261 allowed_comments = Comment.objects.filter(Q(text_version=text_version),Q(deleted=False),Q(id__in=allowed_ids)) |
|
262 #print allowed_ids |
|
263 |
|
264 # authors |
|
265 # names = list(Comment.objects.filter(text_version__text__key=key).filter(user__isnull=True).values('name').annotate(nb_comments=Count('id'))) #.order_by('name')) |
|
266 names = list(allowed_comments.filter(user__isnull=True).values('name').annotate(nb_comments=Count('id'))) #.order_by('name')) |
|
267 names += list(User.objects.filter(Q(comment__text_version=text_version),Q(comment__deleted=False), Q(comment__id__in=allowed_ids)).extra(select={'name': "username"}).values('name').annotate(nb_comments=Count('id'))) #.order_by('username')) |
|
268 names.sort(key = lambda obj:obj["name"]) |
|
269 |
|
270 # dates |
|
271 # TODO maybe optimize by comparing dates in python and saving these 'by day db requests' |
|
272 nb_days = [1, 3, 7, 30] |
|
273 dates = [] |
|
274 today = datetime.today() |
|
275 for nb_day in nb_days : |
|
276 day_date = today - timedelta(nb_day) |
|
277 dates.append({'nb_day' : nb_day, 'nb_day_date':datetime_to_epoch(day_date), 'nb_comments':allowed_comments.filter(modified__gt = day_date).count()}) |
|
278 |
|
279 # tags |
|
280 comment_ids = [c.id for c in allowed_comments] |
|
281 tags = list(Tag.objects.filter(items__content_type = ContentType.objects.get_for_model(Comment),items__object_id__in=comment_ids).values("name").annotate(nb_comments=Count('id')).distinct().order_by('name')) |
|
282 |
|
283 # states |
|
284 states = [] |
|
285 for state in comment_states : |
|
286 states.append({'state' : state, 'nb_comments':allowed_comments.filter(state = state).count()}) |
|
287 |
|
288 return {'names':names, 'dates':dates, 'tags':tags, 'states':states} |
|
289 |
|
290 #def get_ordered_ids(text_version_id): |
|
291 # comments_and_replies = Comment.objects.filter(text_version__id=text_version_id) |
|
292 # comments = comments_and_replies.filter(reply_to__isnull=True) |
|
293 # |
|
294 # dic = {} |
|
295 # for c in comments_and_replies : |
|
296 # top_comment = c.top_comment() |
|
297 # max_modif = dic.get(top_comment.id, c.modified) |
|
298 # dic[top_comment.id] = c.modified if max_modif < c.modified else max_modif |
|
299 # |
|
300 # ordered_comment_ids = {'scope' : [c.id for c in comments.order_by('start_wrapper','start_offset','end_offset')], |
|
301 # 'thread_modified' : map(operator.itemgetter(0), sorted(dic.items(), key=operator.itemgetter(1)))} |
|
302 # return ordered_comment_ids |
|
303 |
|
304 def validate_tags(tags): |
|
305 if tags : |
|
306 try : |
|
307 if len(tags) > 250 : |
|
308 return _(u"Tags input must be no longer than 250 characters.") |
|
309 TagField().formfield().clean(tags) |
|
310 except ValidationError, e : |
|
311 return ",".join(e.messages) |
|
312 return '' |
|
313 |
|
314 MAX_NB_TAGS_IN_COMMENT_CLOUD = 30 |
|
315 def get_tagcloud(key) : |
|
316 tagCloud = Tag.objects.cloud_for_model(Comment, steps=8, distribution=LOGARITHMIC, filters=dict(text_version__text__key=key)) |
|
317 return tagCloud |
|
318 |
|
319 # returns a flat list of viewable comments and their replies ordered as they should be : |
|
320 # order is : |
|
321 # 'start_wrapper','start_offset','end_wrapper','end_offset' for 'real' comments |
|
322 # 'created' for replies |
|
323 # TODO: get rid of text here, keep text_version |
|
324 def comments_thread(request, text_version, text) : |
|
325 commentsnoreply = text_version.comment_set.filter(reply_to__isnull=True)#id=3) |
|
326 viewable_commentsnoreply = get_viewable_comments(request, commentsnoreply, text, order_by = ('start_wrapper','start_offset','end_wrapper','end_offset')) |
|
327 viewable_comments = [] |
|
328 for cc in viewable_commentsnoreply : |
|
329 viewable_comments += list_viewable_comments(request, [cc], text) |
|
330 return viewable_comments |
|
331 |