feat: Create DRF for course team (#32782)

This commit is contained in:
ruzniaievdm
2023-08-17 16:12:02 +03:00
committed by GitHub
parent 59782fa625
commit ddb092c07c
8 changed files with 213 additions and 20 deletions

View File

@@ -2,6 +2,7 @@
Serializers for v1 contentstore API.
"""
from .course_details import CourseDetailsSerializer
from .course_team import CourseTeamSerializer
from .grading import CourseGradingModelSerializer, CourseGradingSerializer
from .proctoring import (
LimitedProctoredExamSettingsSerializer,

View File

@@ -0,0 +1,20 @@
"""
API Serializers for course team
"""
from rest_framework import serializers
class UserCourseTeamSerializer(serializers.Serializer):
"""Serializer for user in course team"""
email = serializers.CharField()
id = serializers.IntegerField()
role = serializers.CharField()
username = serializers.CharField()
class CourseTeamSerializer(serializers.Serializer):
"""Serializer for course team context data"""
show_transfer_ownership_hint = serializers.BooleanField()
users = UserCourseTeamSerializer(many=True)
allow_actions = serializers.BooleanField()

View File

@@ -8,6 +8,7 @@ from openedx.core.constants import COURSE_ID_PATTERN
from .views import (
CourseDetailsView,
CourseTeamView,
CourseGradingView,
CourseSettingsView,
ProctoredExamSettingsView,
@@ -43,6 +44,11 @@ urlpatterns = [
CourseDetailsView.as_view(),
name="course_details"
),
re_path(
fr'^course_team/{COURSE_ID_PATTERN}$',
CourseTeamView.as_view(),
name="course_team"
),
re_path(
fr'^course_grading/{COURSE_ID_PATTERN}$',
CourseGradingView.as_view(),

View File

@@ -2,6 +2,7 @@
Views for v1 contentstore API.
"""
from .course_details import CourseDetailsView
from .course_team import CourseTeamView
from .grading import CourseGradingView
from .proctoring import ProctoredExamSettingsView, ProctoringErrorsView
from .settings import CourseSettingsView

View File

@@ -0,0 +1,74 @@
""" API Views for course team """
import edx_api_doc_tools as apidocs
from opaque_keys.edx.keys import CourseKey
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView
from cms.djangoapps.contentstore.utils import get_course_team
from common.djangoapps.student.auth import STUDIO_VIEW_USERS, get_user_permissions
from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, verify_course_exists, view_auth_classes
from ..serializers import CourseTeamSerializer
@view_auth_classes(is_authenticated=True)
class CourseTeamView(DeveloperErrorViewMixin, APIView):
"""
View for getting data for course team.
"""
@apidocs.schema(
parameters=[
apidocs.string_parameter("course_id", apidocs.ParameterLocation.PATH, description="Course ID"),
],
responses={
200: CourseTeamSerializer,
401: "The requester is not authenticated.",
403: "The requester cannot access the specified course.",
404: "The requested course does not exist.",
},
)
@verify_course_exists()
def get(self, request: Request, course_id: str):
"""
Get all CMS users who are editors for the specified course.
**Example Request**
GET /api/contentstore/v1/course_team/{course_id}
**Response Values**
If the request is successful, an HTTP 200 "OK" response is returned.
The HTTP 200 response contains a single dict that contains keys that
are the course's team info.
**Example Response**
```json
{
"show_transfer_ownership_hint": true,
"users": [
{
"email": "edx@example.com",
"id": "3",
"role": "instructor",
"username": "edx"
},
],
"allow_actions": true
}
```
"""
user = request.user
course_key = CourseKey.from_string(course_id)
user_perms = get_user_permissions(user, course_key)
if not user_perms & STUDIO_VIEW_USERS:
self.permission_denied(request)
course_team_context = get_course_team(user, course_key, user_perms)
serializer = CourseTeamSerializer(course_team_context)
return Response(serializer.data)

View File

@@ -0,0 +1,78 @@
"""
Unit tests for course team.
"""
import ddt
from django.urls import reverse
from rest_framework import status
from common.djangoapps.student.roles import CourseInstructorRole, CourseStaffRole
from common.djangoapps.student.tests.factories import UserFactory
from cms.djangoapps.contentstore.tests.utils import CourseTestCase
from ...mixins import PermissionAccessMixin
@ddt.ddt
class CourseTeamViewTest(CourseTestCase, PermissionAccessMixin):
"""
Tests for CourseTeamView.
"""
def setUp(self):
super().setUp()
self.url = reverse(
"cms.djangoapps.contentstore:v1:course_team",
kwargs={"course_id": self.course.id},
)
def get_expected_course_data(self, instructor=None, staff=None):
"""Utils is used to get expected data for course team"""
users = []
if instructor:
users.append({
"email": instructor.email,
"id": instructor.id,
"role": "instructor",
"username": instructor.username
})
if staff:
users.append({
"email": staff.email,
"id": staff.id,
"role": "staff",
"username": staff.username
})
return {
"show_transfer_ownership_hint": False,
"users": users,
"allow_actions": True,
}
def create_course_user_roles(self, course_id):
"""Get course staff and instructor roles user"""
instructor = UserFactory()
CourseInstructorRole(course_id).add_users(instructor)
staff = UserFactory()
CourseStaffRole(course_id).add_users(staff)
return instructor, staff
def test_course_team_response(self):
"""Check successful response content"""
response = self.client.get(self.url)
expected_response = self.get_expected_course_data()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertDictEqual(expected_response, response.data)
def test_users_response(self):
"""Test the response for users in the course."""
instructor, staff = self.create_course_user_roles(self.course.id)
response = self.client.get(self.url)
users_response = [dict(item) for item in response.data["users"]]
expected_response = self.get_expected_course_data(instructor, staff)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertListEqual(expected_response["users"], users_response)

View File

@@ -26,7 +26,7 @@ from cms.djangoapps.contentstore.toggles import exam_setting_view_enabled
from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.edxmako.services import MakoService
from common.djangoapps.student import auth
from common.djangoapps.student.auth import has_studio_read_access, has_studio_write_access
from common.djangoapps.student.auth import has_studio_read_access, has_studio_write_access, STUDIO_EDIT_ROLES
from common.djangoapps.student.models import CourseEnrollment
from common.djangoapps.student.roles import (
CourseInstructorRole,
@@ -1323,6 +1323,35 @@ def get_course_settings(request, course_key, course_block):
return settings_context
def get_course_team(auth_user, course_key, user_perms):
"""
Utils is used to get context of all CMS users who are editors for the specified course.
It is used for both DRF and django views.
"""
from cms.djangoapps.contentstore.views.user import user_with_role
course_block = modulestore().get_course(course_key)
instructors = set(CourseInstructorRole(course_key).users_with_role())
# the page only lists staff and assumes they're a superset of instructors. Do a union to ensure.
staff = set(CourseStaffRole(course_key).users_with_role()).union(instructors)
formatted_users = []
for user in instructors:
formatted_users.append(user_with_role(user, 'instructor'))
for user in staff - instructors:
formatted_users.append(user_with_role(user, 'staff'))
course_team_context = {
'context_course': course_block,
'show_transfer_ownership_hint': auth_user in instructors and len(instructors) == 1,
'users': formatted_users,
'allow_actions': bool(user_perms & STUDIO_EDIT_ROLES),
}
return course_team_context
def get_course_grading(course_key):
"""
Utils is used to get context of course grading.

View File

@@ -19,10 +19,9 @@ from common.djangoapps.student.auth import STUDIO_EDIT_ROLES, STUDIO_VIEW_USERS,
from common.djangoapps.student.models import CourseEnrollment
from common.djangoapps.student.roles import CourseInstructorRole, CourseStaffRole, LibraryUserRole
from common.djangoapps.util.json_request import JsonResponse, expect_json
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
from ..toggles import use_new_course_team_page
from ..utils import get_course_team_url
from ..utils import get_course_team_url, get_course_team
__all__ = ['request_course_creator', 'course_team_handler']
@@ -85,23 +84,8 @@ def _manage_users(request, course_key):
if not user_perms & STUDIO_VIEW_USERS:
raise PermissionDenied()
course_block = modulestore().get_course(course_key)
instructors = set(CourseInstructorRole(course_key).users_with_role())
# the page only lists staff and assumes they're a superset of instructors. Do a union to ensure.
staff = set(CourseStaffRole(course_key).users_with_role()).union(instructors)
formatted_users = []
for user in instructors:
formatted_users.append(user_with_role(user, 'instructor'))
for user in staff - instructors:
formatted_users.append(user_with_role(user, 'staff'))
return render_to_response('manage_users.html', {
'context_course': course_block,
'show_transfer_ownership_hint': request.user in instructors and len(instructors) == 1,
'users': formatted_users,
'allow_actions': bool(user_perms & STUDIO_EDIT_ROLES),
})
manage_users_context = get_course_team(request.user, course_key, user_perms)
return render_to_response('manage_users.html', manage_users_context)
@expect_json