diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py index c3e98ced72..2dc803d47b 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py @@ -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, diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/course_team.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/course_team.py new file mode 100644 index 0000000000..bd5da6625a --- /dev/null +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/course_team.py @@ -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() diff --git a/cms/djangoapps/contentstore/rest_api/v1/urls.py b/cms/djangoapps/contentstore/rest_api/v1/urls.py index 277677f393..ed1e4d63e1 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/urls.py +++ b/cms/djangoapps/contentstore/rest_api/v1/urls.py @@ -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(), diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py b/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py index 34d7c0bfb6..0e584debaa 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py @@ -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 diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/course_team.py b/cms/djangoapps/contentstore/rest_api/v1/views/course_team.py new file mode 100644 index 0000000000..5b8f7d200a --- /dev/null +++ b/cms/djangoapps/contentstore/rest_api/v1/views/course_team.py @@ -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) diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_course_team.py b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_course_team.py new file mode 100644 index 0000000000..c0abca0819 --- /dev/null +++ b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_course_team.py @@ -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) diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index 6f2835887d..198a48b488 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -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. diff --git a/cms/djangoapps/contentstore/views/user.py b/cms/djangoapps/contentstore/views/user.py index d97c8c24fa..80a09db96d 100644 --- a/cms/djangoapps/contentstore/views/user.py +++ b/cms/djangoapps/contentstore/views/user.py @@ -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