646 lines
20 KiB
Python
646 lines
20 KiB
Python
import functools
|
|
import logging
|
|
import os.path
|
|
import random
|
|
import time
|
|
import urlparse
|
|
|
|
from django.contrib.auth.decorators import login_required
|
|
from django.contrib.auth.models import User
|
|
from django.core import exceptions
|
|
from django.http import Http404, HttpResponseBadRequest
|
|
from django.utils.translation import ugettext as _
|
|
from django.views.decorators import csrf
|
|
from django.views.decorators.http import require_GET, require_POST
|
|
from opaque_keys.edx.keys import CourseKey
|
|
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
|
|
|
from courseware.access import has_access
|
|
from util.file import store_uploaded_file
|
|
from courseware.courses import get_course_with_access, get_course_by_id
|
|
import django_comment_client.settings as cc_settings
|
|
from django_comment_client.utils import (
|
|
add_courseware_context,
|
|
get_annotated_content_info,
|
|
get_ability,
|
|
JsonError,
|
|
JsonResponse,
|
|
prepare_content,
|
|
get_group_id_for_comments_service,
|
|
get_discussion_categories_ids
|
|
)
|
|
from django_comment_client.permissions import check_permissions_by_view, cached_has_permission
|
|
import lms.lib.comment_client as cc
|
|
|
|
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
|
|
course_key = SlashSeparatedCourseKey.from_deprecated_string(kwargs['course_id'])
|
|
if check_permissions_by_view(request.user, course_key, fetch_content(), request.view_name):
|
|
return fn(request, *args, **kwargs)
|
|
else:
|
|
return JsonError("unauthorized", status=401)
|
|
return wrapper
|
|
|
|
|
|
def ajax_content_response(request, course_key, content):
|
|
user_info = cc.User.from_django_user(request.user).to_dict()
|
|
annotated_content_info = get_annotated_content_info(course_key, content, request.user, user_info)
|
|
return JsonResponse({
|
|
'content': prepare_content(content, course_key),
|
|
'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_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
|
course = get_course_with_access(request.user, 'load', course_key)
|
|
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_key.to_deprecated_string(),
|
|
user_id=request.user.id,
|
|
thread_type=post["thread_type"],
|
|
body=post["body"],
|
|
title=post["title"]
|
|
)
|
|
|
|
# Cohort the thread if required
|
|
try:
|
|
group_id = get_group_id_for_comments_service(request, course_key, commentable_id)
|
|
except ValueError:
|
|
return HttpResponseBadRequest("Invalid cohort id")
|
|
if group_id is not None:
|
|
thread.group_id = group_id
|
|
|
|
thread.save()
|
|
|
|
# patch for backward compatibility to comments service
|
|
if 'pinned' not 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_key, data)
|
|
else:
|
|
return JsonResponse(prepare_content(data, course_key))
|
|
|
|
|
|
@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"))
|
|
|
|
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
|
thread = cc.Thread.find(thread_id)
|
|
thread.body = request.POST["body"]
|
|
thread.title = request.POST["title"]
|
|
# The following checks should avoid issues we've seen during deploys, where end users are hitting an updated server
|
|
# while their browser still has the old client code. This will avoid erasing present values in those cases.
|
|
if "thread_type" in request.POST:
|
|
thread.thread_type = request.POST["thread_type"]
|
|
if "commentable_id" in request.POST:
|
|
course = get_course_with_access(request.user, 'load', course_key)
|
|
commentable_ids = get_discussion_categories_ids(course)
|
|
if request.POST.get("commentable_id") in commentable_ids:
|
|
thread.commentable_id = request.POST["commentable_id"]
|
|
else:
|
|
return JsonError(_("Topic doesn't exist"))
|
|
|
|
thread.save()
|
|
if request.is_ajax():
|
|
return ajax_content_response(request, course_key, thread.to_dict())
|
|
else:
|
|
return JsonResponse(prepare_content(thread.to_dict(), course_key))
|
|
|
|
|
|
def _create_comment(request, course_key, thread_id=None, parent_id=None):
|
|
"""
|
|
given a course_key, thread_id, and parent_id, create a comment,
|
|
called from create_comment to do the actual creation
|
|
"""
|
|
assert isinstance(course_key, CourseKey)
|
|
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, 'load', course_key)
|
|
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_key.to_deprecated_string(),
|
|
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_key, comment.to_dict())
|
|
else:
|
|
return JsonResponse(prepare_content(comment.to_dict(), course.id))
|
|
|
|
|
|
@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, SlashSeparatedCourseKey.from_deprecated_string(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
|
|
"""
|
|
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
|
thread = cc.Thread.find(thread_id)
|
|
thread.delete()
|
|
|
|
return JsonResponse(prepare_content(thread.to_dict(), course_key))
|
|
|
|
|
|
@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
|
|
"""
|
|
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
|
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_key, comment.to_dict())
|
|
else:
|
|
return JsonResponse(prepare_content(comment.to_dict(), course_key))
|
|
|
|
|
|
@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
|
|
"""
|
|
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
|
comment = cc.Comment.find(comment_id)
|
|
comment.endorsed = request.POST.get('endorsed', 'false').lower() == 'true'
|
|
comment.endorsement_user_id = request.user.id
|
|
comment.save()
|
|
return JsonResponse(prepare_content(comment.to_dict(), course_key))
|
|
|
|
|
|
@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
|
|
"""
|
|
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
|
thread = cc.Thread.find(thread_id)
|
|
thread.closed = request.POST.get('closed', 'false').lower() == 'true'
|
|
thread.save()
|
|
|
|
return JsonResponse({
|
|
'content': prepare_content(thread.to_dict(), course_key),
|
|
'ability': get_ability(course_key, thread.to_dict(), 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, SlashSeparatedCourseKey.from_deprecated_string(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
|
|
"""
|
|
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
|
comment = cc.Comment.find(comment_id)
|
|
comment.delete()
|
|
return JsonResponse(prepare_content(comment.to_dict(), course_key))
|
|
|
|
|
|
@require_POST
|
|
@login_required
|
|
@permitted
|
|
def vote_for_comment(request, course_id, comment_id, value):
|
|
"""
|
|
given a course_id and comment_id,
|
|
"""
|
|
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
|
user = cc.User.from_django_user(request.user)
|
|
comment = cc.Comment.find(comment_id)
|
|
user.vote(comment, value)
|
|
return JsonResponse(prepare_content(comment.to_dict(), course_key))
|
|
|
|
|
|
@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
|
|
"""
|
|
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
|
user = cc.User.from_django_user(request.user)
|
|
comment = cc.Comment.find(comment_id)
|
|
user.unvote(comment)
|
|
return JsonResponse(prepare_content(comment.to_dict(), course_key))
|
|
|
|
|
|
@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
|
|
"""
|
|
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
|
user = cc.User.from_django_user(request.user)
|
|
thread = cc.Thread.find(thread_id)
|
|
user.vote(thread, value)
|
|
|
|
return JsonResponse(prepare_content(thread.to_dict(), course_key))
|
|
|
|
|
|
@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
|
|
"""
|
|
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
|
user = cc.User.from_django_user(request.user)
|
|
thread = cc.Thread.find(thread_id)
|
|
thread.flagAbuse(user, thread)
|
|
|
|
return JsonResponse(prepare_content(thread.to_dict(), course_key))
|
|
|
|
|
|
@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_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
|
course = get_course_by_id(course_key)
|
|
thread = cc.Thread.find(thread_id)
|
|
remove_all = cached_has_permission(request.user, 'openclose_thread', course_key) or has_access(request.user, 'staff', course)
|
|
thread.unFlagAbuse(user, thread, remove_all)
|
|
|
|
return JsonResponse(prepare_content(thread.to_dict(), course_key))
|
|
|
|
|
|
@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
|
|
"""
|
|
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
|
user = cc.User.from_django_user(request.user)
|
|
comment = cc.Comment.find(comment_id)
|
|
comment.flagAbuse(user, comment)
|
|
return JsonResponse(prepare_content(comment.to_dict(), course_key))
|
|
|
|
|
|
@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_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
|
course = get_course_by_id(course_key)
|
|
remove_all = cached_has_permission(request.user, 'openclose_thread', course_key) or has_access(request.user, 'staff', course)
|
|
comment = cc.Comment.find(comment_id)
|
|
comment.unFlagAbuse(user, comment, remove_all)
|
|
return JsonResponse(prepare_content(comment.to_dict(), course_key))
|
|
|
|
|
|
@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
|
|
"""
|
|
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
|
user = cc.User.from_django_user(request.user)
|
|
thread = cc.Thread.find(thread_id)
|
|
user.unvote(thread)
|
|
|
|
return JsonResponse(prepare_content(thread.to_dict(), course_key))
|
|
|
|
|
|
@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
|
|
"""
|
|
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
|
user = cc.User.from_django_user(request.user)
|
|
thread = cc.Thread.find(thread_id)
|
|
thread.pin(user, thread_id)
|
|
|
|
return JsonResponse(prepare_content(thread.to_dict(), course_key))
|
|
|
|
|
|
@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
|
|
"""
|
|
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
|
user = cc.User.from_django_user(request.user)
|
|
thread = cc.Thread.find(thread_id)
|
|
thread.un_pin(user, thread_id)
|
|
|
|
return JsonResponse(prepare_content(thread.to_dict(), course_key))
|
|
|
|
|
|
@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
|
|
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()
|
|
|
|
base_file_name = str(time.time()).replace('.', str(random.randint(0, 100000)))
|
|
file_storage, new_file_name = store_uploaded_file(
|
|
request, 'file-upload', cc_settings.ALLOWED_UPLOAD_FILE_TYPES, base_file_name,
|
|
max_file_size=cc_settings.MAX_UPLOAD_FILE_SIZE
|
|
)
|
|
|
|
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,
|
|
}
|
|
})
|
|
|
|
|
|
@require_GET
|
|
@login_required
|
|
def users(request, course_id):
|
|
"""
|
|
Given a `username` query parameter, find matches for users in the forum for this course.
|
|
|
|
Only exact matches are supported here, so the length of the result set will either be 0 or 1.
|
|
"""
|
|
|
|
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
|
try:
|
|
course = get_course_with_access(request.user, 'load_forum', course_key)
|
|
except Http404:
|
|
# course didn't exist, or requesting user does not have access to it.
|
|
return JsonError(status=404)
|
|
|
|
try:
|
|
username = request.GET['username']
|
|
except KeyError:
|
|
# 400 is default status for JsonError
|
|
return JsonError(["username parameter is required"])
|
|
|
|
user_objs = []
|
|
try:
|
|
matched_user = User.objects.get(username=username)
|
|
cc_user = cc.User.from_django_user(matched_user)
|
|
cc_user.course_id = course_key
|
|
cc_user.retrieve(complete=False)
|
|
if (cc_user['threads_count'] + cc_user['comments_count']) > 0:
|
|
user_objs.append({
|
|
'id': matched_user.id,
|
|
'username': matched_user.username,
|
|
})
|
|
except User.DoesNotExist:
|
|
pass
|
|
return JsonResponse({"users": user_objs})
|