Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
6cdcf8e9
Commit
6cdcf8e9
authored
Jun 13, 2017
by
Albert St. Aubin
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Discussion group moderation
parent
8fb86474
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
299 additions
and
44 deletions
+299
-44
common/djangoapps/django_comment_common/models.py
+1
-0
common/djangoapps/django_comment_common/tests.py
+2
-2
common/djangoapps/django_comment_common/utils.py
+14
-1
common/djangoapps/student/tests/test_auto_auth.py
+1
-1
lms/djangoapps/django_comment_client/base/tests.py
+0
-0
lms/djangoapps/django_comment_client/base/views.py
+14
-3
lms/djangoapps/django_comment_client/permissions.py
+47
-26
lms/djangoapps/django_comment_client/tests/test_utils.py
+147
-3
lms/djangoapps/django_comment_client/utils.py
+46
-4
lms/djangoapps/instructor/views/api.py
+13
-4
lms/templates/instructor/instructor_dashboard_2/membership.html
+14
-0
No files found.
common/djangoapps/django_comment_common/models.py
View file @
6cdcf8e9
...
...
@@ -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'
)
...
...
common/djangoapps/django_comment_common/tests.py
View file @
6cdcf8e9
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
...
...
common/djangoapps/django_comment_common/utils.py
View file @
6cdcf8e9
...
...
@@ -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
...
...
common/djangoapps/student/tests/test_auto_auth.py
View file @
6cdcf8e9
...
...
@@ -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
})
...
...
lms/djangoapps/django_comment_client/base/tests.py
View file @
6cdcf8e9
This diff is collapsed.
Click to expand it.
lms/djangoapps/django_comment_client/base/views.py
View file @
6cdcf8e9
...
...
@@ -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 comment
a
ble ID, create the thread
"""
log
.
debug
(
"Creating new thread in
%
r, id
%
r"
,
course_id
,
commentable_id
)
...
...
lms/djangoapps/django_comment_client/permissions.py
View file @
6cdcf8e9
...
...
@@ -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
)
lms/djangoapps/django_comment_client/tests/test_utils.py
View file @
6cdcf8e9
...
...
@@ -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."""
...
...
lms/djangoapps/django_comment_client/utils.py
View file @
6cdcf8e9
...
...
@@ -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
:
...
...
lms/djangoapps/instructor/views/api.py
View file @
6cdcf8e9
...
...
@@ -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
)
))
...
...
lms/templates/instructor/instructor_dashboard_2/membership.html
View file @
6cdcf8e9
...
...
@@ -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=
"
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment