Merge branch 'ccp0101/moderation' of github.com:dementrock/mitx into ccp0101/moderation
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
# call some function from permissions so that the post_save hook is imported
|
||||
from permissions import assign_default_role
|
||||
|
||||
@@ -13,6 +13,7 @@ urlpatterns = patterns('django_comment_client.base.views',
|
||||
url(r'threads/(?P<thread_id>[\w\-]+)/unvote$', 'undo_vote_for_thread', name='undo_vote_for_thread'),
|
||||
url(r'threads/(?P<thread_id>[\w\-]+)/follow$', 'follow_thread', name='follow_thread'),
|
||||
url(r'threads/(?P<thread_id>[\w\-]+)/unfollow$', 'unfollow_thread', name='unfollow_thread'),
|
||||
url(r'threads/(?P<thread_id>[\w\-]+)/close$', 'openclose_thread', name='openclose_thread'),
|
||||
|
||||
url(r'comments/(?P<comment_id>[\w\-]+)/update$', 'update_comment', name='update_comment'),
|
||||
url(r'comments/(?P<comment_id>[\w\-]+)/endorse$', 'endorse_comment', name='endorse_comment'),
|
||||
|
||||
@@ -18,6 +18,29 @@ 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 check_permissions_by_view
|
||||
import functools
|
||||
|
||||
|
||||
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:
|
||||
content = None
|
||||
return content
|
||||
|
||||
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):
|
||||
def verified_fn(request, *args, **kwargs):
|
||||
thread_id = kwargs.get('thread_id', False)
|
||||
@@ -48,6 +71,7 @@ def instructor_only(fn):
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@permitted
|
||||
def create_thread(request, course_id, commentable_id):
|
||||
attributes = extract(request.POST, ['body', 'title', 'tags'])
|
||||
attributes['user_id'] = request.user.id
|
||||
@@ -72,7 +96,7 @@ def create_thread(request, course_id, commentable_id):
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@thread_author_only
|
||||
@permitted
|
||||
def update_thread(request, course_id, thread_id):
|
||||
attributes = extract(request.POST, ['body', 'title', 'tags'])
|
||||
response = comment_client.update_thread(thread_id, attributes)
|
||||
@@ -112,6 +136,7 @@ def _create_comment(request, course_id, _response_from_attributes):
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@permitted
|
||||
def create_comment(request, course_id, thread_id):
|
||||
def _response_from_attributes(attributes):
|
||||
return comment_client.create_comment(thread_id, attributes)
|
||||
@@ -119,14 +144,14 @@ def create_comment(request, course_id, thread_id):
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@thread_author_only
|
||||
@permitted
|
||||
def delete_thread(request, course_id, thread_id):
|
||||
response = comment_client.delete_thread(thread_id)
|
||||
return JsonResponse(response)
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@comment_author_only
|
||||
@permitted
|
||||
def update_comment(request, course_id, comment_id):
|
||||
attributes = extract(request.POST, ['body'])
|
||||
response = comment_client.update_comment(comment_id, attributes)
|
||||
@@ -145,7 +170,7 @@ def update_comment(request, course_id, comment_id):
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@instructor_only
|
||||
@permitted
|
||||
def endorse_comment(request, course_id, comment_id):
|
||||
attributes = extract(request.POST, ['endorsed'])
|
||||
response = comment_client.update_comment(comment_id, attributes)
|
||||
@@ -153,6 +178,15 @@ def endorse_comment(request, course_id, comment_id):
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@permitted
|
||||
def openclose_thread(request, course_id, thread_id):
|
||||
attributes = extract(request.POST, ['closed'])
|
||||
response = comment_client.update_thread(thread_id, attributes)
|
||||
return JsonResponse(response)
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@permitted
|
||||
def create_sub_comment(request, course_id, comment_id):
|
||||
def _response_from_attributes(attributes):
|
||||
return comment_client.create_sub_comment(comment_id, attributes)
|
||||
@@ -160,13 +194,14 @@ def create_sub_comment(request, course_id, comment_id):
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@comment_author_only
|
||||
@permitted
|
||||
def delete_comment(request, course_id, comment_id):
|
||||
response = comment_client.delete_comment(comment_id)
|
||||
return JsonResponse(response)
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@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)
|
||||
@@ -174,6 +209,7 @@ def vote_for_comment(request, course_id, comment_id, value):
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@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)
|
||||
@@ -181,6 +217,7 @@ def undo_vote_for_comment(request, course_id, comment_id):
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@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)
|
||||
@@ -188,6 +225,7 @@ def vote_for_thread(request, course_id, thread_id, value):
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@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)
|
||||
@@ -195,6 +233,7 @@ def undo_vote_for_thread(request, course_id, thread_id):
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@permitted
|
||||
def follow_thread(request, course_id, thread_id):
|
||||
user_id = request.user.id
|
||||
response = comment_client.subscribe_thread(user_id, thread_id)
|
||||
@@ -202,6 +241,7 @@ def follow_thread(request, course_id, thread_id):
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@permitted
|
||||
def follow_commentable(request, course_id, commentable_id):
|
||||
user_id = request.user.id
|
||||
response = comment_client.subscribe_commentable(user_id, commentable_id)
|
||||
@@ -209,6 +249,7 @@ def follow_commentable(request, course_id, commentable_id):
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@permitted
|
||||
def follow_user(request, course_id, followed_user_id):
|
||||
user_id = request.user.id
|
||||
response = comment_client.follow(user_id, followed_user_id)
|
||||
@@ -216,6 +257,7 @@ def follow_user(request, course_id, followed_user_id):
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@permitted
|
||||
def unfollow_thread(request, course_id, thread_id):
|
||||
user_id = request.user.id
|
||||
response = comment_client.unsubscribe_thread(user_id, thread_id)
|
||||
@@ -223,6 +265,7 @@ def unfollow_thread(request, course_id, thread_id):
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@permitted
|
||||
def unfollow_commentable(request, course_id, commentable_id):
|
||||
user_id = request.user.id
|
||||
response = comment_client.unsubscribe_commentable(user_id, commentable_id)
|
||||
@@ -230,6 +273,7 @@ def unfollow_commentable(request, course_id, commentable_id):
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@permitted
|
||||
def unfollow_user(request, course_id, followed_user_id):
|
||||
user_id = request.user.id
|
||||
response = comment_client.unfollow(user_id, followed_user_id)
|
||||
|
||||
@@ -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
|
||||
@@ -53,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.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,
|
||||
@@ -133,28 +134,32 @@ 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"),
|
||||
'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"),
|
||||
}
|
||||
|
||||
def get_annotated_content_infos(thread, user_id):
|
||||
def get_annotated_content_infos(thread, user, is_thread=True):
|
||||
infos = {}
|
||||
def _annotate(content):
|
||||
infos[str(content['id'])] = get_annotated_content_info(content, user_id)
|
||||
def _annotate(content, is_thread=is_thread):
|
||||
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
|
||||
|
||||
def render_single_thread(request, course_id, thread_id):
|
||||
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, \
|
||||
user_id=request.user.id)
|
||||
user=request.user, is_thread=True)
|
||||
|
||||
context = {
|
||||
'discussion_id': discussion_id,
|
||||
'thread': thread,
|
||||
'user_info': comment_client.get_user_info(request.user.id, raw=True),
|
||||
'annotated_content_info': json.dumps(annotated_content_info),
|
||||
@@ -168,8 +173,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)
|
||||
|
||||
@@ -182,9 +186,10 @@ def single_thread(request, course_id, discussion_id, thread_id):
|
||||
course = check_course(course_id)
|
||||
|
||||
context = {
|
||||
'discussion_id': discussion_id,
|
||||
'csrf': csrf(request)['csrf_token'],
|
||||
'init': '',
|
||||
'content': render_single_thread(request, course_id, thread_id),
|
||||
'content': render_single_thread(request, discussion_id, course_id, thread_id),
|
||||
'accordion': render_accordion(request, course, discussion_id),
|
||||
'course': course,
|
||||
}
|
||||
|
||||
140
lms/djangoapps/django_comment_client/migrations/0001_initial.py
Normal file
140
lms/djangoapps/django_comment_client/migrations/0001_initial.py
Normal file
@@ -0,0 +1,140 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
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)),
|
||||
))
|
||||
db.send_create_signal('django_comment_client', ['Role'])
|
||||
|
||||
# Adding M2M table for field users on 'Role'
|
||||
db.create_table('django_comment_client_role_users', (
|
||||
('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
|
||||
('role', models.ForeignKey(orm['django_comment_client.role'], null=False)),
|
||||
('user', models.ForeignKey(orm['auth.user'], null=False))
|
||||
))
|
||||
db.create_unique('django_comment_client_role_users', ['role_id', 'user_id'])
|
||||
|
||||
# Adding model 'Permission'
|
||||
db.create_table('django_comment_client_permission', (
|
||||
('name', self.gf('django.db.models.fields.CharField')(max_length=30, primary_key=True)),
|
||||
))
|
||||
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)),
|
||||
('permission', models.ForeignKey(orm['django_comment_client.permission'], null=False)),
|
||||
('role', models.ForeignKey(orm['django_comment_client.role'], null=False))
|
||||
))
|
||||
db.create_unique('django_comment_client_permission_roles', ['permission_id', 'role_id'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Deleting model 'Role'
|
||||
db.delete_table('django_comment_client_role')
|
||||
|
||||
# Removing M2M table for field users on 'Role'
|
||||
db.delete_table('django_comment_client_role_users')
|
||||
|
||||
# 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')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}),
|
||||
'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}),
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
|
||||
'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
|
||||
'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}),
|
||||
'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
|
||||
'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}),
|
||||
'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}),
|
||||
'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'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']"})
|
||||
},
|
||||
'django_comment_client.role': {
|
||||
'Meta': {'object_name': 'Role'},
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'primary_key': 'True'}),
|
||||
'users': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'roles'", 'symmetrical': 'False', 'to': "orm['auth.User']"})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['django_comment_client']
|
||||
36
lms/djangoapps/django_comment_client/models.py
Normal file
36
lms/djangoapps/django_comment_client/models.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
|
||||
class Role(models.Model):
|
||||
name = models.CharField(max_length=30, null=False, blank=False, primary_key=True)
|
||||
users = models.ManyToManyField(User, related_name="roles")
|
||||
|
||||
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))
|
||||
|
||||
def inherit_permissions(self, role):
|
||||
self.register_permissions(map(lambda p: p.name, role.permissions.all()))
|
||||
|
||||
|
||||
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]
|
||||
|
||||
111
lms/djangoapps/django_comment_client/permissions.py
Normal file
111
lms/djangoapps/django_comment_client/permissions.py
Normal file
@@ -0,0 +1,111 @@
|
||||
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
|
||||
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
|
||||
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")
|
||||
|
||||
|
||||
@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):
|
||||
"""
|
||||
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",
|
||||
"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)
|
||||
36
lms/djangoapps/django_comment_client/tests.py
Normal file
36
lms/djangoapps/django_comment_client/tests.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils import unittest
|
||||
import string
|
||||
import random
|
||||
from .permissions import student_role, moderator_role, add_permission, has_permission
|
||||
from .models import Role, Permission
|
||||
|
||||
|
||||
class PermissionsTestCase(unittest.TestCase):
|
||||
def random_str(self, length=15, chars=string.ascii_uppercase + string.digits):
|
||||
return ''.join(random.choice(chars) for x in range(length))
|
||||
|
||||
def setUp(self):
|
||||
self.student = User.objects.create(username=self.random_str(),
|
||||
password="123456", email="john@yahoo.com")
|
||||
self.moderator = User.objects.create(username=self.random_str(),
|
||||
password="123456", email="staff@edx.org")
|
||||
self.moderator.is_staff = True
|
||||
self.moderator.save()
|
||||
|
||||
def tearDown(self):
|
||||
self.student.delete()
|
||||
self.moderator.delete()
|
||||
|
||||
def testDefaultRoles(self):
|
||||
self.assertTrue(student_role in self.student.roles.all())
|
||||
self.assertTrue(moderator_role in self.moderator.roles.all())
|
||||
|
||||
def testPermission(self):
|
||||
name = self.random_str()
|
||||
Permission.register(name)
|
||||
add_permission(moderator_role, name)
|
||||
self.assertTrue(has_permission(self.moderator, name))
|
||||
|
||||
add_permission(self.student, name)
|
||||
self.assertTrue(has_permission(self.student, name))
|
||||
@@ -115,8 +115,12 @@ class JsonError(HttpResponse):
|
||||
indent=2,
|
||||
ensure_ascii=False)
|
||||
super(JsonError, self).__init__(content,
|
||||
mimetype='application/json; charset=utf8')
|
||||
mimetype='application/json; charset=utf8', status=500)
|
||||
|
||||
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__
|
||||
|
||||
@@ -305,6 +305,8 @@ MIDDLEWARE_CLASSES = (
|
||||
'askbot.middleware.spaceless.SpacelessMiddleware',
|
||||
# 'askbot.middleware.pagesize.QuestionsPageSizeMiddleware',
|
||||
# 'debug_toolbar.middleware.DebugToolbarMiddleware',
|
||||
|
||||
'django_comment_client.utils.ViewNameMiddleware',
|
||||
)
|
||||
|
||||
############################### Pipeline #######################################
|
||||
|
||||
@@ -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)
|
||||
@@ -195,6 +196,51 @@ initializeFollowThread = (thread) ->
|
||||
else
|
||||
$(content).removeClass("endorsed")
|
||||
|
||||
handleOpenClose = (elem, text) ->
|
||||
url = Discussion.urlFor('openclose_thread', id)
|
||||
closed = undefined
|
||||
if text.match(/Close/)
|
||||
closed = true
|
||||
else if text.match(/[Oo]pen/)
|
||||
closed = false
|
||||
else
|
||||
return console.log "Unexpected text " + text + "for open/close thread."
|
||||
|
||||
Discussion.safeAjax
|
||||
$elem: $(elem)
|
||||
url: url
|
||||
type: "POST"
|
||||
dataType: "json"
|
||||
data: {closed: closed}
|
||||
success: (response, textStatus) =>
|
||||
if textStatus == "success"
|
||||
if closed
|
||||
$(content).addClass("closed")
|
||||
$(elem).text "Re-open Thread"
|
||||
else
|
||||
$(content).removeClass("closed")
|
||||
$(elem).text "Close Thread"
|
||||
error: (response, textStatus, e) ->
|
||||
console.log e
|
||||
|
||||
handleDelete = (elem) ->
|
||||
if $content.hasClass("thread")
|
||||
url = Discussion.urlFor('delete_thread', id)
|
||||
else
|
||||
url = Discussion.urlFor('delete_comment', id)
|
||||
|
||||
Discussion.safeAjax
|
||||
$elem: $(elem)
|
||||
url: url
|
||||
type: "POST"
|
||||
dataType: "json"
|
||||
data: {}
|
||||
success: (response, textStatus) =>
|
||||
if textStatus == "success"
|
||||
$(content).remove()
|
||||
error: (response, textStatus, e) ->
|
||||
console.log e
|
||||
|
||||
handleHideSingleThread = (elem) ->
|
||||
$threadTitle = $local(".thread-title")
|
||||
$showComments = $local(".discussion-show-comments")
|
||||
@@ -271,12 +317,18 @@ initializeFollowThread = (thread) ->
|
||||
"click .discussion-endorse": ->
|
||||
handleEndorse(this, $(this).is(":checked"))
|
||||
|
||||
"click .discussion-openclose": ->
|
||||
handleOpenClose(this, $(this).text())
|
||||
|
||||
"click .discussion-edit": ->
|
||||
if $content.hasClass("thread")
|
||||
handleEditThread(this)
|
||||
else
|
||||
handleEditComment(this)
|
||||
|
||||
"click .discussion-delete": ->
|
||||
handleDelete(this)
|
||||
|
||||
initializeContent: (content) ->
|
||||
|
||||
unescapeHighlightTag = (text) ->
|
||||
@@ -314,3 +366,9 @@ 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()
|
||||
if not Discussion.getContentInfo id, 'can_endorse'
|
||||
$local(".discussion-endorse-control").remove()
|
||||
if not Discussion.getContentInfo id, 'can_delete'
|
||||
$local(".discussion-delete").remove()
|
||||
|
||||
@@ -30,6 +30,7 @@ wmdEditors = {}
|
||||
undo_vote_for_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/unvote"
|
||||
follow_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/follow"
|
||||
unfollow_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/unfollow"
|
||||
openclose_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/close"
|
||||
update_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/update"
|
||||
endorse_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/endorse"
|
||||
create_sub_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/reply"
|
||||
|
||||
@@ -9,6 +9,10 @@ def base_url_for_search():
|
||||
%>
|
||||
|
||||
<form action="${base_url_for_search()}" method="get" class="discussion-search-form">
|
||||
<input class="search-input" type="text" value="${text}" id="keywords" autocomplete="off"/>
|
||||
% if 'tag' in query_params:
|
||||
<input class="search-input" type="text" value="[${tags}]${text}" id="keywords" autocomplete="off"/>
|
||||
% else:
|
||||
<input class="search-input" type="text" value="[${tags}]${text}" id="keywords" autocomplete="off"/>
|
||||
% endif
|
||||
<div class="discussion-link discussion-search-link" href="javascript:void(0)">Search posts</div>
|
||||
</form>
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
<div class="discussion-right-wrapper clearfix">
|
||||
${render_title(content, type, **kwargs)}
|
||||
<div class="discussion-content-view">
|
||||
<a name="${content['id']}"></a>
|
||||
% if content.get('highlighted_body', None):
|
||||
<div class="content-body ${type}-body" id="content-body-${content['id']}">${content['highlighted_body'] | h}</div>
|
||||
% else:
|
||||
@@ -87,14 +88,32 @@
|
||||
${render_info(content)}
|
||||
${render_link("discussion-link discussion-reply discussion-reply-" + type, "Reply")}
|
||||
${render_link("discussion-link discussion-edit", "Edit")}
|
||||
% if type == "comment" and request.user.is_staff:
|
||||
% if content['endorsed']:
|
||||
<input type="checkbox" checked="checked" class="discussion-link discussion-endorse" id="discussion-endorse-${content['id']}">
|
||||
|
||||
% if type == "thread":
|
||||
<a class="discussion-link discussion-permanent-link" href="${reverse('django_comment_client.forum.views.single_thread', kwargs={'discussion_id':discussion_id, 'thread_id':content['id'], 'course_id':course_id})}">Permanent Link</a>
|
||||
% else:
|
||||
<a class="discussion-link discussion-permanent-link" href="${reverse('django_comment_client.forum.views.single_thread', kwargs={'discussion_id':discussion_id, 'thread_id':content['thread_id'], 'course_id':course_id})}#${content['id']}">Permanent Link</a>
|
||||
% endif
|
||||
|
||||
<span class="discussion-endorse-control">
|
||||
% if content.get('endorsed', False):
|
||||
<input type="checkbox" checked="checked" class="discussion-link discussion-endorse" id="discussion-endorse-${content['id']}" />
|
||||
% else:
|
||||
<input type="checkbox" class="discussion-link discussion-endorse" id="discussion-endorse-${content['id']}">
|
||||
<input type="checkbox" class="discussion-link discussion-endorse" id="discussion-endorse-${content['id']}" />
|
||||
% endif
|
||||
<label class="discussion-link" for="discussion-endorse-${content['id']}">Endorsed</label>
|
||||
</span>
|
||||
|
||||
% if type == "thread" and request.user.is_staff:
|
||||
% if content['closed']:
|
||||
<a class="discussion-openclose" id="discussion-openclose-${content['id']}" href="javascript:void(0);">Re-open thread</a>
|
||||
% else:
|
||||
<a class="discussion-openclose" id="discussion-openclose-${content['id']}" href="javascript:void(0);">Close thread</a>
|
||||
% endif
|
||||
% endif
|
||||
|
||||
${render_link("discussion-link discussion-delete", "Delete")}
|
||||
|
||||
</div>
|
||||
</%def>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user