diff --git a/lms/djangoapps/django_comment_client/base/views.py b/lms/djangoapps/django_comment_client/base/views.py index d921198b16..c21fa6a3f3 100644 --- a/lms/djangoapps/django_comment_client/base/views.py +++ b/lms/djangoapps/django_comment_client/base/views.py @@ -34,7 +34,7 @@ def permitted(fn): content = None return content - if check_permissions_by_view(request.user, fetch_content(), request.view_name): + if check_permissions_by_view(request.user, kwargs['course_id'], fetch_content(), request.view_name): return fn(request, *args, **kwargs) else: return JsonError("unauthorized") diff --git a/lms/djangoapps/django_comment_client/forum/views.py b/lms/djangoapps/django_comment_client/forum/views.py index 565f683a60..b8f767e8bd 100644 --- a/lms/djangoapps/django_comment_client/forum/views.py +++ b/lms/djangoapps/django_comment_client/forum/views.py @@ -54,7 +54,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, is_thread=True) for thread in threads} + annotated_content_info = {thread['id']: get_annotated_content_info(course_id, thread, request.user, is_thread=True) for thread in threads} context = { 'threads': threads, @@ -148,18 +148,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, is_thread): +def get_annotated_content_info(course_id, content, user, is_thread): return { - '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"), - 'can_endorse': check_permissions_by_view(user, content, "endorse_comment") if not is_thread else False, - 'can_delete': check_permissions_by_view(user, content, "delete_thread" if is_thread else "delete_comment"), + 'editable': check_permissions_by_view(user, course_id, content, "update_thread" if is_thread else "update_comment"), + 'can_reply': check_permissions_by_view(user, course_id, content, "create_comment" if is_thread else "create_sub_comment"), + 'can_endorse': check_permissions_by_view(user, course_id, content, "endorse_comment") if not is_thread else False, + 'can_delete': check_permissions_by_view(user, course_id, content, "delete_thread" if is_thread else "delete_comment"), } -def get_annotated_content_infos(thread, user, is_thread=True): +def get_annotated_content_infos(course_id, thread, user, is_thread=True): infos = {} def _annotate(content, is_thread=is_thread): - infos[str(content['id'])] = get_annotated_content_info(content, user, is_thread) + infos[str(content['id'])] = get_annotated_content_info(course_id, content, user, is_thread) for child in content.get('children', []): _annotate(child, is_thread=False) _annotate(thread) @@ -169,7 +169,7 @@ def render_single_thread(request, discussion_id, course_id, thread_id): thread = comment_client.get_thread(thread_id, recursive=True) - annotated_content_info = get_annotated_content_infos(thread=thread, \ + annotated_content_info = get_annotated_content_infos(course_id, thread=thread, \ user=request.user, is_thread=True) context = { @@ -187,7 +187,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, request.user) + annotated_content_info = get_annotated_content_infos(course_id, thread, request.user) context = {'thread': thread} html = render_to_string('discussion/_ajax_single_thread.html', context) diff --git a/lms/djangoapps/django_comment_client/management/__init__.py b/lms/djangoapps/django_comment_client/management/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/django_comment_client/management/commands/__init__.py b/lms/djangoapps/django_comment_client/management/commands/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/django_comment_client/management/commands/assign_role.py b/lms/djangoapps/django_comment_client/management/commands/assign_role.py new file mode 100644 index 0000000000..82daa34622 --- /dev/null +++ b/lms/djangoapps/django_comment_client/management/commands/assign_role.py @@ -0,0 +1,18 @@ +from django.core.management.base import BaseCommand, CommandError +from django_comment_client.models import Permission, Role +from django.contrib.auth.models import User + + +class Command(BaseCommand): + args = 'user role course_id' + help = 'Assign a role to a user' + + def handle(self, *args, **options): + role = Role.objects.get(name=args[1], course_id=args[2]) + + if '@' in args[0]: + user = User.objects.get(email=args[0]) + else: + user = User.objects.get(username=args[0]) + + user.roles.add(role) \ No newline at end of file diff --git a/lms/djangoapps/django_comment_client/management/commands/seed_permissions_roles.py b/lms/djangoapps/django_comment_client/management/commands/seed_permissions_roles.py new file mode 100644 index 0000000000..fd09e97d27 --- /dev/null +++ b/lms/djangoapps/django_comment_client/management/commands/seed_permissions_roles.py @@ -0,0 +1,22 @@ +from django.core.management.base import BaseCommand, CommandError +from django_comment_client.models import Permission, Role + + +class Command(BaseCommand): + args = '' + help = 'Seed default permisssions and roles' + + def handle(self, *args, **options): + moderator_role = Role.objects.get_or_create(name="Moderator", course_id="MITx/6.002x/2012_Fall")[0] + student_role = Role.objects.get_or_create(name="Student", course_id="MITx/6.002x/2012_Fall")[0] + + for per in ["vote", "update_thread", "follow_thread", "unfollow_thread", + "update_comment", "create_sub_comment", "unvote" , "create_thread", + "follow_commentable", "unfollow_commentable", "create_comment", ]: + student_role.add_permission(per) + + for per in ["edit_content", "delete_thread", "openclose_thread", + "endorse_comment", "delete_comment"]: + moderator_role.add_permission(per) + + moderator_role.inherit_permissions(student_role) diff --git a/lms/djangoapps/django_comment_client/management/commands/show_permissions.py b/lms/djangoapps/django_comment_client/management/commands/show_permissions.py new file mode 100644 index 0000000000..3f99bbaca0 --- /dev/null +++ b/lms/djangoapps/django_comment_client/management/commands/show_permissions.py @@ -0,0 +1,31 @@ +from django.core.management.base import BaseCommand, CommandError +from django_comment_client.models import Permission, Role +from django.contrib.auth.models import User + + +class Command(BaseCommand): + args = 'user' + help = "Show a user's roles and permissions" + + def handle(self, *args, **options): + if len(args) != 1: + raise CommandError("The number of arguments does not match. ") + try: + if '@' in args[0]: + user = User.objects.get(email=args[0]) + else: + user = User.objects.get(username=args[0]) + except User.DoesNotExist: + print "User %s does not exist. " % args[0] + print "Available users: " + print User.objects.all() + return + + roles = user.roles.all() + print "%s has %d roles:" % (user, len(roles)) + for role in roles: + print "\t%s" % role + + for role in roles: + print "%s has permissions: " % role + print role.permissions.all() diff --git a/lms/djangoapps/django_comment_client/migrations/0001_initial.py b/lms/djangoapps/django_comment_client/migrations/0001_initial.py index 826cbdae35..4993984d74 100644 --- a/lms/djangoapps/django_comment_client/migrations/0001_initial.py +++ b/lms/djangoapps/django_comment_client/migrations/0001_initial.py @@ -10,7 +10,9 @@ class Migration(SchemaMigration): def forwards(self, orm): # Adding model 'Role' db.create_table('django_comment_client_role', ( - ('name', self.gf('django.db.models.fields.CharField')(max_length=30, primary_key=True)), + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=30)), + ('course_id', self.gf('django.db.models.fields.CharField')(db_index=True, max_length=255, blank=True)), )) db.send_create_signal('django_comment_client', ['Role']) @@ -28,14 +30,6 @@ class Migration(SchemaMigration): )) db.send_create_signal('django_comment_client', ['Permission']) - # Adding M2M table for field users on 'Permission' - db.create_table('django_comment_client_permission_users', ( - ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), - ('permission', models.ForeignKey(orm['django_comment_client.permission'], null=False)), - ('user', models.ForeignKey(orm['auth.user'], null=False)) - )) - db.create_unique('django_comment_client_permission_users', ['permission_id', 'user_id']) - # Adding M2M table for field roles on 'Permission' db.create_table('django_comment_client_permission_roles', ( ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), @@ -55,9 +49,6 @@ class Migration(SchemaMigration): # Deleting model 'Permission' db.delete_table('django_comment_client_permission') - # Removing M2M table for field users on 'Permission' - db.delete_table('django_comment_client_permission_users') - # Removing M2M table for field roles on 'Permission' db.delete_table('django_comment_client_permission_roles') @@ -127,12 +118,13 @@ class Migration(SchemaMigration): 'django_comment_client.permission': { 'Meta': {'object_name': 'Permission'}, 'name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'primary_key': 'True'}), - 'roles': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'permissions'", 'symmetrical': 'False', 'to': "orm['django_comment_client.Role']"}), - 'users': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'permissions'", 'symmetrical': 'False', 'to': "orm['auth.User']"}) + 'roles': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'permissions'", 'symmetrical': 'False', 'to': "orm['django_comment_client.Role']"}) }, 'django_comment_client.role': { 'Meta': {'object_name': 'Role'}, - 'name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'primary_key': 'True'}), + 'course_id': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '30'}), 'users': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'roles'", 'symmetrical': 'False', 'to': "orm['auth.User']"}) } } diff --git a/lms/djangoapps/django_comment_client/models.py b/lms/djangoapps/django_comment_client/models.py index 22794677fa..06bf8c0d6b 100644 --- a/lms/djangoapps/django_comment_client/models.py +++ b/lms/djangoapps/django_comment_client/models.py @@ -1,36 +1,33 @@ from django.db import models from django.contrib.auth.models import User +import logging class Role(models.Model): - name = models.CharField(max_length=30, null=False, blank=False, primary_key=True) + name = models.CharField(max_length=30, null=False, blank=False) users = models.ManyToManyField(User, related_name="roles") + course_id = models.CharField(max_length=255, blank=True, db_index=True) def __unicode__(self): - return self.name - - @staticmethod - def register(name): - return Role.objects.get_or_create(name=name)[0] - - def register_permissions(self, permissions): - for p in permissions: - if not self.permissions.filter(name=p): - self.permissions.add(Permission.register(p)) + return self.name + " for " + (self.course_id if self.course_id else "all courses") def inherit_permissions(self, role): - self.register_permissions(map(lambda p: p.name, role.permissions.all())) + if role.course_id and role.course_id != self.course_id: + logging.warning("%s cannot inheret permissions from %s due to course_id inconsistency" % + (self, role)) + for per in role.permissions.all(): + self.add_permission(per) + + def add_permission(self, permission): + self.permissions.add(Permission.objects.get_or_create(name=permission)[0]) + + def has_permission(self, permission): + return self.permissions.filter(name=permission).exists() class Permission(models.Model): name = models.CharField(max_length=30, null=False, blank=False, primary_key=True) - users = models.ManyToManyField(User, related_name="permissions") roles = models.ManyToManyField(Role, related_name="permissions") def __unicode__(self): return self.name - - @staticmethod - def register(name): - return Permission.objects.get_or_create(name=name)[0] - diff --git a/lms/djangoapps/django_comment_client/permissions.py b/lms/djangoapps/django_comment_client/permissions.py index 9beb2b8dae..eae67a8361 100644 --- a/lms/djangoapps/django_comment_client/permissions.py +++ b/lms/djangoapps/django_comment_client/permissions.py @@ -2,110 +2,98 @@ from .models import Role, Permission from django.contrib.auth.models import User from django.db.models.signals import post_save from django.dispatch import receiver +from student.models import CourseEnrollment + import logging -def has_permission(user, p): - if not Permission.objects.filter(name=p).exists(): - logging.warning("Permission %s was not registered. " % p) - if Permission.objects.filter(users=user, name=p).exists(): - return True - if Permission.objects.filter(roles__in=user.roles.all(), name=p).exists(): - return True + +@receiver(post_save, sender=CourseEnrollment) +def assign_default_role(sender, instance, **kwargs): + if instance.user.is_staff: + role = Role.objects.get(course_id=instance.course_id, name="Moderator") + else: + role = Role.objects.get(course_id=instance.course_id, name="Student") + + logging.info("assign_default_role: adding %s as %s" % (instance.user, role)) + instance.user.roles.add(role) + + +def has_permission(user, permission, course_id=None): + # if user.permissions.filter(name=permission).exists(): + # return True + for role in user.roles.filter(course_id=course_id): + if role.has_permission(permission): + return True return False -def has_permissions(user, *args): - for p in args: - if not has_permission(user, p): - return False - return True -def add_permission(instance, p): - permission = Permission.register(name=p) - if isinstance(instance, User) or isinstance(isinstance, Role): - instance.permissions.add(permission) - else: - raise TypeError("Permission can only be added to a role or user") +CONDITIONS = ['is_open', 'is_author'] +def check_condition(user, condition, course_id, data): + def check_open(user, condition, course_id, data): + return not data['content']['closed'] + + def check_author(user, condition, course_id, data): + return data['content']['user_id'] == str(user.id) + + handlers = { + 'is_open' : check_open, + 'is_author' : check_author, + } + + return handlers[condition](user, condition, course_id, data) -@receiver(post_save, sender=User) -def assign_default_role(sender, instance, **kwargs): - # if kwargs.get("created", True): - role = moderator_role if instance.is_staff else student_role - logging.info("assign_default_role: adding %s as %s" % (instance, role)) - instance.roles.add(role) - - -def check_permissions(user, content, per): +def check_conditions_permissions(user, permissions, course_id, **kwargs): """ 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 + Note that ["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'] + a list. """ - 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] + def test(user, per, operator="or"): + if isinstance(per, basestring): + if per in CONDITIONS: + return check_condition(user, per, course_id, kwargs) + return has_permission(user, per, course_id=course_id) + elif isinstance(per, list) and operator in ["and", "or"]: + results = [test(user, x, operator="and") for x in per] if operator == "or": return True in results elif operator == "and": return not False in results - return test_permission(user, permissions, operator="or") + return test(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_comment' : (['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'), + 'update_thread' : ['edit_content', ['update_thread', 'is_open', 'author']], + # 'create_comment' : [["create_comment", "is_open"]], + 'create_comment' : ["create_comment"], + 'delete_thread' : ['delete_thread'], + 'update_comment' : ['edit_content', ['update_comment', 'is_open', 'author']], + 'endorse_comment' : ['endorse_comment'], + 'openclose_thread' : ['openclose_thread'], + 'create_sub_comment': [['create_sub_comment', 'is_open']], + 'delete_comment' : ['delete_comment'], + 'vote_for_comment' : [['vote', 'is_open']], + 'undo_vote_for_comment': [['unvote', 'is_open']], + 'vote_for_thread' : [['vote', 'is_open']], + 'undo_vote_for_thread': [['unvote', 'is_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): + +def check_permissions_by_view(user, course_id, content, name): + # import pdb; pdb.set_trace() 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", - "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", ]) - -moderator_role.inherit_permissions(student_role) \ No newline at end of file + return check_conditions_permissions(user, p, course_id, content=content) diff --git a/lms/templates/discussion/_thread.html b/lms/templates/discussion/_thread.html index eb2d4f3468..c22e41eaad 100644 --- a/lms/templates/discussion/_thread.html +++ b/lms/templates/discussion/_thread.html @@ -89,9 +89,6 @@ ${render_link("discussion-link discussion-reply discussion-reply-" + type, "Reply")} ${render_link("discussion-link discussion-edit", "Edit")} - % if type == 'thread': - Permanent Link - % endif % if content.get('endorsed', False):