Previously, an error was raised if the comments service returned data including an unexpected field, which unnecessarily complicated the release path for new features, since the list of allowed fields would need to be modified before cs_comments_service could be modified, and only then could edx-platform take advantage of the new CS feature. We still log a warning if an unexpected field is returned, so we will still be able to tell if the CS returns a corrupt response. JIRA: FOR-180
600 lines
18 KiB
Python
600 lines
18 KiB
Python
import time
|
|
import random
|
|
import os
|
|
import os.path
|
|
import logging
|
|
import urlparse
|
|
import functools
|
|
|
|
import lms.lib.comment_client as cc
|
|
import django_comment_client.utils as utils
|
|
import django_comment_client.settings as cc_settings
|
|
|
|
|
|
from django.core import exceptions
|
|
from django.contrib.auth.decorators import login_required
|
|
from django.views.decorators.http import require_POST, require_GET
|
|
from django.views.decorators import csrf
|
|
from django.core.files.storage import get_storage_class
|
|
from django.utils.translation import ugettext as _
|
|
|
|
from edxmako.shortcuts import render_to_string
|
|
from courseware.courses import get_course_with_access, get_course_by_id
|
|
from course_groups.cohorts import get_cohort_id, is_commentable_cohorted
|
|
|
|
from django_comment_client.utils import JsonResponse, JsonError, extract, add_courseware_context
|
|
|
|
from django_comment_client.permissions import check_permissions_by_view, cached_has_permission
|
|
from courseware.access import has_access
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
def permitted(fn):
|
|
@functools.wraps(fn)
|
|
def wrapper(request, *args, **kwargs):
|
|
def fetch_content():
|
|
if "thread_id" in kwargs:
|
|
content = cc.Thread.find(kwargs["thread_id"]).to_dict()
|
|
elif "comment_id" in kwargs:
|
|
content = cc.Comment.find(kwargs["comment_id"]).to_dict()
|
|
else:
|
|
content = None
|
|
return content
|
|
if check_permissions_by_view(request.user, kwargs['course_id'], fetch_content(), request.view_name):
|
|
return fn(request, *args, **kwargs)
|
|
else:
|
|
return JsonError("unauthorized", status=401)
|
|
return wrapper
|
|
|
|
|
|
def ajax_content_response(request, course_id, content):
|
|
context = {
|
|
'course_id': course_id,
|
|
'content': content,
|
|
}
|
|
user_info = cc.User.from_django_user(request.user).to_dict()
|
|
annotated_content_info = utils.get_annotated_content_info(course_id, content, request.user, user_info)
|
|
return JsonResponse({
|
|
'content': utils.safe_content(content),
|
|
'annotated_content_info': annotated_content_info,
|
|
})
|
|
|
|
|
|
@require_POST
|
|
@login_required
|
|
@permitted
|
|
def create_thread(request, course_id, commentable_id):
|
|
"""
|
|
Given a course and commentble ID, create the thread
|
|
"""
|
|
|
|
log.debug("Creating new thread in %r, id %r", course_id, commentable_id)
|
|
course = get_course_with_access(request.user, course_id, 'load')
|
|
post = request.POST
|
|
|
|
if course.allow_anonymous:
|
|
anonymous = post.get('anonymous', 'false').lower() == 'true'
|
|
else:
|
|
anonymous = False
|
|
|
|
if course.allow_anonymous_to_peers:
|
|
anonymous_to_peers = post.get('anonymous_to_peers', 'false').lower() == 'true'
|
|
else:
|
|
anonymous_to_peers = False
|
|
|
|
if 'title' not in post or not post['title'].strip():
|
|
return JsonError(_("Title can't be empty"))
|
|
if 'body' not in post or not post['body'].strip():
|
|
return JsonError(_("Body can't be empty"))
|
|
|
|
thread = cc.Thread(
|
|
anonymous=anonymous,
|
|
anonymous_to_peers=anonymous_to_peers,
|
|
commentable_id=commentable_id,
|
|
course_id=course_id,
|
|
user_id=request.user.id,
|
|
body=post["body"],
|
|
title=post["title"]
|
|
)
|
|
|
|
user = cc.User.from_django_user(request.user)
|
|
|
|
#kevinchugh because the new requirement is that all groups will be determined
|
|
#by the group id in the request this all goes away
|
|
#not anymore, only for admins
|
|
|
|
# Cohort the thread if the commentable is cohorted.
|
|
if is_commentable_cohorted(course_id, commentable_id):
|
|
user_group_id = get_cohort_id(user, course_id)
|
|
|
|
# TODO (vshnayder): once we have more than just cohorts, we'll want to
|
|
# change this to a single get_group_for_user_and_commentable function
|
|
# that can do different things depending on the commentable_id
|
|
if cached_has_permission(request.user, "see_all_cohorts", course_id):
|
|
# admins can optionally choose what group to post as
|
|
group_id = post.get('group_id', user_group_id)
|
|
else:
|
|
# regular users always post with their own id.
|
|
group_id = user_group_id
|
|
|
|
if group_id:
|
|
thread.group_id = group_id
|
|
|
|
thread.save()
|
|
|
|
#patch for backward compatibility to comments service
|
|
if not 'pinned' in thread.attributes:
|
|
thread['pinned'] = False
|
|
|
|
if post.get('auto_subscribe', 'false').lower() == 'true':
|
|
user = cc.User.from_django_user(request.user)
|
|
user.follow(thread)
|
|
data = thread.to_dict()
|
|
add_courseware_context([data], course)
|
|
if request.is_ajax():
|
|
return ajax_content_response(request, course_id, data)
|
|
else:
|
|
return JsonResponse(utils.safe_content(data))
|
|
|
|
|
|
@require_POST
|
|
@login_required
|
|
@permitted
|
|
def update_thread(request, course_id, thread_id):
|
|
"""
|
|
Given a course id and thread id, update a existing thread, used for both static and ajax submissions
|
|
"""
|
|
if 'title' not in request.POST or not request.POST['title'].strip():
|
|
return JsonError(_("Title can't be empty"))
|
|
if 'body' not in request.POST or not request.POST['body'].strip():
|
|
return JsonError(_("Body can't be empty"))
|
|
thread = cc.Thread.find(thread_id)
|
|
thread.body = request.POST["body"]
|
|
thread.title = request.POST["title"]
|
|
thread.save()
|
|
if request.is_ajax():
|
|
return ajax_content_response(request, course_id, thread.to_dict())
|
|
else:
|
|
return JsonResponse(utils.safe_content(thread.to_dict()))
|
|
|
|
|
|
def _create_comment(request, course_id, thread_id=None, parent_id=None):
|
|
"""
|
|
given a course_id, thread_id, and parent_id, create a comment,
|
|
called from create_comment to do the actual creation
|
|
"""
|
|
post = request.POST
|
|
|
|
if 'body' not in post or not post['body'].strip():
|
|
return JsonError(_("Body can't be empty"))
|
|
|
|
course = get_course_with_access(request.user, course_id, 'load')
|
|
if course.allow_anonymous:
|
|
anonymous = post.get('anonymous', 'false').lower() == 'true'
|
|
else:
|
|
anonymous = False
|
|
|
|
if course.allow_anonymous_to_peers:
|
|
anonymous_to_peers = post.get('anonymous_to_peers', 'false').lower() == 'true'
|
|
else:
|
|
anonymous_to_peers = False
|
|
|
|
comment = cc.Comment(
|
|
anonymous=anonymous,
|
|
anonymous_to_peers=anonymous_to_peers,
|
|
user_id=request.user.id,
|
|
course_id=course_id,
|
|
thread_id=thread_id,
|
|
parent_id=parent_id,
|
|
body=post["body"]
|
|
)
|
|
comment.save()
|
|
if post.get('auto_subscribe', 'false').lower() == 'true':
|
|
user = cc.User.from_django_user(request.user)
|
|
user.follow(comment.thread)
|
|
if request.is_ajax():
|
|
return ajax_content_response(request, course_id, comment.to_dict())
|
|
else:
|
|
return JsonResponse(utils.safe_content(comment.to_dict()))
|
|
|
|
|
|
@require_POST
|
|
@login_required
|
|
@permitted
|
|
def create_comment(request, course_id, thread_id):
|
|
"""
|
|
given a course_id and thread_id, test for comment depth. if not too deep,
|
|
call _create_comment to create the actual comment.
|
|
"""
|
|
if cc_settings.MAX_COMMENT_DEPTH is not None:
|
|
if cc_settings.MAX_COMMENT_DEPTH < 0:
|
|
return JsonError(_("Comment level too deep"))
|
|
return _create_comment(request, course_id, thread_id=thread_id)
|
|
|
|
|
|
@require_POST
|
|
@login_required
|
|
@permitted
|
|
def delete_thread(request, course_id, thread_id):
|
|
"""
|
|
given a course_id and thread_id, delete this thread
|
|
this is ajax only
|
|
"""
|
|
thread = cc.Thread.find(thread_id)
|
|
thread.delete()
|
|
return JsonResponse(utils.safe_content(thread.to_dict()))
|
|
|
|
|
|
@require_POST
|
|
@login_required
|
|
@permitted
|
|
def update_comment(request, course_id, comment_id):
|
|
"""
|
|
given a course_id and comment_id, update the comment with payload attributes
|
|
handles static and ajax submissions
|
|
"""
|
|
comment = cc.Comment.find(comment_id)
|
|
if 'body' not in request.POST or not request.POST['body'].strip():
|
|
return JsonError(_("Body can't be empty"))
|
|
comment.body = request.POST["body"]
|
|
comment.save()
|
|
if request.is_ajax():
|
|
return ajax_content_response(request, course_id, comment.to_dict())
|
|
else:
|
|
return JsonResponse(utils.safe_content(comment.to_dict()))
|
|
|
|
|
|
@require_POST
|
|
@login_required
|
|
@permitted
|
|
def endorse_comment(request, course_id, comment_id):
|
|
"""
|
|
given a course_id and comment_id, toggle the endorsement of this comment,
|
|
ajax only
|
|
"""
|
|
comment = cc.Comment.find(comment_id)
|
|
comment.endorsed = request.POST.get('endorsed', 'false').lower() == 'true'
|
|
comment.save()
|
|
return JsonResponse(utils.safe_content(comment.to_dict()))
|
|
|
|
|
|
@require_POST
|
|
@login_required
|
|
@permitted
|
|
def openclose_thread(request, course_id, thread_id):
|
|
"""
|
|
given a course_id and thread_id, toggle the status of this thread
|
|
ajax only
|
|
"""
|
|
thread = cc.Thread.find(thread_id)
|
|
thread.closed = request.POST.get('closed', 'false').lower() == 'true'
|
|
thread.save()
|
|
thread = thread.to_dict()
|
|
return JsonResponse({
|
|
'content': utils.safe_content(thread),
|
|
'ability': utils.get_ability(course_id, thread, request.user),
|
|
})
|
|
|
|
|
|
@require_POST
|
|
@login_required
|
|
@permitted
|
|
def create_sub_comment(request, course_id, comment_id):
|
|
"""
|
|
given a course_id and comment_id, create a response to a comment
|
|
after checking the max depth allowed, if allowed
|
|
"""
|
|
if cc_settings.MAX_COMMENT_DEPTH is not None:
|
|
if cc_settings.MAX_COMMENT_DEPTH <= cc.Comment.find(comment_id).depth:
|
|
return JsonError(_("Comment level too deep"))
|
|
return _create_comment(request, course_id, parent_id=comment_id)
|
|
|
|
|
|
@require_POST
|
|
@login_required
|
|
@permitted
|
|
def delete_comment(request, course_id, comment_id):
|
|
"""
|
|
given a course_id and comment_id delete this comment
|
|
ajax only
|
|
"""
|
|
comment = cc.Comment.find(comment_id)
|
|
comment.delete()
|
|
return JsonResponse(utils.safe_content(comment.to_dict()))
|
|
|
|
|
|
@require_POST
|
|
@login_required
|
|
@permitted
|
|
def vote_for_comment(request, course_id, comment_id, value):
|
|
"""
|
|
given a course_id and comment_id,
|
|
"""
|
|
user = cc.User.from_django_user(request.user)
|
|
comment = cc.Comment.find(comment_id)
|
|
user.vote(comment, value)
|
|
return JsonResponse(utils.safe_content(comment.to_dict()))
|
|
|
|
|
|
@require_POST
|
|
@login_required
|
|
@permitted
|
|
def undo_vote_for_comment(request, course_id, comment_id):
|
|
"""
|
|
given a course id and comment id, remove vote
|
|
ajax only
|
|
"""
|
|
user = cc.User.from_django_user(request.user)
|
|
comment = cc.Comment.find(comment_id)
|
|
user.unvote(comment)
|
|
return JsonResponse(utils.safe_content(comment.to_dict()))
|
|
|
|
|
|
@require_POST
|
|
@login_required
|
|
@permitted
|
|
def vote_for_thread(request, course_id, thread_id, value):
|
|
"""
|
|
given a course id and thread id vote for this thread
|
|
ajax only
|
|
"""
|
|
user = cc.User.from_django_user(request.user)
|
|
thread = cc.Thread.find(thread_id)
|
|
user.vote(thread, value)
|
|
return JsonResponse(utils.safe_content(thread.to_dict()))
|
|
|
|
|
|
@require_POST
|
|
@login_required
|
|
@permitted
|
|
def flag_abuse_for_thread(request, course_id, thread_id):
|
|
"""
|
|
given a course_id and thread_id flag this thread for abuse
|
|
ajax only
|
|
"""
|
|
user = cc.User.from_django_user(request.user)
|
|
thread = cc.Thread.find(thread_id)
|
|
thread.flagAbuse(user, thread)
|
|
return JsonResponse(utils.safe_content(thread.to_dict()))
|
|
|
|
|
|
@require_POST
|
|
@login_required
|
|
@permitted
|
|
def un_flag_abuse_for_thread(request, course_id, thread_id):
|
|
"""
|
|
given a course id and thread id, remove abuse flag for this thread
|
|
ajax only
|
|
"""
|
|
user = cc.User.from_django_user(request.user)
|
|
course = get_course_by_id(course_id)
|
|
thread = cc.Thread.find(thread_id)
|
|
removeAll = cached_has_permission(request.user, 'openclose_thread', course_id) or has_access(request.user, course, 'staff')
|
|
thread.unFlagAbuse(user, thread, removeAll)
|
|
return JsonResponse(utils.safe_content(thread.to_dict()))
|
|
|
|
|
|
@require_POST
|
|
@login_required
|
|
@permitted
|
|
def flag_abuse_for_comment(request, course_id, comment_id):
|
|
"""
|
|
given a course and comment id, flag comment for abuse
|
|
ajax only
|
|
"""
|
|
user = cc.User.from_django_user(request.user)
|
|
comment = cc.Comment.find(comment_id)
|
|
comment.flagAbuse(user, comment)
|
|
return JsonResponse(utils.safe_content(comment.to_dict()))
|
|
|
|
|
|
@require_POST
|
|
@login_required
|
|
@permitted
|
|
def un_flag_abuse_for_comment(request, course_id, comment_id):
|
|
"""
|
|
given a course_id and comment id, unflag comment for abuse
|
|
ajax only
|
|
"""
|
|
user = cc.User.from_django_user(request.user)
|
|
course = get_course_by_id(course_id)
|
|
removeAll = cached_has_permission(request.user, 'openclose_thread', course_id) or has_access(request.user, course, 'staff')
|
|
comment = cc.Comment.find(comment_id)
|
|
comment.unFlagAbuse(user, comment, removeAll)
|
|
return JsonResponse(utils.safe_content(comment.to_dict()))
|
|
|
|
|
|
@require_POST
|
|
@login_required
|
|
@permitted
|
|
def undo_vote_for_thread(request, course_id, thread_id):
|
|
"""
|
|
given a course id and thread id, remove users vote for thread
|
|
ajax only
|
|
"""
|
|
user = cc.User.from_django_user(request.user)
|
|
thread = cc.Thread.find(thread_id)
|
|
user.unvote(thread)
|
|
return JsonResponse(utils.safe_content(thread.to_dict()))
|
|
|
|
|
|
@require_POST
|
|
@login_required
|
|
@permitted
|
|
def pin_thread(request, course_id, thread_id):
|
|
"""
|
|
given a course id and thread id, pin this thread
|
|
ajax only
|
|
"""
|
|
user = cc.User.from_django_user(request.user)
|
|
thread = cc.Thread.find(thread_id)
|
|
thread.pin(user, thread_id)
|
|
return JsonResponse(utils.safe_content(thread.to_dict()))
|
|
|
|
|
|
@require_POST
|
|
@login_required
|
|
@permitted
|
|
def un_pin_thread(request, course_id, thread_id):
|
|
"""
|
|
given a course id and thread id, remove pin from this thread
|
|
ajax only
|
|
"""
|
|
user = cc.User.from_django_user(request.user)
|
|
thread = cc.Thread.find(thread_id)
|
|
thread.un_pin(user, thread_id)
|
|
return JsonResponse(utils.safe_content(thread.to_dict()))
|
|
|
|
|
|
@require_POST
|
|
@login_required
|
|
@permitted
|
|
def follow_thread(request, course_id, thread_id):
|
|
user = cc.User.from_django_user(request.user)
|
|
thread = cc.Thread.find(thread_id)
|
|
user.follow(thread)
|
|
return JsonResponse({})
|
|
|
|
|
|
@require_POST
|
|
@login_required
|
|
@permitted
|
|
def follow_commentable(request, course_id, commentable_id):
|
|
"""
|
|
given a course_id and commentable id, follow this commentable
|
|
ajax only
|
|
"""
|
|
user = cc.User.from_django_user(request.user)
|
|
commentable = cc.Commentable.find(commentable_id)
|
|
user.follow(commentable)
|
|
return JsonResponse({})
|
|
|
|
|
|
@require_POST
|
|
@login_required
|
|
@permitted
|
|
def follow_user(request, course_id, followed_user_id):
|
|
user = cc.User.from_django_user(request.user)
|
|
followed_user = cc.User.find(followed_user_id)
|
|
user.follow(followed_user)
|
|
return JsonResponse({})
|
|
|
|
|
|
@require_POST
|
|
@login_required
|
|
@permitted
|
|
def unfollow_thread(request, course_id, thread_id):
|
|
"""
|
|
given a course id and thread id, stop following this thread
|
|
ajax only
|
|
"""
|
|
user = cc.User.from_django_user(request.user)
|
|
thread = cc.Thread.find(thread_id)
|
|
user.unfollow(thread)
|
|
return JsonResponse({})
|
|
|
|
|
|
@require_POST
|
|
@login_required
|
|
@permitted
|
|
def unfollow_commentable(request, course_id, commentable_id):
|
|
"""
|
|
given a course id and commentable id stop following commentable
|
|
ajax only
|
|
"""
|
|
user = cc.User.from_django_user(request.user)
|
|
commentable = cc.Commentable.find(commentable_id)
|
|
user.unfollow(commentable)
|
|
return JsonResponse({})
|
|
|
|
|
|
@require_POST
|
|
@login_required
|
|
@permitted
|
|
def unfollow_user(request, course_id, followed_user_id):
|
|
"""
|
|
given a course id and user id, stop following this user
|
|
ajax only
|
|
"""
|
|
user = cc.User.from_django_user(request.user)
|
|
followed_user = cc.User.find(followed_user_id)
|
|
user.unfollow(followed_user)
|
|
return JsonResponse({})
|
|
|
|
|
|
@require_POST
|
|
@login_required
|
|
@csrf.csrf_exempt
|
|
def upload(request, course_id): # ajax upload file to a question or answer
|
|
"""view that handles file upload via Ajax
|
|
"""
|
|
|
|
# check upload permission
|
|
result = ''
|
|
error = ''
|
|
new_file_name = ''
|
|
try:
|
|
# TODO authorization
|
|
#may raise exceptions.PermissionDenied
|
|
#if request.user.is_anonymous():
|
|
# msg = _('Sorry, anonymous users cannot upload files')
|
|
# raise exceptions.PermissionDenied(msg)
|
|
|
|
#request.user.assert_can_upload_file()
|
|
|
|
# check file type
|
|
f = request.FILES['file-upload']
|
|
file_extension = os.path.splitext(f.name)[1].lower()
|
|
if not file_extension in cc_settings.ALLOWED_UPLOAD_FILE_TYPES:
|
|
file_types = "', '".join(cc_settings.ALLOWED_UPLOAD_FILE_TYPES)
|
|
msg = _("allowed file types are '%(file_types)s'") % \
|
|
{'file_types': file_types}
|
|
raise exceptions.PermissionDenied(msg)
|
|
|
|
# generate new file name
|
|
new_file_name = str(time.time()).replace('.', str(random.randint(0, 100000))) + file_extension
|
|
|
|
file_storage = get_storage_class()()
|
|
# use default storage to store file
|
|
file_storage.save(new_file_name, f)
|
|
# check file size
|
|
# byte
|
|
size = file_storage.size(new_file_name)
|
|
if size > cc_settings.MAX_UPLOAD_FILE_SIZE:
|
|
file_storage.delete(new_file_name)
|
|
msg = _("maximum upload file size is %(file_size)sK") % \
|
|
{'file_size': cc_settings.MAX_UPLOAD_FILE_SIZE}
|
|
raise exceptions.PermissionDenied(msg)
|
|
|
|
except exceptions.PermissionDenied, err:
|
|
error = unicode(err)
|
|
except Exception, err:
|
|
print err
|
|
logging.critical(unicode(err))
|
|
error = _('Error uploading file. Please contact the site administrator. Thank you.')
|
|
|
|
if error == '':
|
|
result = _('Good')
|
|
file_url = file_storage.url(new_file_name)
|
|
parsed_url = urlparse.urlparse(file_url)
|
|
file_url = urlparse.urlunparse(
|
|
urlparse.ParseResult(
|
|
parsed_url.scheme,
|
|
parsed_url.netloc,
|
|
parsed_url.path,
|
|
'', '', ''
|
|
)
|
|
)
|
|
else:
|
|
result = ''
|
|
file_url = ''
|
|
|
|
return JsonResponse({
|
|
'result': {
|
|
'msg': result,
|
|
'error': error,
|
|
'file_url': file_url,
|
|
}
|
|
})
|