diff -r 000000000000 -r 40c8f766c9b8 src/cm/client.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/cm/client.py Mon Nov 23 15:14:29 2009 +0100 @@ -0,0 +1,331 @@ +from django.forms.fields import email_re +from django.http import HttpResponse, HttpResponseRedirect +from django.core.exceptions import ObjectDoesNotExist +from django.forms import ValidationError +from django.utils import simplejson +from django.utils.translation import ugettext as _ +from tagging.models import Tag +from tagging.utils import parse_tag_input, LOGARITHMIC, calculate_cloud +from tagging.models import TaggedItem +from cm.models import * +from cm.utils.timezone import request_tz_convert +from itertools import groupby +from time import mktime, sleep +from cm.converters.pandoc_converters import pandoc_convert +from cm.security import get_viewable_comments, list_viewable_comments, has_perm, has_perm_on_text, has_perm_on_comment, has_own_perm +from cm.activity import register_activity +from cm.utils.date import datetime_to_user_str, datetime_to_epoch +from cm.cm_settings import AUTO_CONTRIB_REGISTER +from settings import CLIENT_DATE_FMT +import re +import time +import operator + + +selection_place_error_msg = _(u'A selection is required. Select in the text the part your comment applies to.') +comment_states = ('approved', 'unapproved', 'pending') + +def is_valid_email(email): + if email_re.match(email) : + return True + return False + +# +def jsonize(obj, request): + return simplejson.dumps(obj, cls=RequestComplexEncoder, request=request) + +class RequestComplexEncoder(simplejson.JSONEncoder): + def __init__(self, request, **kw): + self.request = request + simplejson.JSONEncoder.__init__(self, **kw) + + + def default(self, obj): + if isinstance(obj, Comment) : + comment = obj + #replies = list(comment.comment_set.order_by('created')) + text=comment.text_version.text + replies = get_viewable_comments(self.request, comment.comment_set.all(), text) + + # can_view == true because of get_viewable_comments filter + can_moderate = has_perm(self.request, 'can_edit_comment', text) + can_edit = has_perm(self.request, 'can_edit_comment', text) or has_own_perm(self.request, 'can_edit_comment_own', text, comment) + can_delete = has_perm(self.request, 'can_delete_comment', text) or has_own_perm(self.request, 'can_delete_comment_own', text, comment) + + return {'id' : comment.id, + 'key' : comment.key, + 'created_user_str' : datetime_to_user_str(request_tz_convert(comment.created, self.request)), + 'modified_user_str' : datetime_to_user_str(request_tz_convert(comment.modified, self.request)), +# 'created_str' : datetime_to_str(comment.created), # TODO change to a simple number as modified if possible + 'created' : datetime_to_epoch(comment.created), # TODO change to a simple number as modified if possible + 'modified' : datetime_to_epoch(comment.modified), +# 'modified' : time.mktime(comment.modified.timetuple()), +# 'created' : datetime_to_js_date_str(comment.created), + 'reply_to_id' : comment.reply_to_id, + 'replies' : replies, + 'name' : comment.get_name(), + 'email' : comment.get_email(), + 'logged_author' : (comment.user != None), + 'title':comment.title, + 'content':comment.content, + 'content_html':comment.content_html, + 'tags': ', '.join(parse_tag_input(comment.tags)), + 'format': comment.format, + 'start_wrapper' : comment.start_wrapper, + 'end_wrapper' : comment.end_wrapper, + 'start_offset' : comment.start_offset, + 'end_offset' : comment.end_offset, + 'state' : comment.state, + 'permalink' : reverse('text-view-show-comment', args=[text.key, comment.key]), + # permission + 'can_edit' : can_edit, + 'can_delete' : can_delete, + 'can_moderate' : can_moderate, + } + if isinstance(obj, Tag) : + tag = obj + # RBE each time issuing a db request to find comments related to this tag !!! TODO + return { 'ids' : [t.id for t in tag.items.all()], 'name' : tag.name, 'font_size' : tag.font_size} + + return simplejson.JSONEncoder.default(self, obj) + +def experiment() : + sleep(5) ; + return {"key":"value"} + +def read_comment_args(request): + name = request.POST.get('name', None) + email = request.POST.get('email', None) + if name != None : + name = name.lower().strip() + if email != None : + email = email.lower().strip() + + title = request.POST['title'].strip() + content = request.POST['content'].strip() + + tags = request.POST['tags'] + + reply_to_id = request.POST.get('reply_to_id', None) + + format = request.POST['format'] + + start_wrapper = request.POST.get('start_wrapper', None) + end_wrapper = request.POST.get('end_wrapper', None) + start_offset = request.POST.get('start_offset', None) + end_offset = request.POST.get('end_offset', None) + + if start_wrapper : + start_wrapper = int(start_wrapper.strip()) + if end_wrapper : + end_wrapper = int(end_wrapper.strip()) + if start_offset : + start_offset = int(start_offset.strip()) + if end_offset : + end_offset = int(end_offset.strip()) + + return name, email, title, content, tags, reply_to_id, format, start_wrapper, end_wrapper, start_offset, end_offset + +def validate_comment_args(name, email, title, content, tags): + errors = {} + if name != None : + if name == "" : + errors['name'] = _(u'name is required') + if email != None : + if email == "" : + errors['email'] = _(u'email is required') + if not is_valid_email(email) : + errors['email'] = _('invalid email') + if title == "" : + errors['title'] = _(u'title is required') + if content == "" : + errors['content'] = _(u'content is required') + + tag_errors = validate_tags(tags) + if tag_errors != "" : + errors['tags'] = tag_errors + + return errors + +@has_perm_on_comment("can_delete_comment") +def remove_comment(request, key, comment_key): + ret={} + try: + text = Text.objects.get(key=key) + comment = Comment.objects.get(key = comment_key) + comment.delete() + ret['msg'] = _(u'comment removed') + register_activity(request, "comment_removed", text=text, comment=comment) + except ObjectDoesNotExist: + pass + return ret ; + +@has_perm_on_comment("can_edit_comment") +def edit_comment(request, key, comment_key): + state = request.POST.get('state', None) + change_state = state and state in comment_states + + errors = {} + if not change_state : # moderation action + change_scope = request.POST.get('change_scope', None) + + name, email, title, content, tags, reply_to_id, format, start_wrapper, end_wrapper, start_offset, end_offset = read_comment_args(request) + + errors = validate_comment_args(name, email, title, content, tags) + + if (change_scope) and start_wrapper=="" : + errors['selection_place'] = selection_place_error_msg + + content_html = pandoc_convert(content, format, "html", full=False) + + ret = {} + if errors != {} : + ret['errors'] = errors + else : + # INSERT + # TODO check version still exist ... + #comment = Comment.objects.get(id=edit_comment_id) + comment = Comment.objects.get(key=comment_key) + if change_state : # moderation action + comment.state = state + else : + comment.name = name + comment.email = email + comment.title = title + comment.content = content + comment.content_html = content_html + comment.tags = tags + + if change_scope : + comment.start_wrapper = start_wrapper + comment.start_offset = start_offset + comment.end_wrapper = end_wrapper + comment.end_offset = end_offset + + comment.save() + + ret['comment'] = comment + ret['msg'] = _(u'comment saved') + return ret + +@has_perm_on_text("can_create_comment") +def add_comment(request, key): +# if edit_comment_id : # +# if self.request.user.is_anonymous() : # accessing via an admin url ? +# and comment.user == self.request.user + user = None if request.user.is_anonymous() else request.user + name, email, title, content, tags, reply_to_id, format, start_wrapper, end_wrapper, start_offset, end_offset = read_comment_args(request) + errors = {} + errors = validate_comment_args(name, email, title, content, tags) + + if start_wrapper == "" : + errors['selection_place'] = selection_place_error_msg + + #TODO validate pandoc conversion + content_html = pandoc_convert(content, format, "html", full=False) + + ret = {} + if errors != {} : + ret['errors'] = errors + else : + # INSERT + # TODO check version still exist ... + reply_to = None + if reply_to_id : + reply_to = Comment.objects.get(id=reply_to_id) + + text = Text.objects.get(key=key) + text_version = text.get_latest_version() + + comment_state = 'approved' if text_version.mod_posteriori else 'pending' + 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) + + if text_version.mod_posteriori or has_perm(request, 'can_view_unapproved_comment', text=text) : + ret['comment'] = comment + ret['msg'] = _(u"comment saved") + else : + ret['msg'] = _(u"comment saved, it is being held for moderation") + + if AUTO_CONTRIB_REGISTER: + Notification.objects.set_notification_to_own_discussions(text=text, email_or_user=user or email) + register_activity(request, "comment_created", text, comment) + return ret + +#we need to call comments_thread from here this function will be very expensive +# TODO: stupid get rid of text argument +def get_filter_datas(request, text_version, text): + from django.db.models import Count + from datetime import datetime, timedelta + + allowed_ids = [c.id for c in comments_thread(request, text_version, text)] + allowed_comments = Comment.objects.filter(Q(text_version=text_version),Q(deleted=False),Q(id__in=allowed_ids)) + #print allowed_ids + + # authors +# names = list(Comment.objects.filter(text_version__text__key=key).filter(user__isnull=True).values('name').annotate(nb_comments=Count('id'))) #.order_by('name')) + names = list(allowed_comments.filter(user__isnull=True).values('name').annotate(nb_comments=Count('id'))) #.order_by('name')) + 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')) + names.sort(key = lambda obj:obj["name"]) + + # dates + # TODO maybe optimize by comparing dates in python and saving these 'by day db requests' + nb_days = [1, 3, 7, 30] + dates = [] + today = datetime.today() + for nb_day in nb_days : + day_date = today - timedelta(nb_day) + dates.append({'nb_day' : nb_day, 'nb_day_date':datetime_to_epoch(day_date), 'nb_comments':allowed_comments.filter(modified__gt = day_date).count()}) + + # tags + comment_ids = [c.id for c in allowed_comments] + 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')) + + # states + states = [] + for state in comment_states : + states.append({'state' : state, 'nb_comments':allowed_comments.filter(state = state).count()}) + + return {'names':names, 'dates':dates, 'tags':tags, 'states':states} + +#def get_ordered_ids(text_version_id): +# comments_and_replies = Comment.objects.filter(text_version__id=text_version_id) +# comments = comments_and_replies.filter(reply_to__isnull=True) +# +# dic = {} +# for c in comments_and_replies : +# top_comment = c.top_comment() +# max_modif = dic.get(top_comment.id, c.modified) +# dic[top_comment.id] = c.modified if max_modif < c.modified else max_modif +# +# ordered_comment_ids = {'scope' : [c.id for c in comments.order_by('start_wrapper','start_offset','end_offset')], +# 'thread_modified' : map(operator.itemgetter(0), sorted(dic.items(), key=operator.itemgetter(1)))} +# return ordered_comment_ids + +def validate_tags(tags): + if tags : + try : + if len(tags) > 250 : + return _(u"Tags input must be no longer than 250 characters.") + TagField().formfield().clean(tags) + except ValidationError, e : + return ",".join(e.messages) + return '' + +MAX_NB_TAGS_IN_COMMENT_CLOUD = 30 +def get_tagcloud(key) : + tagCloud = Tag.objects.cloud_for_model(Comment, steps=8, distribution=LOGARITHMIC, filters=dict(text_version__text__key=key)) + return tagCloud + +# returns a flat list of viewable comments and their replies ordered as they should be : +# order is : +# 'start_wrapper','start_offset','end_wrapper','end_offset' for 'real' comments +# 'created' for replies +# TODO: get rid of text here, keep text_version +def comments_thread(request, text_version, text) : + commentsnoreply = text_version.comment_set.filter(reply_to__isnull=True)#id=3) + viewable_commentsnoreply = get_viewable_comments(request, commentsnoreply, text, order_by = ('start_wrapper','start_offset','end_wrapper','end_offset')) + viewable_comments = [] + for cc in viewable_commentsnoreply : + viewable_comments += list_viewable_comments(request, [cc], text) + return viewable_comments +