Skip to content

Commit

Permalink
feat: Create DRF for course team (openedx#32782)
Browse files Browse the repository at this point in the history
  • Loading branch information
ruzniaievdm authored Aug 17, 2023
1 parent 59782fa commit ddb092c
Show file tree
Hide file tree
Showing 8 changed files with 213 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
20 changes: 20 additions & 0 deletions cms/djangoapps/contentstore/rest_api/v1/serializers/course_team.py
Original file line number Diff line number Diff line change
@@ -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()
6 changes: 6 additions & 0 deletions cms/djangoapps/contentstore/rest_api/v1/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from .views import (
CourseDetailsView,
CourseTeamView,
CourseGradingView,
CourseSettingsView,
ProctoredExamSettingsView,
Expand Down Expand Up @@ -43,6 +44,11 @@
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(),
Expand Down
1 change: 1 addition & 0 deletions cms/djangoapps/contentstore/rest_api/v1/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
74 changes: 74 additions & 0 deletions cms/djangoapps/contentstore/rest_api/v1/views/course_team.py
Original file line number Diff line number Diff line change
@@ -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": "[email protected]",
"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)
Original file line number Diff line number Diff line change
@@ -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)
31 changes: 30 additions & 1 deletion cms/djangoapps/contentstore/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
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,
Expand Down Expand Up @@ -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.
Expand Down
22 changes: 3 additions & 19 deletions cms/djangoapps/contentstore/views/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,9 @@
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']

Expand Down Expand Up @@ -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
Expand Down

0 comments on commit ddb092c

Please sign in to comment.