|
0
|
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 |
|