feat: return user roles and privilege information in course metadata (#29448)

The frontends need to be aware of a user's privileges in order to know what operations are supported. This adds the user roles and privilege information to the discussion metadata API.
This commit is contained in:
Kshitij Sobti
2021-12-06 06:21:25 +00:00
committed by GitHub
parent ed73fd3b29
commit 606fb95059
6 changed files with 122 additions and 39 deletions

View File

@@ -1,6 +1,8 @@
# pylint: skip-file
import json
import logging
from typing import Set
import regex
from collections import defaultdict
from datetime import datetime
@@ -85,6 +87,26 @@ def get_role_ids(course_id):
return {role.name: list(role.users.values_list('id', flat=True)) for role in roles}
def get_user_role_names(user: User, course_key: CourseKey) -> Set[str]:
"""
Get a set of discussion roles a user has for the specified course.
Args:
user (User): a user
course_key (CourseKey): a course key
Returns:
(Set[str]) a set of role names that the user has.
"""
return set(
Role.objects.filter(
users=user,
course_id=course_key,
).values_list('name', flat=True).distinct()
)
def has_discussion_privileges(user, course_id):
"""
Returns True if the user is privileged in teams discussions for

View File

@@ -25,7 +25,12 @@ from openedx.core.djangoapps.django_comment_common.comment_client.comment import
from openedx.core.djangoapps.django_comment_common.comment_client.course import get_course_commentable_counts
from openedx.core.djangoapps.django_comment_common.comment_client.thread import Thread
from openedx.core.djangoapps.django_comment_common.comment_client.utils import CommentClientRequestError
from openedx.core.djangoapps.django_comment_common.models import CourseDiscussionSettings
from openedx.core.djangoapps.django_comment_common.models import (
CourseDiscussionSettings,
FORUM_ROLE_ADMINISTRATOR,
FORUM_ROLE_COMMUNITY_TA,
FORUM_ROLE_MODERATOR,
)
from openedx.core.djangoapps.django_comment_common.signals import (
comment_created,
comment_deleted,
@@ -39,6 +44,7 @@ from openedx.core.djangoapps.django_comment_common.signals import (
from openedx.core.djangoapps.user_api.accounts.api import get_account_settings
from openedx.core.lib.exceptions import CourseNotFoundError, DiscussionNotFoundError, PageNotFoundError
from xmodule.course_module import CourseBlock
from xmodule.tabs import CourseTabList
from .exceptions import (
CommentNotFoundError,
DiscussionBlackOutException,
@@ -67,9 +73,9 @@ from ..django_comment_client.base.views import (
)
from ..django_comment_client.utils import (
get_group_id_for_user,
get_user_role_names,
is_commentable_divided,
)
from xmodule.tabs import CourseTabList
User = get_user_model()
@@ -247,6 +253,7 @@ def get_course(request, course_key):
return dt.isoformat().replace('+00:00', 'Z')
course = _get_course(course_key, request.user)
user_roles = get_user_role_names(request.user, course_key)
return {
"id": str(course_key),
"blackouts": [
@@ -263,6 +270,12 @@ def get_course(request, course_key):
),
"allow_anonymous": course.allow_anonymous,
"allow_anonymous_to_peers": course.allow_anonymous_to_peers,
"user_roles": user_roles,
"user_is_privileged": bool(user_roles & {
FORUM_ROLE_ADMINISTRATOR,
FORUM_ROLE_MODERATOR,
FORUM_ROLE_COMMUNITY_TA,
})
}

View File

@@ -18,7 +18,6 @@ from lms.djangoapps.discussion.django_comment_client.utils import (
get_group_name,
is_comment_too_deep,
)
from openedx.core.djangoapps.discussions.utils import get_group_names_by_id
from lms.djangoapps.discussion.rest_api.permissions import (
NON_UPDATABLE_COMMENT_FIELDS,
NON_UPDATABLE_THREAD_FIELDS,
@@ -26,6 +25,7 @@ from lms.djangoapps.discussion.rest_api.permissions import (
get_editable_fields,
)
from lms.djangoapps.discussion.rest_api.render import render_body
from openedx.core.djangoapps.discussions.utils import get_group_names_by_id
from openedx.core.djangoapps.django_comment_common.comment_client.comment import Comment
from openedx.core.djangoapps.django_comment_common.comment_client.thread import Thread
from openedx.core.djangoapps.django_comment_common.comment_client.user import User as CommentClientUser
@@ -37,6 +37,7 @@ from openedx.core.djangoapps.django_comment_common.models import (
FORUM_ROLE_MODERATOR,
Role,
)
from openedx.core.lib.api.serializers import CourseKeyField
User = get_user_model()
@@ -631,3 +632,43 @@ class DiscussionRolesListSerializer(serializers.Serializer):
Overriden update abstract method
"""
pass # lint-amnesty, pylint: disable=unnecessary-pass
class BlackoutDateSerializer(serializers.Serializer):
"""
Serializer for blackout dates.
"""
start = serializers.DateTimeField(help_text="The ISO 8601 timestamp for the start of the blackout period")
end = serializers.DateTimeField(help_text="The ISO 8601 timestamp for the end of the blackout period")
class CourseMetadataSerailizer(serializers.Serializer):
"""
Serializer for course metadata.
"""
id = CourseKeyField(help_text="The identifier of the course")
blackouts = serializers.ListField(
child=BlackoutDateSerializer(),
help_text="A list of objects representing blackout periods "
"(during which discussions are read-only except for privileged users)."
)
thread_list_url = serializers.URLField(
help_text="The URL of the list of all threads in the course.",
)
following_thread_list_url = serializers.URLField(
help_text="thread_list_url with parameter following=True",
)
topics_url = serializers.URLField(help_text="The URL of the topic listing for the course.")
allow_anonymous = serializers.BooleanField(
help_text="A boolean which indicating whether anonymous posts are allowed or not.",
)
allow_anonymous_to_peers = serializers.BooleanField(
help_text="A boolean which indicating whether posts anonymous to peers are allowed or not.",
)
user_roles = serializers.ListField(
child=serializers.CharField(),
help_text="A list of all the roles the requesting user has for this course.",
)
user_is_privileged = serializers.BooleanField(
help_text="A boolean indicating if the current user has a privileged role",
)

View File

@@ -139,6 +139,7 @@ def _set_course_discussion_blackout(course, user_id):
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
@ddt.ddt
class GetCourseTest(ForumsEnableMixin, UrlResetMixin, SharedModuleStoreTestCase):
"""Test for get_course"""
@@ -179,8 +180,24 @@ class GetCourseTest(ForumsEnableMixin, UrlResetMixin, SharedModuleStoreTestCase)
'topics_url': 'http://testserver/api/discussion/v1/course_topics/x/y/z',
'allow_anonymous': True,
'allow_anonymous_to_peers': False,
'user_is_privileged': False,
'user_roles': {'Student'},
}
@ddt.data(
FORUM_ROLE_ADMINISTRATOR,
FORUM_ROLE_MODERATOR,
FORUM_ROLE_COMMUNITY_TA,
)
def test_privileged_roles(self, role):
"""
Test that the api returns the correct roles and privileges.
"""
_assign_role_to_user(user=self.user, course_id=self.course.id, role=role)
course_meta = get_course(self.request, self.course.id)
assert course_meta["user_is_privileged"]
assert course_meta["user_roles"] == {FORUM_ROLE_STUDENT} | {role}
@ddt.ddt
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})

View File

@@ -315,6 +315,8 @@ class CourseViewTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
"topics_url": "http://testserver/api/discussion/v1/course_topics/x/y/z",
"allow_anonymous": True,
"allow_anonymous_to_peers": False,
'user_is_privileged': False,
'user_roles': ['Student'],
}
)

View File

@@ -6,9 +6,9 @@ Discussion API views
import logging
import uuid
from django.core.exceptions import BadRequest
import edx_api_doc_tools as apidocs
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.core.exceptions import BadRequest, ValidationError
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
from edx_rest_framework_extensions.auth.session.authentication import SessionAuthenticationAllowInactiveUser
from opaque_keys.edx.keys import CourseKey
@@ -21,8 +21,8 @@ from rest_framework.views import APIView
from rest_framework.viewsets import ViewSet
from common.djangoapps.util.file import store_uploaded_file
from lms.djangoapps.discussion.django_comment_client import settings as cc_settings
from lms.djangoapps.course_goals.models import UserActivity
from lms.djangoapps.discussion.django_comment_client import settings as cc_settings
from lms.djangoapps.instructor.access import update_forum_role
from openedx.core.djangoapps.discussions.serializers import DiscussionSettingsSerializer
from openedx.core.djangoapps.django_comment_common import comment_client
@@ -54,11 +54,12 @@ from ..rest_api.forms import (
CourseDiscussionSettingsForm,
ThreadListGetForm,
)
from ..rest_api.permissions import IsStaffOrCourseTeamOrEnrolled
from ..rest_api.serializers import (
CourseMetadataSerailizer,
DiscussionRolesListSerializer,
DiscussionRolesSerializer,
)
from ..rest_api.permissions import IsStaffOrCourseTeamOrEnrolled
log = logging.getLogger(__name__)
@@ -68,41 +69,28 @@ User = get_user_model()
@view_auth_classes()
class CourseView(DeveloperErrorViewMixin, APIView):
"""
**Use Cases**
Retrieve general discussion metadata for a course.
**Example Requests**:
GET /api/discussion/v1/courses/course-v1:ExampleX+Subject101+2015
**Response Values**:
* id: The identifier of the course
* blackouts: A list of objects representing blackout periods (during
which discussions are read-only except for privileged users). Each
item in the list includes:
* start: The ISO 8601 timestamp for the start of the blackout period
* end: The ISO 8601 timestamp for the end of the blackout period
* thread_list_url: The URL of the list of all threads in the course.
* following_thread_list_url: thread_list_url with parameter following=True
* topics_url: The URL of the topic listing for the course.
* allow_anonymous: A boolean which indicating whether anonymous posts
are allowed or not.
* allow_anonymous_to_peers: A boolean which indicating whether posts
anonymous to peers are allowed or not.
General discussion metadata API.
"""
@apidocs.schema(
parameters=[
apidocs.string_parameter("course_id", apidocs.ParameterLocation.PATH, description="Course ID")
],
responses={
200: CourseMetadataSerailizer(read_only=True, required=False),
401: "The requester is not authenticated.",
403: "The requester cannot access the specified course.",
404: "The requested course does not exist.",
}
)
def get(self, request, course_id):
"""Implements the GET method as described in the class docstring."""
"""
Retrieve general discussion metadata for a course.
**Example Requests**:
GET /api/discussion/v1/courses/course-v1:ExampleX+Subject101+2015
"""
course_key = CourseKey.from_string(course_id) # TODO: which class is right?
# Record user activity for tracking progress towards a user's course goals (for mobile app)
UserActivity.record_user_activity(request.user, course_key, request=request, only_if_mobile_app=True)