diff --git a/lms/djangoapps/django_comment_client/base/views.py b/lms/djangoapps/django_comment_client/base/views.py index 9173c205e0..fa6fc5803b 100644 --- a/lms/djangoapps/django_comment_client/base/views.py +++ b/lms/djangoapps/django_comment_client/base/views.py @@ -18,62 +18,27 @@ from django.conf import settings from mitxmako.shortcuts import render_to_response, render_to_string from django_comment_client.utils import JsonResponse, JsonError, extract -from django_comment_client.permissions import has_permission, has_permission +from django_comment_client.permissions import check_permissions_by_view import functools -# -def permitted(*per): - """ - Accepts a list of permissions and proceed if any of the permission is valid. - Note that @permitted("can_view", "can_edit") will proceed if the user has either - "can_view" or "can_edit" permission. To use AND operator in between, wrap them in - a list: - @permitted(["can_view", "can_edit"]) - - Special conditions can be used like permissions, e.g. - @permitted(["can_vote", "open"]) # where open is True if not content['closed'] - """ - def decorator(fn): - @functools.wraps(fn) - def wrapper(request, *args, **kwargs): - permissions = filter(lambda x: len(x), list(per)) - user = request.user - import pdb; pdb.set_trace() - - def fetch_content(): - if "thread_id" in kwargs: - content = comment_client.get_thread(kwargs["thread_id"]) - elif "comment_id" in kwargs: - content = comment_client.get_comment(kwargs["comment_id"]) - else: - logging.warning("missing thread_id or comment_id") - return None - return content - - def test_permission(user, permission, operator="or"): - if isinstance(permission, basestring): - if permission == "": - return True - elif permission == "author": - return fetch_content()["user_id"] == request.user.id - elif permission == "open": - return not fetch_content()["closed"] - return has_permission(user, permission) - elif isinstance(permission, list) and operator in ["and", "or"]: - results = [test_permission(user, x, operator="and") for x in permission] - if operator == "or": - return True in results - elif operator == "and": - return not False in results - - if test_permission(user, permissions, operator="or"): - return fn(request, *args, **kwargs) +def permitted(fn): + @functools.wraps(fn) + def wrapper(request, *args, **kwargs): + def fetch_content(): + if "thread_id" in kwargs: + content = comment_client.get_thread(kwargs["thread_id"]) + elif "comment_id" in kwargs: + content = comment_client.get_comment(kwargs["comment_id"]) else: - return JsonError("unauthorized") + content = None + return content - return wrapper - return decorator + if check_permissions_by_view(request.user, fetch_content(), request.view_name): + return fn(request, *args, **kwargs) + else: + return JsonError("unauthorized") + return wrapper def thread_author_only(fn): @@ -106,7 +71,7 @@ def instructor_only(fn): @require_POST @login_required -@permitted("create_thread") +@permitted def create_thread(request, course_id, commentable_id): attributes = extract(request.POST, ['body', 'title', 'tags']) attributes['user_id'] = request.user.id @@ -131,7 +96,7 @@ def create_thread(request, course_id, commentable_id): @require_POST @login_required -@permitted("edit_content", ["update_thread", "open", "author"]) +@permitted def update_thread(request, course_id, thread_id): attributes = extract(request.POST, ['body', 'title', 'tags']) response = comment_client.update_thread(thread_id, attributes) @@ -171,7 +136,7 @@ def _create_comment(request, course_id, _response_from_attributes): @require_POST @login_required -@permitted(["create_comment", "open"]) +@permitted def create_comment(request, course_id, thread_id): def _response_from_attributes(attributes): return comment_client.create_comment(thread_id, attributes) @@ -179,14 +144,14 @@ def create_comment(request, course_id, thread_id): @require_POST @login_required -@permitted("delete_thread") +@permitted def delete_thread(request, course_id, thread_id): response = comment_client.delete_thread(thread_id) return JsonResponse(response) @require_POST @login_required -@permitted("update_comment", ["update_comment", "open", "author"]) +@permitted def update_comment(request, course_id, comment_id): attributes = extract(request.POST, ['body']) response = comment_client.update_comment(comment_id, attributes) @@ -205,7 +170,7 @@ def update_comment(request, course_id, comment_id): @require_POST @login_required -@permitted("endorse_comment") +@permitted def endorse_comment(request, course_id, comment_id): attributes = extract(request.POST, ['endorsed']) response = comment_client.update_comment(comment_id, attributes) @@ -213,7 +178,7 @@ def endorse_comment(request, course_id, comment_id): @require_POST @login_required -@permitted("openclose_thread") +@permitted def openclose_thread(request, course_id, thread_id): attributes = extract(request.POST, ['closed']) response = comment_client.update_thread(thread_id, attributes) @@ -221,7 +186,7 @@ def openclose_thread(request, course_id, thread_id): @require_POST @login_required -@permitted(["create_sub_comment", "open"]) +@permitted def create_sub_comment(request, course_id, comment_id): def _response_from_attributes(attributes): return comment_client.create_sub_comment(comment_id, attributes) @@ -229,14 +194,14 @@ def create_sub_comment(request, course_id, comment_id): @require_POST @login_required -@permitted("delete_comment") +@permitted def delete_comment(request, course_id, comment_id): response = comment_client.delete_comment(comment_id) return JsonResponse(response) @require_POST @login_required -@permitted(["vote", "open"]) +@permitted def vote_for_comment(request, course_id, comment_id, value): user_id = request.user.id response = comment_client.vote_for_comment(comment_id, user_id, value) @@ -244,7 +209,7 @@ def vote_for_comment(request, course_id, comment_id, value): @require_POST @login_required -@permitted(["unvote", "open"]) +@permitted def undo_vote_for_comment(request, course_id, comment_id): user_id = request.user.id response = comment_client.undo_vote_for_comment(comment_id, user_id) @@ -252,7 +217,7 @@ def undo_vote_for_comment(request, course_id, comment_id): @require_POST @login_required -@permitted(["vote", "open"]) +@permitted def vote_for_thread(request, course_id, thread_id, value): user_id = request.user.id response = comment_client.vote_for_thread(thread_id, user_id, value) @@ -260,7 +225,7 @@ def vote_for_thread(request, course_id, thread_id, value): @require_POST @login_required -@permitted(["unvote", "open"]) +@permitted def undo_vote_for_thread(request, course_id, thread_id): user_id = request.user.id response = comment_client.undo_vote_for_thread(thread_id, user_id) @@ -268,7 +233,7 @@ def undo_vote_for_thread(request, course_id, thread_id): @require_POST @login_required -@permitted("follow_thread") +@permitted def follow_thread(request, course_id, thread_id): user_id = request.user.id response = comment_client.subscribe_thread(user_id, thread_id) @@ -276,7 +241,7 @@ def follow_thread(request, course_id, thread_id): @require_POST @login_required -@permitted("follow_commentable") +@permitted def follow_commentable(request, course_id, commentable_id): user_id = request.user.id response = comment_client.subscribe_commentable(user_id, commentable_id) @@ -284,7 +249,7 @@ def follow_commentable(request, course_id, commentable_id): @require_POST @login_required -@permitted("follow_user") +@permitted def follow_user(request, course_id, followed_user_id): user_id = request.user.id response = comment_client.follow(user_id, followed_user_id) @@ -292,7 +257,7 @@ def follow_user(request, course_id, followed_user_id): @require_POST @login_required -@permitted("unfollow_thread") +@permitted def unfollow_thread(request, course_id, thread_id): user_id = request.user.id response = comment_client.unsubscribe_thread(user_id, thread_id) @@ -300,7 +265,7 @@ def unfollow_thread(request, course_id, thread_id): @require_POST @login_required -@permitted("unfollow_commentable") +@permitted def unfollow_commentable(request, course_id, commentable_id): user_id = request.user.id response = comment_client.unsubscribe_commentable(user_id, commentable_id) @@ -308,7 +273,7 @@ def unfollow_commentable(request, course_id, commentable_id): @require_POST @login_required -@permitted("unfollow_user") +@permitted def unfollow_user(request, course_id, followed_user_id): user_id = request.user.id response = comment_client.unfollow(user_id, followed_user_id) diff --git a/lms/djangoapps/django_comment_client/forum/views.py b/lms/djangoapps/django_comment_client/forum/views.py index 69f426055f..859e3805b0 100644 --- a/lms/djangoapps/django_comment_client/forum/views.py +++ b/lms/djangoapps/django_comment_client/forum/views.py @@ -18,6 +18,7 @@ import json import comment_client import dateutil +from django_comment_client.permissions import check_permissions_by_view THREADS_PER_PAGE = 5 PAGES_NEARBY_DELTA = 2 @@ -48,7 +49,7 @@ def render_discussion(request, course_id, threads, discussion_id=None, \ 'forum': (lambda: reverse('django_comment_client.forum.views.forum_form_discussion', args=[course_id, discussion_id])), }[discussion_type]() - annotated_content_info = {thread['id']: get_annotated_content_info(thread, request.user.id) for thread in threads} + annotated_content_info = {thread['id']: get_annotated_content_info(thread, request.user, is_thread=True) for thread in threads} context = { 'threads': threads, @@ -127,17 +128,18 @@ def forum_form_discussion(request, course_id, discussion_id): return render_to_response('discussion/index.html', context) -def get_annotated_content_info(content, user_id): +def get_annotated_content_info(content, user, is_thread): return { - 'editable': str(content['user_id']) == str(user_id), # TODO may relax this to instructors + 'editable': check_permissions_by_view(user, content, "update_thread" if is_thread else "update_comment"), + 'can_reply': check_permissions_by_view(user, content, "create_comment" if is_thread else "create_sub_comment"), } -def get_annotated_content_infos(thread, user_id): +def get_annotated_content_infos(thread, user): infos = {} - def _annotate(content): - infos[str(content['id'])] = get_annotated_content_info(content, user_id) + def _annotate(content, is_thread=True): + infos[str(content['id'])] = get_annotated_content_info(content, user, is_thread) for child in content.get('children', []): - _annotate(child) + _annotate(child, is_thread=False) _annotate(thread) return infos @@ -146,7 +148,7 @@ def render_single_thread(request, course_id, thread_id): thread = comment_client.get_thread(thread_id, recursive=True) annotated_content_info = get_annotated_content_infos(thread=thread, \ - user_id=request.user.id) + user=request.user, is_thread=True) context = { 'thread': thread, @@ -162,8 +164,7 @@ def single_thread(request, course_id, discussion_id, thread_id): if request.is_ajax(): thread = comment_client.get_thread(thread_id, recursive=True) - annotated_content_info = get_annotated_content_infos(thread=thread, \ - user_id=request.user.id) + annotated_content_info = get_annotated_content_infos(thread, request.user) context = {'thread': thread} html = render_to_string('discussion/_ajax_single_thread.html', context) diff --git a/lms/djangoapps/django_comment_client/permissions.py b/lms/djangoapps/django_comment_client/permissions.py index 3bd69969cb..7049bf7dbb 100644 --- a/lms/djangoapps/django_comment_client/permissions.py +++ b/lms/djangoapps/django_comment_client/permissions.py @@ -34,11 +34,76 @@ def assign_default_role(sender, instance, **kwargs): logging.info("assign_default_role: adding %s as %s" % (instance, role)) instance.roles.add(role) + +def check_permissions(user, content, per): + """ + Accepts a list of permissions and proceed if any of the permission is valid. + Note that check_permissions("can_view", "can_edit") will proceed if the user has either + "can_view" or "can_edit" permission. To use AND operator in between, wrap them in + a list: + check_permissions(["can_view", "can_edit"]) + + Special conditions can be used like permissions, e.g. + (["can_vote", "open"]) # where open is True if not content['closed'] + """ + permissions = filter(lambda x: len(x), list(per)) + + def test_permission(user, permission, operator="or"): + if isinstance(permission, basestring): + # import pdb; pdb.set_trace() + if permission == "": + return True + elif permission == "author": + return content["user_id"] == str(user.id) + elif permission == "open": + return not content["closed"] + return has_permission(user, permission) + elif isinstance(permission, list) and operator in ["and", "or"]: + results = [test_permission(user, x, operator="and") for x in permission] + if operator == "or": + return True in results + elif operator == "and": + return not False in results + + return test_permission(user, permissions, operator="or") + + +VIEW_PERMISSIONS = { + 'update_thread' : ('edit_content', ['update_thread', 'open', 'author']), + 'create_comment' : (["create_comment", "open"]), + 'delete_thread' : ('delete_thread'), + 'update_comment' : ('edit_content', ['update_comment', 'open', 'author']), + 'endorse_comment' : ('endorse_comment'), + 'openclose_thread' : ('openclose_thread'), + 'create_sub_comment': (['create_sub_comment', 'open']), + 'delete_comment' : ('delete_comment'), + 'vote_for_commend' : (['vote', 'open']), + 'undo_vote_for_comment': (['unvote', 'open']), + 'vote_for_thread' : (['vote', 'open']), + 'undo_vote_for_thread': (['unvote', 'open']), + 'follow_thread' : ('follow_thread'), + 'follow_commentable': ('follow_commentable'), + 'follow_user' : ('follow_user'), + 'unfollow_thread' : ('unfollow_thread'), + 'unfollow_commentable': ('unfollow_commentable'), + 'unfollow_user' : ('unfollow_user'), + 'create_thread' : ('create_thread'), +} + +def check_permissions_by_view(user, content, name): + try: + p = VIEW_PERMISSIONS[name] + except KeyError: + logging.warning("Permission for view named %s does not exist in permissions.py" % name) + permissions = list((p, ) if isinstance(p, basestring) else p) + return check_permissions(user, content, permissions) + + moderator_role = Role.register("Moderator") student_role = Role.register("Student") moderator_role.register_permissions(["edit_content", "delete_thread", "openclose_thread", - "update_thread", "endorse_comment", "delete_comment"]) + "endorse_comment", "delete_comment"]) student_role.register_permissions(["vote", "update_thread", "follow_thread", "unfollow_thread", "update_comment", "create_sub_comment", "unvote" , "create_thread", "follow_commentable", "unfollow_commentable", "create_comment", ]) diff --git a/lms/djangoapps/django_comment_client/utils.py b/lms/djangoapps/django_comment_client/utils.py index dc78802564..b2e6758b6b 100644 --- a/lms/djangoapps/django_comment_client/utils.py +++ b/lms/djangoapps/django_comment_client/utils.py @@ -120,3 +120,7 @@ class JsonError(HttpResponse): class HtmlResponse(HttpResponse): def __init__(self, html=''): super(HtmlResponse, self).__init__(html, content_type='text/plain') + +class ViewNameMiddleware(object): + def process_view(self, request, view_func, view_args, view_kwargs): + request.view_name = view_func.__name__ diff --git a/lms/envs/common.py b/lms/envs/common.py index 2c7655b183..defae7cb12 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -294,6 +294,8 @@ MIDDLEWARE_CLASSES = ( 'askbot.middleware.spaceless.SpacelessMiddleware', # 'askbot.middleware.pagesize.QuestionsPageSizeMiddleware', # 'debug_toolbar.middleware.DebugToolbarMiddleware', + + 'django_comment_client.utils.ViewNameMiddleware', ) ############################### Pipeline ####################################### diff --git a/lms/static/coffee/src/discussion/content.coffee b/lms/static/coffee/src/discussion/content.coffee index b195514afa..2c0754e046 100644 --- a/lms/static/coffee/src/discussion/content.coffee +++ b/lms/static/coffee/src/discussion/content.coffee @@ -78,6 +78,7 @@ initializeFollowThread = (thread) -> $comment = $(response.html) $content.children(".comments").prepend($comment) Discussion.setWmdContent $content, $local, "reply-body", "" + Discussion.setContentInfo response.content['id'], 'can_reply', true Discussion.setContentInfo response.content['id'], 'editable', true Discussion.initializeContent($comment) Discussion.bindContentEvents($comment) @@ -321,3 +322,5 @@ initializeFollowThread = (thread) -> id = $content.attr("_id") if not Discussion.getContentInfo id, 'editable' $local(".discussion-edit").remove() + if not Discussion.getContentInfo id, 'can_reply' + $local(".discussion-reply").remove()