Commit 6cdcf8e9 by Albert St. Aubin

Discussion group moderation

parent 8fb86474
......@@ -16,6 +16,7 @@ from xmodule.modulestore.exceptions import ItemNotFoundError
FORUM_ROLE_ADMINISTRATOR = ugettext_noop('Administrator')
FORUM_ROLE_MODERATOR = ugettext_noop('Moderator')
FORUM_ROLE_GROUP_MODERATOR = ugettext_noop('Group Moderator')
FORUM_ROLE_COMMUNITY_TA = ugettext_noop('Community TA')
FORUM_ROLE_STUDENT = ugettext_noop('Student')
......
from django.test import TestCase
from nose.plugins.attrib import attr
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from django.test import TestCase
from django_comment_common.models import Role
from models import CourseDiscussionSettings
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from openedx.core.djangoapps.course_groups.cohorts import CourseCohortsSettings
from student.models import CourseEnrollment, User
from utils import get_course_discussion_settings, set_course_discussion_settings
......
......@@ -5,6 +5,7 @@ Common comment client utility functions.
from django_comment_common.models import (
FORUM_ROLE_ADMINISTRATOR,
FORUM_ROLE_COMMUNITY_TA,
FORUM_ROLE_GROUP_MODERATOR,
FORUM_ROLE_MODERATOR,
FORUM_ROLE_STUDENT,
Role
......@@ -28,6 +29,9 @@ STUDENT_ROLE_PERMISSIONS = ["vote", "update_thread", "follow_thread", "unfollow_
MODERATOR_ROLE_PERMISSIONS = ["edit_content", "delete_thread", "openclose_thread",
"endorse_comment", "delete_comment", "see_all_cohorts"]
GROUP_MODERATOR_ROLE_PERMISSIONS = ["group_edit_content", "group_delete_thread", "group_openclose_thread",
"group_endorse_comment", "group_delete_comment"]
ADMINISTRATOR_ROLE_PERMISSIONS = ["manage_moderator"]
......@@ -50,6 +54,7 @@ def seed_permissions_roles(course_key):
"""
administrator_role = _save_forum_role(course_key, FORUM_ROLE_ADMINISTRATOR)
moderator_role = _save_forum_role(course_key, FORUM_ROLE_MODERATOR)
group_moderator_role = _save_forum_role(course_key, FORUM_ROLE_GROUP_MODERATOR)
community_ta_role = _save_forum_role(course_key, FORUM_ROLE_COMMUNITY_TA)
student_role = _save_forum_role(course_key, FORUM_ROLE_STUDENT)
......@@ -59,11 +64,14 @@ def seed_permissions_roles(course_key):
for per in MODERATOR_ROLE_PERMISSIONS:
moderator_role.add_permission(per)
for per in GROUP_MODERATOR_ROLE_PERMISSIONS:
group_moderator_role.add_permission(per)
for per in ADMINISTRATOR_ROLE_PERMISSIONS:
administrator_role.add_permission(per)
moderator_role.inherit_permissions(student_role)
group_moderator_role.inherit_permissions(student_role)
# For now, Community TA == Moderator, except for the styling.
community_ta_role.inherit_permissions(moderator_role)
......@@ -78,6 +86,7 @@ def are_permissions_roles_seeded(course_id):
try:
administrator_role = Role.objects.get(name=FORUM_ROLE_ADMINISTRATOR, course_id=course_id)
moderator_role = Role.objects.get(name=FORUM_ROLE_MODERATOR, course_id=course_id)
group_moderator_role = Role.objects.get(name=FORUM_ROLE_GROUP_MODERATOR, course_id=course_id)
student_role = Role.objects.get(name=FORUM_ROLE_STUDENT, course_id=course_id)
except:
return False
......@@ -90,6 +99,10 @@ def are_permissions_roles_seeded(course_id):
if not moderator_role.has_permission(per):
return False
for per in GROUP_MODERATOR_ROLE_PERMISSIONS + STUDENT_ROLE_PERMISSIONS:
if not group_moderator_role.has_permission(per):
return False
for per in ADMINISTRATOR_ROLE_PERMISSIONS + MODERATOR_ROLE_PERMISSIONS + STUDENT_ROLE_PERMISSIONS:
if not administrator_role.has_permission(per):
return False
......
......@@ -139,7 +139,7 @@ class AutoAuthEnabledTestCase(AutoAuthTestCase):
def test_set_roles(self, course_id, course_key):
seed_permissions_roles(course_key)
course_roles = dict((r.name, r) for r in Role.objects.filter(course_id=course_key))
self.assertEqual(len(course_roles), 4) # sanity check
self.assertEqual(len(course_roles), 5) # sanity check
# Student role is assigned by default on course enrollment.
self._auto_auth({'username': 'a_student', 'course_id': course_id})
......
......@@ -28,6 +28,8 @@ from django_comment_client.utils import (
get_annotated_content_info,
get_cached_discussion_id_map,
get_group_id_for_comments_service,
get_group_id_for_user,
get_user_group_ids,
is_comment_too_deep,
prepare_content
)
......@@ -169,6 +171,8 @@ def permitted(func):
"""
Extract the forum object from the keyword arguments to the view.
"""
user_group_id = None
content_user_group_id = None
if "thread_id" in kwargs:
content = cc.Thread.find(kwargs["thread_id"]).to_dict()
elif "comment_id" in kwargs:
......@@ -177,9 +181,16 @@ def permitted(func):
content = cc.Commentable.find(kwargs["commentable_id"]).to_dict()
else:
content = None
return content
if 'username' in content:
(user_group_id, content_user_group_id) = get_user_group_ids(course_key, content, request.user)
return content, user_group_id, content_user_group_id
course_key = CourseKey.from_string(kwargs['course_id'])
if check_permissions_by_view(request.user, course_key, fetch_content(), request.view_name):
content, user_group_id, content_user_group_id = fetch_content()
if check_permissions_by_view(request.user, course_key, content,
request.view_name, user_group_id, content_user_group_id):
return func(request, *args, **kwargs)
else:
return JsonError("unauthorized", status=401)
......@@ -203,7 +214,7 @@ def ajax_content_response(request, course_key, content):
@permitted
def create_thread(request, course_id, commentable_id):
"""
Given a course and commentble ID, create the thread
Given a course and commentable ID, create the thread
"""
log.debug("Creating new thread in %r, id %r", course_id, commentable_id)
......
......@@ -7,7 +7,8 @@ from types import NoneType
from opaque_keys.edx.keys import CourseKey
from django_comment_common.models import all_permissions_for_user_in_course
from django_comment_common.models import CourseDiscussionSettings, all_permissions_for_user_in_course
from django_comment_common.utils import get_course_discussion_settings
from lms.djangoapps.teams.models import CourseTeam
from lms.lib.comment_client import Thread
from request_cache.middleware import RequestCache, request_cached
......@@ -44,6 +45,7 @@ def get_team(commentable_id):
def _check_condition(user, condition, content):
""" Check whether or not the given condition applies for the given user and content. """
def check_open(_user, content):
""" Check whether the content is open. """
try:
......@@ -106,7 +108,7 @@ def _check_condition(user, condition, content):
return handlers[condition](user, content)
def _check_conditions_permissions(user, permissions, course_id, content):
def _check_conditions_permissions(user, permissions, course_id, content, user_group_id=None, content_user_group=None):
"""
Accepts a list of permissions and proceed if any of the permission is valid.
Note that ["can_view", "can_edit"] will proceed if the user has either
......@@ -118,6 +120,17 @@ def _check_conditions_permissions(user, permissions, course_id, content):
if isinstance(per, basestring):
if per in CONDITIONS:
return _check_condition(user, per, content)
if 'group_' in per:
# If a course does not have divided discussions
# or a course has divided discussions, but the current user's content group does not equal
# the content group of the commenter/poster,
# then the current user does not have group edit permissions.
division_scheme = get_course_discussion_settings(course_id).division_scheme
if (division_scheme is CourseDiscussionSettings.NONE
or user_group_id is None
or content_user_group is None
or user_group_id != content_user_group):
return False
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]
......@@ -125,42 +138,50 @@ def _check_conditions_permissions(user, permissions, course_id, content):
return True in results
elif operator == "and":
return False not in results
return test(user, permissions, operator="or")
# Note: 'edit_content' is being used as a generic way of telling if someone is a privileged user
# (forum Moderator/Admin/TA), because there is a desire that team membership does not impact privileged users.
VIEW_PERMISSIONS = {
'update_thread': ['edit_content', ['update_thread', 'is_open', 'is_author']],
'create_comment': ['edit_content', ["create_comment", "is_open", "is_team_member_if_applicable"]],
'delete_thread': ['delete_thread', ['update_thread', 'is_author']],
'update_comment': ['edit_content', ['update_comment', 'is_open', 'is_author']],
'update_thread': ['group_edit_content', 'edit_content', ['update_thread', 'is_open', 'is_author']],
'create_comment': ['group_edit_content', 'edit_content', ["create_comment", "is_open",
"is_team_member_if_applicable"]],
'delete_thread': ['group_delete_thread', 'delete_thread', ['update_thread', 'is_author']],
'update_comment': ['group_edit_content', 'edit_content', ['update_comment', 'is_open', 'is_author']],
'endorse_comment': ['endorse_comment', 'is_question_author'],
'openclose_thread': ['openclose_thread'],
'create_sub_comment': ['edit_content', ['create_sub_comment', 'is_open', 'is_team_member_if_applicable']],
'delete_comment': ['delete_comment', ['update_comment', 'is_open', 'is_author']],
'vote_for_comment': ['edit_content', ['vote', 'is_open', 'is_team_member_if_applicable']],
'undo_vote_for_comment': ['edit_content', ['unvote', 'is_open', 'is_team_member_if_applicable']],
'vote_for_thread': ['edit_content', ['vote', 'is_open', 'is_team_member_if_applicable']],
'flag_abuse_for_thread': ['edit_content', ['vote', 'is_team_member_if_applicable']],
'un_flag_abuse_for_thread': ['edit_content', ['vote', 'is_team_member_if_applicable']],
'flag_abuse_for_comment': ['edit_content', ['vote', 'is_team_member_if_applicable']],
'un_flag_abuse_for_comment': ['edit_content', ['vote', 'is_team_member_if_applicable']],
'undo_vote_for_thread': ['edit_content', ['unvote', 'is_open', 'is_team_member_if_applicable']],
'pin_thread': ['openclose_thread'],
'un_pin_thread': ['openclose_thread'],
'follow_thread': ['edit_content', ['follow_thread', 'is_team_member_if_applicable']],
'follow_commentable': ['edit_content', ['follow_commentable', 'is_team_member_if_applicable']],
'unfollow_thread': ['edit_content', ['unfollow_thread', 'is_team_member_if_applicable']],
'unfollow_commentable': ['edit_content', ['unfollow_commentable', 'is_team_member_if_applicable']],
'create_thread': ['edit_content', ['create_thread', 'is_team_member_if_applicable']],
'openclose_thread': ['group_openclose_thread', 'openclose_thread'],
'create_sub_comment': ['group_edit_content', 'edit_content', ['create_sub_comment', 'is_open',
'is_team_member_if_applicable']],
'delete_comment': ['group_delete_comment', 'delete_comment', ['update_comment', 'is_open', 'is_author']],
'vote_for_comment': ['group_edit_content', 'edit_content', ['vote', 'is_open', 'is_team_member_if_applicable']],
'undo_vote_for_comment': ['group_edit_content', 'edit_content', ['unvote', 'is_open',
'is_team_member_if_applicable']],
'vote_for_thread': ['group_edit_content', 'edit_content', ['vote', 'is_open', 'is_team_member_if_applicable']],
'flag_abuse_for_thread': ['group_edit_content', 'edit_content', ['vote', 'is_team_member_if_applicable']],
'un_flag_abuse_for_thread': ['group_edit_content', 'edit_content', ['vote', 'is_team_member_if_applicable']],
'flag_abuse_for_comment': ['group_edit_content', 'edit_content', ['vote', 'is_team_member_if_applicable']],
'un_flag_abuse_for_comment': ['group_edit_content', 'edit_content', ['vote', 'is_team_member_if_applicable']],
'undo_vote_for_thread': ['group_edit_content', 'edit_content', ['unvote', 'is_open',
'is_team_member_if_applicable']],
'pin_thread': ['group_openclose_thread', 'openclose_thread'],
'un_pin_thread': ['group_openclose_thread', 'openclose_thread'],
'follow_thread': ['group_edit_content', 'edit_content', ['follow_thread', 'is_team_member_if_applicable']],
'follow_commentable': ['group_edit_content', 'edit_content', ['follow_commentable',
'is_team_member_if_applicable']],
'unfollow_thread': ['group_edit_content', 'edit_content', ['unfollow_thread', 'is_team_member_if_applicable']],
'unfollow_commentable': ['group_edit_content', 'edit_content', ['unfollow_commentable',
'is_team_member_if_applicable']],
'create_thread': ['group_edit_content', 'edit_content', ['create_thread', 'is_team_member_if_applicable']],
}
def check_permissions_by_view(user, course_id, content, name):
def check_permissions_by_view(user, course_id, content, name, group_id=None, content_user_group=None):
assert isinstance(course_id, CourseKey)
p = None
try:
p = VIEW_PERMISSIONS[name]
except KeyError:
logging.warning("Permission for view named %s does not exist in permissions.py", name)
return _check_conditions_permissions(user, p, course_id, content)
return _check_conditions_permissions(user, p, course_id, content, group_id, content_user_group)
......@@ -4,6 +4,7 @@ import json
import ddt
import mock
from django.core.management import call_command
from django.core.urlresolvers import reverse
from django.test import RequestFactory, TestCase
from django.utils.timezone import UTC as django_utc
......@@ -20,10 +21,20 @@ from django_comment_client.constants import TYPE_ENTRY, TYPE_SUBCATEGORY
from django_comment_client.tests.factories import RoleFactory
from django_comment_client.tests.unicode import UnicodeTestMixin
from django_comment_client.tests.utils import config_course_discussions, topic_name_to_id
from django_comment_common.models import CourseDiscussionSettings, ForumsConfig
from django_comment_common.utils import get_course_discussion_settings, set_course_discussion_settings
from django_comment_common.models import (
FORUM_ROLE_GROUP_MODERATOR,
CourseDiscussionSettings,
ForumsConfig,
Role,
assign_role
)
from django_comment_common.utils import (
get_course_discussion_settings,
seed_permissions_roles,
set_course_discussion_settings
)
from edxmako import add_lookup
from lms.djangoapps.teams.tests.factories import CourseTeamFactory
from lms.djangoapps.teams.tests.factories import CourseTeamFactory, CourseTeamMembershipFactory
from lms.lib.comment_client.utils import CommentClientMaintenanceError, perform_request
from openedx.core.djangoapps.content.course_structures.models import CourseStructure
from openedx.core.djangoapps.course_groups import cohorts
......@@ -1681,6 +1692,139 @@ class PermissionsTestCase(ModuleStoreTestCase):
self.assertFalse(utils.is_content_authored_by(content, user))
class GroupModeratorPermissionsTestCase(ModuleStoreTestCase):
"""Test utils functionality related to forums "abilities" (permissions) for group moderators"""
def _check_condition(user, condition, content):
return True if condition == 'is_open' or condition == 'is_team_member_if_applicable' else False
def setUp(self):
super(GroupModeratorPermissionsTestCase, self).setUp()
# Create course, seed permissions roles, and create team
self.course = CourseFactory.create()
seed_permissions_roles(self.course.id)
# Create four users: group_moderator (who is within the verified enrollment track and in the cohort),
# verified_user (who is in the verified enrollment track but not the cohort),
# cohorted_user (who is in the cohort but not the verified enrollment track),
# and plain_user (who is neither in the cohort nor the verified enrollment track)x
self.group_moderator = UserFactory(username='group_moderator', email='group_moderator@edx.org')
self.group_moderator.id = 1
CourseEnrollmentFactory(
course_id=self.course.id,
user=self.group_moderator,
mode=CourseMode.VERIFIED
)
self.verified_user = UserFactory(username='verified', email='verified@edx.org')
self.verified_user.id = 2
CourseEnrollmentFactory(
course_id=self.course.id,
user=self.verified_user,
mode=CourseMode.VERIFIED
)
self.cohorted_user = UserFactory(username='cohort', email='cohort@edx.org')
self.cohorted_user.id = 3
CourseEnrollmentFactory(
course_id=self.course.id,
user=self.cohorted_user,
mode=CourseMode.AUDIT
)
self.plain_user = UserFactory(username='plain', email='plain@edx.org')
self.plain_user.id = 4
CourseEnrollmentFactory(
course_id=self.course.id,
user=self.plain_user,
mode=CourseMode.AUDIT
)
CohortFactory(
course_id=self.course.id,
name='Test Cohort',
users=[self.verified_user, self.cohorted_user]
)
# Give group moderator permissions to group_moderator
assign_role(self.course.id, self.group_moderator, 'Group Moderator')
@mock.patch('django_comment_client.permissions._check_condition', side_effect=_check_condition)
def test_not_divided(self, check_condition_function):
"""
Group moderator should not have moderator permissions if the discussions are not divided.
"""
content = {'user_id': self.plain_user.id, 'type': 'thread', 'username': self.plain_user.username}
self.assertEqual(utils.get_ability(self.course.id, content, self.group_moderator), {
'editable': False,
'can_reply': True,
'can_delete': False,
'can_openclose': False,
'can_vote': True,
'can_report': True
})
content = {'user_id': self.cohorted_user.id, 'type': 'thread'}
self.assertEqual(utils.get_ability(self.course.id, content, self.group_moderator), {
'editable': False,
'can_reply': True,
'can_delete': False,
'can_openclose': False,
'can_vote': True,
'can_report': True
})
content = {'user_id': self.verified_user.id, 'type': 'thread'}
self.assertEqual(utils.get_ability(self.course.id, content, self.group_moderator), {
'editable': False,
'can_reply': True,
'can_delete': False,
'can_openclose': False,
'can_vote': True,
'can_report': True
})
@mock.patch('django_comment_client.permissions._check_condition', side_effect=_check_condition)
def test_divided_within_group(self, check_condition_function):
"""
Group moderator should have moderator permissions within their group if the discussions are divided.
"""
set_discussion_division_settings(self.course.id, enable_cohorts=True,
division_scheme=CourseDiscussionSettings.COHORT)
content = {'user_id': self.cohorted_user.id, 'type': 'thread', 'username': self.cohorted_user.username}
self.assertEqual(utils.get_ability(self.course.id, content, self.group_moderator), {
'editable': True,
'can_reply': True,
'can_delete': True,
'can_openclose': True,
'can_vote': True,
'can_report': True
})
set_discussion_division_settings(self.course.id, division_scheme=CourseDiscussionSettings.ENROLLMENT_TRACK)
content = {'user_id': self.verified_user.id, 'type': 'thread', 'username': self.verified_user.username}
self.assertEqual(utils.get_ability(self.course.id, content, self.group_moderator), {
'editable': True,
'can_reply': True,
'can_delete': True,
'can_openclose': True,
'can_vote': True,
'can_report': True
})
@mock.patch('django_comment_client.permissions._check_condition', side_effect=_check_condition)
def test_divided_outside_group(self, check_condition_function):
"""
Group moderator should not have moderator permissions outside of their group.
"""
content = {'user_id': self.plain_user.id, 'type': 'thread', 'username': self.plain_user.username}
set_discussion_division_settings(self.course.id, division_scheme=CourseDiscussionSettings.NONE)
self.assertEqual(utils.get_ability(self.course.id, content, self.group_moderator), {
'editable': False,
'can_reply': True,
'can_delete': False,
'can_openclose': False,
'can_vote': True,
'can_report': True
})
class ClientConfigurationTestCase(TestCase):
"""Simple test cases to ensure enabling/disabling the use of the comment service works as intended."""
......
......@@ -25,6 +25,7 @@ from edxmako import lookup_template
from openedx.core.djangoapps.content.course_structures.models import CourseStructure
from openedx.core.djangoapps.course_groups.cohorts import get_cohort_id, get_cohort_names, is_course_cohorted
from request_cache.middleware import request_cached
from student.models import get_user_by_username_or_email
from student.roles import GlobalStaff
from xmodule.modulestore.django import modulestore
from xmodule.partitions.partitions import ENROLLMENT_TRACK_PARTITION_ID
......@@ -516,11 +517,33 @@ def get_ability(course_id, content, user):
"""
Return a dictionary of forums-oriented actions and the user's permission to perform them
"""
(user_group_id, content_user_group_id) = get_user_group_ids(course_id, content, user)
return {
'editable': check_permissions_by_view(user, course_id, content, "update_thread" if content['type'] == 'thread' else "update_comment"),
'editable': check_permissions_by_view(
user,
course_id,
content,
"update_thread" if content['type'] == 'thread' else "update_comment",
user_group_id,
content_user_group_id
),
'can_reply': check_permissions_by_view(user, course_id, content, "create_comment" if content['type'] == 'thread' else "create_sub_comment"),
'can_delete': check_permissions_by_view(user, course_id, content, "delete_thread" if content['type'] == 'thread' else "delete_comment"),
'can_openclose': check_permissions_by_view(user, course_id, content, "openclose_thread") if content['type'] == 'thread' else False,
'can_delete': check_permissions_by_view(
user,
course_id,
content,
"delete_thread" if content['type'] == 'thread' else "delete_comment",
user_group_id,
content_user_group_id
),
'can_openclose': check_permissions_by_view(
user,
course_id,
content,
"openclose_thread" if content['type'] == 'thread' else False,
user_group_id,
content_user_group_id
),
'can_vote': not is_content_authored_by(content, user) and check_permissions_by_view(
user,
course_id,
......@@ -538,6 +561,25 @@ def get_ability(course_id, content, user):
# TODO: RENAME
def get_user_group_ids(course_id, content, user=None):
"""
Given a user, course ID, and the content of the thread or comment, returns the group ID for the current user
and the user that posted the thread/comment.
"""
content_user_group_id = None
user_group_id = None
if course_id is not None:
if content.get('username'):
try:
content_user = get_user_by_username_or_email(content.get('username'))
content_user_group_id = get_group_id_for_user(content_user, get_course_discussion_settings(course_id))
except User.DoesNotExist:
content_user_group_id = None
user_group_id = get_group_id_for_user(user, get_course_discussion_settings(course_id)) if user else None
return user_group_id, content_user_group_id
def get_annotated_content_info(course_id, content, user, user_info):
"""
Get metadata for an individual content (thread or comment)
......@@ -780,7 +822,7 @@ def get_group_id_for_user(user, course_discussion_settings):
elif division_scheme == CourseDiscussionSettings.ENROLLMENT_TRACK:
partition_service = PartitionService(course_discussion_settings.course_id)
group_id = partition_service.get_user_group_id_for_partition(user, ENROLLMENT_TRACK_PARTITION_ID)
# We negate the group_ids from dynamic partitions so that they will not conflict
# We negate the group_ids from dynamic [partitions so that they will not conflict
# with cohort IDs (which are an auto-incrementing integer field, starting at 1).
return -1 * group_id if group_id is not None else None
else:
......
......@@ -46,7 +46,13 @@ from courseware.access import has_access
from courseware.courses import get_course_by_id, get_course_with_access
from courseware.models import StudentModule
from django_comment_client.utils import has_forum_access
from django_comment_common.models import FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_COMMUNITY_TA, FORUM_ROLE_MODERATOR, Role
from django_comment_common.models import (
Role,
FORUM_ROLE_ADMINISTRATOR,
FORUM_ROLE_MODERATOR,
FORUM_ROLE_GROUP_MODERATOR,
FORUM_ROLE_COMMUNITY_TA,
)
from edxmako.shortcuts import render_to_string
from lms.djangoapps.instructor.access import ROLES, allow_access, list_with_level, revoke_access, update_forum_role
from lms.djangoapps.instructor.enrollment import (
......@@ -2487,7 +2493,8 @@ def list_forum_members(request, course_id):
return HttpResponseBadRequest("Operation requires instructor access.")
# filter out unsupported for roles
if rolename not in [FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA]:
if rolename not in [FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_GROUP_MODERATOR,
FORUM_ROLE_COMMUNITY_TA]:
return HttpResponseBadRequest(strip_tags(
"Unrecognized rolename '{}'.".format(rolename)
))
......@@ -2610,7 +2617,8 @@ def update_forum_role_membership(request, course_id):
Query parameters:
- `email` is the target users email
- `rolename` is one of [FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA]
- `rolename` is one of [FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_GROUP_MODERATOR,
FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA]
- `action` is one of ['allow', 'revoke']
"""
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
......@@ -2634,7 +2642,8 @@ def update_forum_role_membership(request, course_id):
if rolename == FORUM_ROLE_ADMINISTRATOR and not has_instructor_access:
return HttpResponseBadRequest("Operation requires instructor access.")
if rolename not in [FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA]:
if rolename not in [FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_GROUP_MODERATOR,
FORUM_ROLE_COMMUNITY_TA]:
return HttpResponseBadRequest(strip_tags(
"Unrecognized rolename '{}'.".format(rolename)
))
......
......@@ -249,6 +249,20 @@ from django.utils.translation import ugettext as _
></div>
<div class="auth-list-container"
data-rolename="Group Moderator"
data-display-name="${_("Discussion Group Moderators")}"
data-info-text="
${_("Discussion Group Moderators can edit or delete any post, clear misuse flags, close "
"and re-open threads, endorse responses, and see posts from all groups. "
"Their posts are marked as 'staff'. They cannot manage course team membership by "
"adding or removing discussion moderation roles. Only enrolled users can be "
"added as Discussion Moderators.")}"
data-list-endpoint="${ section_data['list_forum_members_url'] }"
data-modify-endpoint="${ section_data['update_forum_role_membership_url'] }"
data-add-button-label="${_("Add Group Moderator")}"
></div>
<div class="auth-list-container"
data-rolename="Community TA"
data-display-name="${_("Discussion Community TAs")}"
data-info-text="
......
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