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:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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'],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user