try:
from django.core.validators import email_re
except ImportError:
# support for django pre 1.2 alpha 1
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, DECORATED_CREATORS
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,
'id_key' : comment.id_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)),
'category': comment.category,
'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.id_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 :
# GIB: Is there any good reasons to transform the name into lower case?
#name = name.lower().strip()
name = name.strip()
if email != None :
email = email.lower().strip()
title = request.POST['title'].strip()
content = request.POST['content'].strip()
tags = request.POST['tags']
category = request.POST.get('category', 0);
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, category, 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, category, 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 is latest (if boolean
#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
comment.category = category
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
# DIRTY : this function has no error check but anyway errors are not listened to client side
@has_perm_on_text("can_create_comment")
def own_notify(request, key):
email_or_user = None if request.user.is_anonymous() else request.user
if not email_or_user :
email_or_user = request.POST.get('email', None)
if email_or_user :
email_or_user = email_or_user.lower().strip()
active = (request.POST.get('active', False) == 'true')
text = Text.objects.get(key=key)
Notification.objects.set_notification(text=None, type='own', active=active, email_or_user=email_or_user)
ret = HttpResponse()
ret.status_code = 200
return ret
@has_perm_on_text("can_create_comment")
def add_comment(request, key, version_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, category, 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 = TextVersion.objects.get(key=version_key)
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, category = category, start_wrapper = start_wrapper, end_wrapper = end_wrapper, start_offset = start_offset, end_offset = end_offset, reply_to=reply_to)
ask_for_notification = True
if user :
workspace_notify_count = Notification.objects.filter(text=None,type='workspace',user=user, active=True).count()
text_notify_count = Notification.objects.filter(text=text,type='text',user=user, active=True).count()
if workspace_notify_count > 0 or text_notify_count > 0 :
ask_for_notification = False
if ask_for_notification :
ask_for_notification = ( None == Notification.objects.get_notifications(text=None, type='own', email_or_user=(user if user else email)))
ret['ask_for_notification'] = ask_for_notification
ret['email'] = '' if user else email
if text_version.mod_posteriori or has_perm(request, 'can_view_unapproved_comment', text=text) or has_perm(request, 'can_view_comment_own', 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(text=text, type='own', active=True, 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'))
if DECORATED_CREATORS:
names = list(allowed_comments.filter(user__isnull=False).values('name').annotate(nb_comments=Count('id'))) #.order_by('name'))
author = text_version.name
else:
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'))
has_author = User.objects.filter(id=text_version.user_id).values('username')
if has_author:
author = has_author[0]['username']
else:
author = ''
if request.GET.get('name', None):
me = request.GET.get('name', None)
else:
me = request.user.username
for name in names:
if name['name']:
if name['name'] == me:
name['display'] = _(u'me') + ' (' + name['name'] + ')'
elif name['name'] == author:
name['display'] = _(u'author') + ' (' + name['name'] + ')'
else:
name['display'] = name['name']
else:
name['display'] = ''
def sort_with_author_or_me_first(x, y):
if x and (x.startswith(_(u'me')) or x.startswith(_(u'author'))):
return -1
if y and (y.startswith(_(u'me')) or y.startswith(_(u'author'))):
return 1
else:
return cmp(x, y)
names.sort(cmp = sort_with_author_or_me_first, key = lambda obj:obj["display"])
# 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'))
# categories
categories = []
for category in [0, 1, 2, 3, 4, 5] :
categories.append({'cat' : category, 'nb_comments':allowed_comments.filter(category = category).count()})
# 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, 'categories':categories, '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