import time import random 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 from django.views.decorators import csrf from django.core.files.storage import get_storage_class from django.utils.translation import ugettext as _ 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 from xmodule.modulestore.locations import SlashSeparatedCourseKey from xmodule.modulestore.keys import CourseKey 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_id, 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_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) course = get_course_with_access(request.user, 'load', course_id) 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.to_deprecated_string(), 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, SlashSeparatedCourseKey.from_deprecated_string(course_id), thread.to_dict()) else: return JsonResponse(utils.safe_content(thread.to_dict())) def _create_comment(request, course_key, 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 """ 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(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, 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 """ 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, SlashSeparatedCourseKey.from_deprecated_string(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(SlashSeparatedCourseKey.from_deprecated_string(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, 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 """ 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_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) course = get_course_by_id(course_id) thread = cc.Thread.find(thread_id) remove_all = cached_has_permission(request.user, 'openclose_thread', course_id) or has_access(request.user, 'staff', course) thread.unFlagAbuse(user, thread, remove_all) 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_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(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, } })