Commit 547114a8 by Rocky Duan

Merge branch 'ccp0101/moderation' of github.com:dementrock/mitx into ccp0101/moderation

parents 6c2f53a4 6bc69ff9
# 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', ...@@ -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\-]+)/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\-]+)/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\-]+)/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\-]+)/update$', 'update_comment', name='update_comment'),
url(r'comments/(?P<comment_id>[\w\-]+)/endorse$', 'endorse_comment', name='endorse_comment'), url(r'comments/(?P<comment_id>[\w\-]+)/endorse$', 'endorse_comment', name='endorse_comment'),
......
...@@ -18,6 +18,29 @@ from django.conf import settings ...@@ -18,6 +18,29 @@ from django.conf import settings
from mitxmako.shortcuts import render_to_response, render_to_string from mitxmako.shortcuts import render_to_response, render_to_string
from django_comment_client.utils import JsonResponse, JsonError, extract 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 thread_author_only(fn):
def verified_fn(request, *args, **kwargs): def verified_fn(request, *args, **kwargs):
thread_id = kwargs.get('thread_id', False) thread_id = kwargs.get('thread_id', False)
...@@ -48,6 +71,7 @@ def instructor_only(fn): ...@@ -48,6 +71,7 @@ def instructor_only(fn):
@require_POST @require_POST
@login_required @login_required
@permitted
def create_thread(request, course_id, commentable_id): def create_thread(request, course_id, commentable_id):
attributes = extract(request.POST, ['body', 'title', 'tags']) attributes = extract(request.POST, ['body', 'title', 'tags'])
attributes['user_id'] = request.user.id attributes['user_id'] = request.user.id
...@@ -72,7 +96,7 @@ def create_thread(request, course_id, commentable_id): ...@@ -72,7 +96,7 @@ def create_thread(request, course_id, commentable_id):
@require_POST @require_POST
@login_required @login_required
@thread_author_only @permitted
def update_thread(request, course_id, thread_id): def update_thread(request, course_id, thread_id):
attributes = extract(request.POST, ['body', 'title', 'tags']) attributes = extract(request.POST, ['body', 'title', 'tags'])
response = comment_client.update_thread(thread_id, attributes) response = comment_client.update_thread(thread_id, attributes)
...@@ -112,6 +136,7 @@ def _create_comment(request, course_id, _response_from_attributes): ...@@ -112,6 +136,7 @@ def _create_comment(request, course_id, _response_from_attributes):
@require_POST @require_POST
@login_required @login_required
@permitted
def create_comment(request, course_id, thread_id): def create_comment(request, course_id, thread_id):
def _response_from_attributes(attributes): def _response_from_attributes(attributes):
return comment_client.create_comment(thread_id, attributes) return comment_client.create_comment(thread_id, attributes)
...@@ -119,14 +144,14 @@ def create_comment(request, course_id, thread_id): ...@@ -119,14 +144,14 @@ def create_comment(request, course_id, thread_id):
@require_POST @require_POST
@login_required @login_required
@thread_author_only @permitted
def delete_thread(request, course_id, thread_id): def delete_thread(request, course_id, thread_id):
response = comment_client.delete_thread(thread_id) response = comment_client.delete_thread(thread_id)
return JsonResponse(response) return JsonResponse(response)
@require_POST @require_POST
@login_required @login_required
@comment_author_only @permitted
def update_comment(request, course_id, comment_id): def update_comment(request, course_id, comment_id):
attributes = extract(request.POST, ['body']) attributes = extract(request.POST, ['body'])
response = comment_client.update_comment(comment_id, attributes) response = comment_client.update_comment(comment_id, attributes)
...@@ -145,7 +170,7 @@ def update_comment(request, course_id, comment_id): ...@@ -145,7 +170,7 @@ def update_comment(request, course_id, comment_id):
@require_POST @require_POST
@login_required @login_required
@instructor_only @permitted
def endorse_comment(request, course_id, comment_id): def endorse_comment(request, course_id, comment_id):
attributes = extract(request.POST, ['endorsed']) attributes = extract(request.POST, ['endorsed'])
response = comment_client.update_comment(comment_id, attributes) response = comment_client.update_comment(comment_id, attributes)
...@@ -153,6 +178,15 @@ def endorse_comment(request, course_id, comment_id): ...@@ -153,6 +178,15 @@ def endorse_comment(request, course_id, comment_id):
@require_POST @require_POST
@login_required @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 create_sub_comment(request, course_id, comment_id):
def _response_from_attributes(attributes): def _response_from_attributes(attributes):
return comment_client.create_sub_comment(comment_id, attributes) return comment_client.create_sub_comment(comment_id, attributes)
...@@ -160,13 +194,14 @@ def create_sub_comment(request, course_id, comment_id): ...@@ -160,13 +194,14 @@ def create_sub_comment(request, course_id, comment_id):
@require_POST @require_POST
@login_required @login_required
@comment_author_only @permitted
def delete_comment(request, course_id, comment_id): def delete_comment(request, course_id, comment_id):
response = comment_client.delete_comment(comment_id) response = comment_client.delete_comment(comment_id)
return JsonResponse(response) return JsonResponse(response)
@require_POST @require_POST
@login_required @login_required
@permitted
def vote_for_comment(request, course_id, comment_id, value): def vote_for_comment(request, course_id, comment_id, value):
user_id = request.user.id user_id = request.user.id
response = comment_client.vote_for_comment(comment_id, user_id, value) 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): ...@@ -174,6 +209,7 @@ def vote_for_comment(request, course_id, comment_id, value):
@require_POST @require_POST
@login_required @login_required
@permitted
def undo_vote_for_comment(request, course_id, comment_id): def undo_vote_for_comment(request, course_id, comment_id):
user_id = request.user.id user_id = request.user.id
response = comment_client.undo_vote_for_comment(comment_id, 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): ...@@ -181,6 +217,7 @@ def undo_vote_for_comment(request, course_id, comment_id):
@require_POST @require_POST
@login_required @login_required
@permitted
def vote_for_thread(request, course_id, thread_id, value): def vote_for_thread(request, course_id, thread_id, value):
user_id = request.user.id user_id = request.user.id
response = comment_client.vote_for_thread(thread_id, user_id, value) 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): ...@@ -188,6 +225,7 @@ def vote_for_thread(request, course_id, thread_id, value):
@require_POST @require_POST
@login_required @login_required
@permitted
def undo_vote_for_thread(request, course_id, thread_id): def undo_vote_for_thread(request, course_id, thread_id):
user_id = request.user.id user_id = request.user.id
response = comment_client.undo_vote_for_thread(thread_id, 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): ...@@ -195,6 +233,7 @@ def undo_vote_for_thread(request, course_id, thread_id):
@require_POST @require_POST
@login_required @login_required
@permitted
def follow_thread(request, course_id, thread_id): def follow_thread(request, course_id, thread_id):
user_id = request.user.id user_id = request.user.id
response = comment_client.subscribe_thread(user_id, thread_id) response = comment_client.subscribe_thread(user_id, thread_id)
...@@ -202,6 +241,7 @@ def follow_thread(request, course_id, thread_id): ...@@ -202,6 +241,7 @@ def follow_thread(request, course_id, thread_id):
@require_POST @require_POST
@login_required @login_required
@permitted
def follow_commentable(request, course_id, commentable_id): def follow_commentable(request, course_id, commentable_id):
user_id = request.user.id user_id = request.user.id
response = comment_client.subscribe_commentable(user_id, commentable_id) response = comment_client.subscribe_commentable(user_id, commentable_id)
...@@ -209,6 +249,7 @@ def follow_commentable(request, course_id, commentable_id): ...@@ -209,6 +249,7 @@ def follow_commentable(request, course_id, commentable_id):
@require_POST @require_POST
@login_required @login_required
@permitted
def follow_user(request, course_id, followed_user_id): def follow_user(request, course_id, followed_user_id):
user_id = request.user.id user_id = request.user.id
response = comment_client.follow(user_id, followed_user_id) response = comment_client.follow(user_id, followed_user_id)
...@@ -216,6 +257,7 @@ def follow_user(request, course_id, followed_user_id): ...@@ -216,6 +257,7 @@ def follow_user(request, course_id, followed_user_id):
@require_POST @require_POST
@login_required @login_required
@permitted
def unfollow_thread(request, course_id, thread_id): def unfollow_thread(request, course_id, thread_id):
user_id = request.user.id user_id = request.user.id
response = comment_client.unsubscribe_thread(user_id, thread_id) response = comment_client.unsubscribe_thread(user_id, thread_id)
...@@ -223,6 +265,7 @@ def unfollow_thread(request, course_id, thread_id): ...@@ -223,6 +265,7 @@ def unfollow_thread(request, course_id, thread_id):
@require_POST @require_POST
@login_required @login_required
@permitted
def unfollow_commentable(request, course_id, commentable_id): def unfollow_commentable(request, course_id, commentable_id):
user_id = request.user.id user_id = request.user.id
response = comment_client.unsubscribe_commentable(user_id, commentable_id) response = comment_client.unsubscribe_commentable(user_id, commentable_id)
...@@ -230,6 +273,7 @@ def unfollow_commentable(request, course_id, commentable_id): ...@@ -230,6 +273,7 @@ def unfollow_commentable(request, course_id, commentable_id):
@require_POST @require_POST
@login_required @login_required
@permitted
def unfollow_user(request, course_id, followed_user_id): def unfollow_user(request, course_id, followed_user_id):
user_id = request.user.id user_id = request.user.id
response = comment_client.unfollow(user_id, followed_user_id) response = comment_client.unfollow(user_id, followed_user_id)
......
...@@ -18,6 +18,7 @@ import json ...@@ -18,6 +18,7 @@ import json
import comment_client import comment_client
import dateutil import dateutil
from django_comment_client.permissions import check_permissions_by_view
THREADS_PER_PAGE = 5 THREADS_PER_PAGE = 5
PAGES_NEARBY_DELTA = 2 PAGES_NEARBY_DELTA = 2
...@@ -53,7 +54,7 @@ def render_discussion(request, course_id, threads, discussion_id=None, \ ...@@ -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])), 'forum': (lambda: reverse('django_comment_client.forum.views.forum_form_discussion', args=[course_id, discussion_id])),
}[discussion_type]() }[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 = { context = {
'threads': threads, 'threads': threads,
...@@ -133,28 +134,32 @@ def forum_form_discussion(request, course_id, discussion_id): ...@@ -133,28 +134,32 @@ def forum_form_discussion(request, course_id, discussion_id):
return render_to_response('discussion/index.html', context) 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 { 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 = {} infos = {}
def _annotate(content): def _annotate(content, is_thread=is_thread):
infos[str(content['id'])] = get_annotated_content_info(content, user_id) infos[str(content['id'])] = get_annotated_content_info(content, user, is_thread)
for child in content.get('children', []): for child in content.get('children', []):
_annotate(child) _annotate(child, is_thread=False)
_annotate(thread) _annotate(thread)
return infos 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) 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(thread=thread, \
user_id=request.user.id) user=request.user, is_thread=True)
context = { context = {
'discussion_id': discussion_id,
'thread': thread, 'thread': thread,
'user_info': comment_client.get_user_info(request.user.id, raw=True), 'user_info': comment_client.get_user_info(request.user.id, raw=True),
'annotated_content_info': json.dumps(annotated_content_info), 'annotated_content_info': json.dumps(annotated_content_info),
...@@ -168,8 +173,7 @@ def single_thread(request, course_id, discussion_id, thread_id): ...@@ -168,8 +173,7 @@ def single_thread(request, course_id, discussion_id, thread_id):
if request.is_ajax(): if request.is_ajax():
thread = comment_client.get_thread(thread_id, recursive=True) 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(thread, request.user)
user_id=request.user.id)
context = {'thread': thread} context = {'thread': thread}
html = render_to_string('discussion/_ajax_single_thread.html', context) html = render_to_string('discussion/_ajax_single_thread.html', context)
...@@ -182,9 +186,10 @@ def single_thread(request, course_id, discussion_id, thread_id): ...@@ -182,9 +186,10 @@ def single_thread(request, course_id, discussion_id, thread_id):
course = check_course(course_id) course = check_course(course_id)
context = { context = {
'discussion_id': discussion_id,
'csrf': csrf(request)['csrf_token'], 'csrf': csrf(request)['csrf_token'],
'init': '', '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), 'accordion': render_accordion(request, course, discussion_id),
'course': course, 'course': course,
} }
......
# -*- 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']
\ No newline at end of file
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]
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)
\ No newline at end of file
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))
\ No newline at end of file
...@@ -115,8 +115,12 @@ class JsonError(HttpResponse): ...@@ -115,8 +115,12 @@ class JsonError(HttpResponse):
indent=2, indent=2,
ensure_ascii=False) ensure_ascii=False)
super(JsonError, self).__init__(content, super(JsonError, self).__init__(content,
mimetype='application/json; charset=utf8') mimetype='application/json; charset=utf8', status=500)
class HtmlResponse(HttpResponse): class HtmlResponse(HttpResponse):
def __init__(self, html=''): def __init__(self, html=''):
super(HtmlResponse, self).__init__(html, content_type='text/plain') 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 = ( ...@@ -305,6 +305,8 @@ MIDDLEWARE_CLASSES = (
'askbot.middleware.spaceless.SpacelessMiddleware', 'askbot.middleware.spaceless.SpacelessMiddleware',
# 'askbot.middleware.pagesize.QuestionsPageSizeMiddleware', # 'askbot.middleware.pagesize.QuestionsPageSizeMiddleware',
# 'debug_toolbar.middleware.DebugToolbarMiddleware', # 'debug_toolbar.middleware.DebugToolbarMiddleware',
'django_comment_client.utils.ViewNameMiddleware',
) )
############################### Pipeline ####################################### ############################### Pipeline #######################################
......
...@@ -78,6 +78,7 @@ initializeFollowThread = (thread) -> ...@@ -78,6 +78,7 @@ initializeFollowThread = (thread) ->
$comment = $(response.html) $comment = $(response.html)
$content.children(".comments").prepend($comment) $content.children(".comments").prepend($comment)
Discussion.setWmdContent $content, $local, "reply-body", "" Discussion.setWmdContent $content, $local, "reply-body", ""
Discussion.setContentInfo response.content['id'], 'can_reply', true
Discussion.setContentInfo response.content['id'], 'editable', true Discussion.setContentInfo response.content['id'], 'editable', true
Discussion.initializeContent($comment) Discussion.initializeContent($comment)
Discussion.bindContentEvents($comment) Discussion.bindContentEvents($comment)
...@@ -195,6 +196,51 @@ initializeFollowThread = (thread) -> ...@@ -195,6 +196,51 @@ initializeFollowThread = (thread) ->
else else
$(content).removeClass("endorsed") $(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) -> handleHideSingleThread = (elem) ->
$threadTitle = $local(".thread-title") $threadTitle = $local(".thread-title")
$showComments = $local(".discussion-show-comments") $showComments = $local(".discussion-show-comments")
...@@ -271,12 +317,18 @@ initializeFollowThread = (thread) -> ...@@ -271,12 +317,18 @@ initializeFollowThread = (thread) ->
"click .discussion-endorse": -> "click .discussion-endorse": ->
handleEndorse(this, $(this).is(":checked")) handleEndorse(this, $(this).is(":checked"))
"click .discussion-openclose": ->
handleOpenClose(this, $(this).text())
"click .discussion-edit": -> "click .discussion-edit": ->
if $content.hasClass("thread") if $content.hasClass("thread")
handleEditThread(this) handleEditThread(this)
else else
handleEditComment(this) handleEditComment(this)
"click .discussion-delete": ->
handleDelete(this)
initializeContent: (content) -> initializeContent: (content) ->
unescapeHighlightTag = (text) -> unescapeHighlightTag = (text) ->
...@@ -314,3 +366,9 @@ initializeFollowThread = (thread) -> ...@@ -314,3 +366,9 @@ initializeFollowThread = (thread) ->
id = $content.attr("_id") id = $content.attr("_id")
if not Discussion.getContentInfo id, 'editable' if not Discussion.getContentInfo id, 'editable'
$local(".discussion-edit").remove() $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 = {} ...@@ -30,6 +30,7 @@ wmdEditors = {}
undo_vote_for_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/unvote" undo_vote_for_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/unvote"
follow_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/follow" follow_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/follow"
unfollow_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/unfollow" 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" update_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/update"
endorse_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/endorse" endorse_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/endorse"
create_sub_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/reply" create_sub_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/reply"
......
...@@ -9,6 +9,10 @@ def base_url_for_search(): ...@@ -9,6 +9,10 @@ def base_url_for_search():
%> %>
<form action="${base_url_for_search()}" method="get" class="discussion-search-form"> <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> <div class="discussion-link discussion-search-link" href="javascript:void(0)">Search posts</div>
</form> </form>
...@@ -42,6 +42,7 @@ ...@@ -42,6 +42,7 @@
<div class="discussion-right-wrapper clearfix"> <div class="discussion-right-wrapper clearfix">
${render_title(content, type, **kwargs)} ${render_title(content, type, **kwargs)}
<div class="discussion-content-view"> <div class="discussion-content-view">
<a name="${content['id']}"></a>
% if content.get('highlighted_body', None): % if content.get('highlighted_body', None):
<div class="content-body ${type}-body" id="content-body-${content['id']}">${content['highlighted_body'] | h}</div> <div class="content-body ${type}-body" id="content-body-${content['id']}">${content['highlighted_body'] | h}</div>
% else: % else:
...@@ -87,14 +88,32 @@ ...@@ -87,14 +88,32 @@
${render_info(content)} ${render_info(content)}
${render_link("discussion-link discussion-reply discussion-reply-" + type, "Reply")} ${render_link("discussion-link discussion-reply discussion-reply-" + type, "Reply")}
${render_link("discussion-link discussion-edit", "Edit")} ${render_link("discussion-link discussion-edit", "Edit")}
% if type == "comment" and request.user.is_staff:
% if content['endorsed']: % if type == "thread":
<input type="checkbox" checked="checked" class="discussion-link discussion-endorse" id="discussion-endorse-${content['id']}"> <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: % 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 % endif
<label class="discussion-link" for="discussion-endorse-${content['id']}">Endorsed</label> <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 % endif
${render_link("discussion-link discussion-delete", "Delete")}
</div> </div>
</%def> </%def>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment