Add a GET endpoint to list ProgramCourseEnrollment data.

This commit is contained in:
Alex Dusenbery
2019-05-01 16:23:55 -04:00
committed by Alex Dusenbery
parent 44359938c4
commit 403fb52e93
9 changed files with 352 additions and 101 deletions

View File

@@ -20,8 +20,6 @@ from lms.djangoapps.grades.api.v1.utils import (
USER_MODEL,
CourseEnrollmentPagination,
GradeViewMixin,
get_course_key,
verify_course_exists
)
from lms.djangoapps.grades.config.waffle import WRITABLE_GRADEBOOK, waffle_flags
from lms.djangoapps.grades.constants import ScoreDatabaseTableEnum
@@ -42,7 +40,13 @@ from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey, UsageKey
from openedx.core.djangoapps.course_groups import cohorts
from openedx.core.djangoapps.util.forms import to_bool
from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, PaginatedAPIView, view_auth_classes
from openedx.core.lib.api.view_utils import (
DeveloperErrorViewMixin,
PaginatedAPIView,
get_course_key,
verify_course_exists,
view_auth_classes,
)
from openedx.core.lib.cache_utils import request_cached
from student.auth import has_course_author_access
from student.models import CourseEnrollment

View File

@@ -2,8 +2,8 @@
Define some view level utility functions here that multiple view modules will share
"""
from contextlib import contextmanager
from functools import wraps
from django.contrib.auth import get_user_model
from rest_framework import status
from rest_framework.exceptions import AuthenticationFailed
from rest_framework.pagination import CursorPagination
@@ -11,11 +11,7 @@ from rest_framework.response import Response
from rest_framework.views import APIView
from six import text_type
from django.contrib.auth import get_user_model
from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin
from student.models import CourseEnrollment
from util.query import use_read_replica_if_available
@@ -23,44 +19,6 @@ from util.query import use_read_replica_if_available
USER_MODEL = get_user_model()
def get_course_key(request, course_id=None):
if not course_id:
return CourseKey.from_string(request.GET.get('course_id'))
return CourseKey.from_string(course_id)
def verify_course_exists(view_func):
"""
A decorator to wrap a view function that takes `course_key` as a parameter.
Raises:
An API error if the `course_key` is invalid, or if no `CourseOverview` exists for the given key.
"""
@wraps(view_func)
def wrapped_function(self, request, **kwargs):
"""
Wraps the given view_function.
"""
try:
course_key = get_course_key(request, kwargs.get('course_id'))
except InvalidKeyError:
raise self.api_error(
status_code=status.HTTP_404_NOT_FOUND,
developer_message='The provided course key cannot be parsed.',
error_code='invalid_course_key'
)
if not CourseOverview.get_from_id_if_exists(course_key):
raise self.api_error(
status_code=status.HTTP_404_NOT_FOUND,
developer_message=u"Requested grade for unknown course {course}".format(course=text_type(course_key)),
error_code='course_does_not_exist'
)
return view_func(self, request, **kwargs)
return wrapped_function
class CourseEnrollmentPagination(CursorPagination):
"""
Paginates over CourseEnrollment objects.

View File

@@ -14,14 +14,12 @@ from lms.djangoapps.grades.api.serializers import GradingPolicySerializer
from lms.djangoapps.grades.api.v1.utils import (
CourseEnrollmentPagination,
GradeViewMixin,
get_course_key,
verify_course_exists
)
from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory
from lms.djangoapps.grades.models import PersistentCourseGrade
from opaque_keys import InvalidKeyError
from openedx.core.lib.api.authentication import OAuth2AuthenticationAllowInactiveUser
from openedx.core.lib.api.view_utils import PaginatedAPIView
from openedx.core.lib.api.view_utils import PaginatedAPIView, get_course_key, verify_course_exists
from xmodule.modulestore.django import modulestore
log = logging.getLogger(__name__)

View File

@@ -2,8 +2,9 @@
API Serializers
"""
from rest_framework import serializers
from six import text_type
from lms.djangoapps.program_enrollments.models import ProgramEnrollment
from lms.djangoapps.program_enrollments.models import ProgramCourseEnrollment, ProgramEnrollment
# pylint: disable=abstract-method
@@ -68,3 +69,25 @@ class ProgramCourseEnrollmentRequestSerializer(serializers.Serializer, InvalidSt
student_key = serializers.CharField(allow_blank=False)
status = serializers.ChoiceField(allow_blank=False, choices=STATUS_CHOICES)
class ProgramCourseEnrollmentListSerializer(serializers.Serializer):
"""
Serializer for listing course enrollments in a program.
"""
student_key = serializers.SerializerMethodField()
status = serializers.CharField()
account_exists = serializers.SerializerMethodField()
curriculum_uuid = serializers.SerializerMethodField()
class Meta(object):
model = ProgramCourseEnrollment
def get_student_key(self, obj):
return obj.program_enrollment.external_user_key
def get_account_exists(self, obj):
return bool(obj.program_enrollment.user)
def get_curriculum_uuid(self, obj):
return text_type(obj.program_enrollment.curriculum_uuid)

View File

@@ -8,7 +8,7 @@ from factory.django import DjangoModelFactory
from opaque_keys.edx.keys import CourseKey
from lms.djangoapps.program_enrollments import models
from student.tests.factories import UserFactory, CourseEnrollmentFactory
from student.tests.factories import CourseEnrollmentFactory, UserFactory
class ProgramEnrollmentFactory(DjangoModelFactory):
@@ -23,14 +23,12 @@ class ProgramEnrollmentFactory(DjangoModelFactory):
status = 'enrolled'
class ProgramCourseEnrollmentFactory(factory.DjangoModelFactory):
"""
Factory for ProgramCourseEnrollment models
"""
class ProgramCourseEnrollmentFactory(DjangoModelFactory):
""" A factory for the ProgramCourseEnrollment model. """
class Meta(object):
model = models.ProgramCourseEnrollment
program_enrollment = factory.SubFactory(ProgramEnrollmentFactory)
course_enrollment = factory.SubFactory(CourseEnrollmentFactory)
course_key = CourseKey.from_string("course-v1:edX+DemoX+Demo_Course")
status = "active"
status = 'active'

View File

@@ -2,12 +2,13 @@
Unit tests for ProgramEnrollment views.
"""
from __future__ import unicode_literals
from uuid import uuid4
import mock
import ddt
from django.core.cache import cache
from django.urls import reverse
import mock
from opaque_keys.edx.keys import CourseKey
from rest_framework import status
from rest_framework.test import APITestCase
@@ -15,37 +16,61 @@ from six import text_type
from lms.djangoapps.courseware.tests.factories import GlobalStaffFactory
from lms.djangoapps.program_enrollments.api.v1.constants import CourseEnrollmentResponseStatuses as CourseStatuses
from lms.djangoapps.program_enrollments.models import ProgramEnrollment, ProgramCourseEnrollment
from student.tests.factories import UserFactory
from lms.djangoapps.program_enrollments.models import ProgramCourseEnrollment, ProgramEnrollment
from openedx.core.djangoapps.catalog.cache import PROGRAM_CACHE_KEY_TPL
from openedx.core.djangoapps.catalog.tests.factories import (
CourseFactory,
OrganizationFactory as CatalogOrganizationFactory,
ProgramFactory,
)
from openedx.core.djangolib.testing.utils import CacheIsolationMixin
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
from openedx.core.djangoapps.catalog.cache import PROGRAM_CACHE_KEY_TPL
from .factories import ProgramEnrollmentFactory, ProgramCourseEnrollmentFactory
from openedx.core.djangolib.testing.utils import CacheIsolationMixin
from student.tests.factories import UserFactory
from .factories import ProgramCourseEnrollmentFactory, ProgramEnrollmentFactory
class ProgramEnrollmentListTest(APITestCase):
class ListViewTestMixin(object):
"""
Tests for GET calls to the Program Enrollments API.
Mixin to define some shared test data objects for program/course enrollment
list view tests.
"""
view_name = None
@classmethod
def setUpClass(cls):
super(ProgramEnrollmentListTest, cls).setUpClass()
super(ListViewTestMixin, cls).setUpClass()
cls.program_uuid = '00000000-1111-2222-3333-444444444444'
cls.curriculum_uuid = 'aaaaaaaa-1111-2222-3333-444444444444'
cls.other_curriculum_uuid = 'bbbbbbbb-1111-2222-3333-444444444444'
cls.course_id = CourseKey.from_string('course-v1:edX+ToyX+Toy_Course')
_ = CourseOverviewFactory.create(id=cls.course_id)
cls.password = 'password'
cls.student = UserFactory.create(username='student', password=cls.password)
cls.global_staff = GlobalStaffFactory.create(username='global-staff', password=cls.password)
@classmethod
def tearDownClass(cls):
super(ProgramEnrollmentListTest, cls).tearDownClass()
super(ListViewTestMixin, cls).tearDownClass()
def create_enrollments(self):
def get_url(self, program_uuid=None, course_id=None):
""" Returns the primary URL requested by the test case. """
kwargs = {'program_uuid': program_uuid or self.program_uuid}
if course_id:
kwargs['course_id'] = course_id or self.course_id
return reverse(self.view_name, kwargs=kwargs)
class ProgramEnrollmentListTest(ListViewTestMixin, APITestCase):
"""
Tests for GET calls to the Program Enrollments API.
"""
view_name = 'programs_api:v1:program_enrollments'
def create_program_enrollments(self):
"""
Helper method for creating program enrollment records.
"""
@@ -65,17 +90,14 @@ class ProgramEnrollmentListTest(APITestCase):
program_uuid=self.program_uuid, curriculum_uuid=self.curriculum_uuid, external_user_key=user_key,
)
self.addCleanup(self.destroy_enrollments)
self.addCleanup(self.destroy_program_enrollments)
def destroy_enrollments(self):
def destroy_program_enrollments(self):
"""
Deletes program enrollments associated with this test case's program_uuid.
"""
ProgramEnrollment.objects.filter(program_uuid=self.program_uuid).delete()
def get_url(self, program_key=None):
return reverse('programs_api:v1:program_enrollments', kwargs={'program_key': program_key})
@mock.patch('lms.djangoapps.program_enrollments.api.v1.views.get_programs', autospec=True, return_value=None)
def test_404_if_no_program_with_key(self, mock_get_programs):
self.client.login(username=self.global_staff.username, password=self.password)
@@ -109,7 +131,7 @@ class ProgramEnrollmentListTest(APITestCase):
def test_200_many_results(self):
self.client.login(username=self.global_staff.username, password=self.password)
self.create_enrollments()
self.create_program_enrollments()
with mock.patch('lms.djangoapps.program_enrollments.api.v1.views.get_programs', autospec=True):
response = self.client.get(self.get_url(self.program_uuid))
@@ -141,7 +163,7 @@ class ProgramEnrollmentListTest(APITestCase):
def test_200_many_pages(self):
self.client.login(username=self.global_staff.username, password=self.password)
self.create_enrollments()
self.create_program_enrollments()
with mock.patch('lms.djangoapps.program_enrollments.api.v1.views.get_programs', autospec=True):
url = self.get_url(self.program_uuid) + '?page_size=2'
response = self.client.get(url)
@@ -404,3 +426,135 @@ class CourseEnrollmentPostTests(APITestCase, ProgramCacheTestCaseMixin):
response = self.client.post(self.default_url, post_data, format="json")
self.assertEqual(response.status_code, 422)
self.assertIn('invalid enrollment record', response.data)
class ProgramCourseEnrollmentListTest(ListViewTestMixin, APITestCase):
"""
Tests for GET calls to the Program Course Enrollments API.
"""
view_name = 'programs_api:v1:program_course_enrollments'
def create_course_enrollments(self):
""" Helper method for creating ProgramCourseEnrollments. """
program_enrollment_1 = ProgramEnrollmentFactory.create(
program_uuid=self.program_uuid, curriculum_uuid=self.curriculum_uuid, external_user_key='user-0',
)
program_enrollment_2 = ProgramEnrollmentFactory.create(
program_uuid=self.program_uuid, curriculum_uuid=self.other_curriculum_uuid, external_user_key='user-0',
)
ProgramCourseEnrollmentFactory.create(
program_enrollment=program_enrollment_1,
course_key=self.course_id,
status='active',
)
ProgramCourseEnrollmentFactory.create(
program_enrollment=program_enrollment_2,
course_key=self.course_id,
status='inactive',
)
self.addCleanup(self.destroy_course_enrollments)
def destroy_course_enrollments(self):
""" Helper method for tearing down ProgramCourseEnrollments. """
ProgramCourseEnrollment.objects.filter(
program_enrollment__program_uuid=self.program_uuid,
course_key=self.course_id
).delete()
@mock.patch('lms.djangoapps.program_enrollments.api.v1.views.get_programs', autospec=True, return_value=None)
def test_404_if_no_program_with_key(self, mock_get_programs):
self.client.login(username=self.global_staff.username, password=self.password)
response = self.client.get(self.get_url(self.program_uuid, self.course_id))
assert status.HTTP_404_NOT_FOUND == response.status_code
mock_get_programs.assert_called_once_with(uuid=self.program_uuid)
def test_404_if_course_does_not_exist(self):
other_course_key = CourseKey.from_string('course-v1:edX+ToyX+Other_Course')
self.client.login(username=self.global_staff.username, password=self.password)
response = self.client.get(self.get_url(self.program_uuid, other_course_key))
assert status.HTTP_404_NOT_FOUND == response.status_code
def test_403_if_not_staff(self):
self.client.login(username=self.student.username, password=self.password)
response = self.client.get(self.get_url(self.program_uuid, self.course_id))
assert status.HTTP_403_FORBIDDEN == response.status_code
def test_401_if_anonymous(self):
response = self.client.get(self.get_url(self.program_uuid, self.course_id))
assert status.HTTP_401_UNAUTHORIZED == response.status_code
def test_200_empty_results(self):
self.client.login(username=self.global_staff.username, password=self.password)
with mock.patch('lms.djangoapps.program_enrollments.api.v1.views.get_programs', autospec=True):
response = self.client.get(self.get_url(self.program_uuid, self.course_id))
assert status.HTTP_200_OK == response.status_code
expected = {
'next': None,
'previous': None,
'results': [],
}
assert expected == response.data
def test_200_many_results(self):
self.client.login(username=self.global_staff.username, password=self.password)
self.create_course_enrollments()
with mock.patch('lms.djangoapps.program_enrollments.api.v1.views.get_programs', autospec=True):
response = self.client.get(self.get_url(self.program_uuid, self.course_id))
assert status.HTTP_200_OK == response.status_code
expected = {
'next': None,
'previous': None,
'results': [
{
'student_key': 'user-0', 'status': 'active', 'account_exists': True,
'curriculum_uuid': text_type(self.curriculum_uuid),
},
{
'student_key': 'user-0', 'status': 'inactive', 'account_exists': True,
'curriculum_uuid': text_type(self.other_curriculum_uuid),
},
],
}
assert expected == response.data
def test_200_many_pages(self):
self.client.login(username=self.global_staff.username, password=self.password)
self.create_course_enrollments()
with mock.patch('lms.djangoapps.program_enrollments.api.v1.views.get_programs', autospec=True):
url = self.get_url(self.program_uuid, self.course_id) + '?page_size=1'
response = self.client.get(url)
assert status.HTTP_200_OK == response.status_code
expected_results = [
{
'student_key': 'user-0', 'status': 'active', 'account_exists': True,
'curriculum_uuid': text_type(self.curriculum_uuid),
},
]
assert expected_results == response.data['results']
# there's going to be a 'cursor' query param, but we have no way of knowing it's value
assert response.data['next'] is not None
assert self.get_url(self.program_uuid, self.course_id) in response.data['next']
assert '?cursor=' in response.data['next']
assert response.data['previous'] is None
next_response = self.client.get(response.data['next'])
assert status.HTTP_200_OK == next_response.status_code
next_expected_results = [
{
'student_key': 'user-0', 'status': 'inactive', 'account_exists': True,
'curriculum_uuid': text_type(self.other_curriculum_uuid),
},
]
assert next_expected_results == next_response.data['results']
assert next_response.data['next'] is None
# there's going to be a 'cursor' query param, but we have no way of knowing it's value
assert next_response.data['previous'] is not None
assert self.get_url(self.program_uuid, self.course_id) in next_response.data['previous']
assert '?cursor=' in next_response.data['previous']

View File

@@ -2,7 +2,7 @@
from django.conf.urls import url
from lms.djangoapps.program_enrollments.api.v1.constants import PROGRAM_UUID_PATTERN
from lms.djangoapps.program_enrollments.api.v1.views import ProgramEnrollmentsView, ProgramCourseEnrollmentsView
from lms.djangoapps.program_enrollments.api.v1.views import ProgramCourseEnrollmentsView, ProgramEnrollmentsView
from openedx.core.constants import COURSE_ID_PATTERN
@@ -10,12 +10,12 @@ app_name = 'lms.djangoapps.program_enrollments'
urlpatterns = [
url(
r'^programs/{program_key}/enrollments/$'.format(program_key=r'(?P<program_key>[0-9a-fA-F-]+)'),
r'^programs/{program_uuid}/enrollments/$'.format(program_uuid=PROGRAM_UUID_PATTERN),
ProgramEnrollmentsView.as_view(),
name='program_enrollments'
),
url(
r'^programs/{program_uuid}/course/{course_id}/enrollments/'.format(
r'^programs/{program_uuid}/courses/{course_id}/enrollments/'.format(
program_uuid=PROGRAM_UUID_PATTERN,
course_id=COURSE_ID_PATTERN
),

View File

@@ -3,35 +3,37 @@
ProgramEnrollment Views
"""
from __future__ import unicode_literals
from functools import wraps
from django.http import Http404
from edx_rest_framework_extensions import permissions
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
from rest_framework import status
from rest_framework.exceptions import ValidationError
from rest_framework.pagination import CursorPagination
from rest_framework.response import Response
from rest_framework.views import APIView
from edx_rest_framework_extensions import permissions
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
from edx_rest_framework_extensions.auth.session.authentication import SessionAuthenticationAllowInactiveUser
from lms.djangoapps.program_enrollments.api.v1.constants import CourseEnrollmentResponseStatuses, MAX_ENROLLMENT_RECORDS
from lms.djangoapps.program_enrollments.api.v1.serializers import (
ProgramEnrollmentListSerializer,
ProgramCourseEnrollmentListSerializer,
ProgramCourseEnrollmentRequestSerializer,
ProgramEnrollmentListSerializer,
)
from lms.djangoapps.program_enrollments.models import ProgramEnrollment, ProgramCourseEnrollment
from lms.djangoapps.program_enrollments.models import ProgramCourseEnrollment, ProgramEnrollment
from openedx.core.djangoapps.catalog.utils import get_programs
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.lib.api.authentication import OAuth2AuthenticationAllowInactiveUser
from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, PaginatedAPIView
from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, PaginatedAPIView, verify_course_exists
from util.query import use_read_replica_if_available
def verify_program_exists(view_func):
"""
Raises:
An API error if the `program_key` kwarg in the wrapped function
An API error if the `program_uuid` kwarg in the wrapped function
does not exist in the catalog programs cache.
"""
@wraps(view_func)
@@ -39,7 +41,7 @@ def verify_program_exists(view_func):
"""
Wraps the given view_function.
"""
program_uuid = kwargs['program_key']
program_uuid = kwargs['program_uuid']
program = get_programs(uuid=program_uuid)
if not program:
raise self.api_error(
@@ -76,7 +78,7 @@ class ProgramEnrollmentsView(DeveloperErrorViewMixin, PaginatedAPIView):
"""
A view for Create/Read/Update methods on Program Enrollment data.
Path: `/api/program_enrollments/v1/programs/{program_key}/enrollments/`
Path: `/api/program_enrollments/v1/programs/{program_uuid}/enrollments/`
The path can contain an optional `page_size?=N` query parameter. The default page size is 100.
Returns:
@@ -97,7 +99,7 @@ class ProgramEnrollmentsView(DeveloperErrorViewMixin, PaginatedAPIView):
Example:
{
"next": null,
"previous": "http://testserver.com/api/program_enrollments/v1/programs/{program_key}/enrollments/?curor=abcd",
"previous": "http://testserver.com/api/program_enrollments/v1/programs/{program_uuid}/enrollments/?curor=abcd",
"results": [
{
"student_key": "user-0", "status": "pending",
@@ -128,8 +130,11 @@ class ProgramEnrollmentsView(DeveloperErrorViewMixin, PaginatedAPIView):
pagination_class = ProgramEnrollmentPagination
@verify_program_exists
def get(self, request, program_key=None):
enrollments = ProgramEnrollment.objects.filter(program_uuid=program_key)
def get(self, request, program_uuid=None):
""" Defines the GET list endpoint for ProgramEnrollment objects. """
enrollments = use_read_replica_if_available(
ProgramEnrollment.objects.filter(program_uuid=program_uuid)
)
paginated_enrollments = self.paginate_queryset(enrollments)
serializer = ProgramEnrollmentListSerializer(paginated_enrollments, many=True)
return self.get_paginated_response(serializer.data)
@@ -188,27 +193,79 @@ class ProgramCourseRunSpecificViewMixin(ProgramSpecificViewMixin):
return CourseKey.from_string(self.kwargs['course_id'])
class ProgramCourseEnrollmentsView(ProgramCourseRunSpecificViewMixin, APIView):
# pylint: disable=line-too-long
class ProgramCourseEnrollmentsView(DeveloperErrorViewMixin, ProgramCourseRunSpecificViewMixin, PaginatedAPIView):
"""
A view for enrolling students in a course through a program,
modifying program course enrollments, and listing program course
enrollments
enrollments.
Path: /api/v1/programs/{program_uuid}/courses/{course_id}/enrollments
Path: ``/api/program_enrollments/v1/programs/{program_uuid}/courses/{course_id}/enrollments/``
Accepts: [POST]
Accepts: [GET, POST]
For GET requests, the path can contain an optional `page_size?=N` query parameter.
The default page size is 100.
------------------------------------------------------------------------------------
POST
------------------------------------------------------------------------------------
Returns:
* 200: Returns a map of students and their enrollment status.
* 207: Not all students enrolled. Returns resulting enrollment status.
* 401: User is not authenticated
* 403: User lacks read access organization of specified program.
* 404: Program does not exist, or course does not exist in program
* 422: Invalid request, unable to enroll students.
**Returns**
* 200: Returns a map of students and their enrollment status.
* 207: Not all students enrolled. Returns resulting enrollment status.
* 401: User is not authenticated
* 403: User lacks read access organization of specified program.
* 404: Program does not exist, or course does not exist in program
* 422: Invalid request, unable to enroll students.
------------------------------------------------------------------------------------
GET
------------------------------------------------------------------------------------
**Returns**
* 200: OK - Contains a paginated set of program course enrollment data.
* 401: The requesting user is not authenticated.
* 403: The requesting user lacks access for the given program/course.
* 404: The requested program or course does not exist.
**Response**
In the case of a 200 response code, the response will include a paginated
data set. The `results` section of the response consists of a list of
program course enrollment records, where each record contains the following keys:
* student_key: The identifier of the student enrolled in the program and course.
* status: The student's course enrollment status.
* account_exists: A boolean indicating if the student has created an edx-platform user account.
* curriculum_uuid: The curriculum UUID of the enrollment record for the (student, program).
**Example**
{
"next": null,
"previous": "http://testserver.com/api/program_enrollments/v1/programs/{program_uuid}/courses/{course_id}/enrollments/?curor=abcd",
"results": [
{
"student_key": "user-0", "status": "inactive",
"account_exists": False, "curriculum_uuid": "00000000-1111-2222-3333-444444444444"
},
{
"student_key": "user-1", "status": "inactive",
"account_exists": False, "curriculum_uuid": "00000001-1111-2222-3333-444444444444"
},
{
"student_key": "user-2", "status": "active",
"account_exists": True, "curriculum_uuid": "00000002-1111-2222-3333-444444444444"
},
{
"student_key": "user-3", "status": "active",
"account_exists": True, "curriculum_uuid": "00000003-1111-2222-3333-444444444444"
},
],
}
"""
authentication_classes = (
JwtAuthentication,
@@ -218,6 +275,22 @@ class ProgramCourseEnrollmentsView(ProgramCourseRunSpecificViewMixin, APIView):
permission_classes = (permissions.JWT_RESTRICTED_APPLICATION_OR_USER_ACCESS,)
pagination_class = ProgramEnrollmentPagination
@verify_course_exists
@verify_program_exists
def get(self, request, program_uuid=None, course_id=None):
""" Defines the GET list endpoint for ProgramCourseEnrollment objects. """
course_key = CourseKey.from_string(course_id)
enrollments = use_read_replica_if_available(
ProgramCourseEnrollment.objects.filter(
program_enrollment__program_uuid=program_uuid, course_key=course_key
).select_related(
'program_enrollment'
)
)
paginated_enrollments = self.paginate_queryset(enrollments)
serializer = ProgramCourseEnrollmentListSerializer(paginated_enrollments, many=True)
return self.get_paginated_response(serializer.data)
def post(self, request, program_uuid=None, course_id=None):
"""
Enroll a list of students in a course in a program

View File

@@ -3,11 +3,15 @@ Utilities related to API views
"""
from __future__ import absolute_import
from collections import Sequence
from functools import wraps
from django.core.exceptions import NON_FIELD_ERRORS, ObjectDoesNotExist, ValidationError
from django.http import Http404
from django.utils.translation import ugettext as _
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
from edx_rest_framework_extensions.auth.session.authentication import SessionAuthenticationAllowInactiveUser
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from rest_framework import status
from rest_framework.exceptions import APIException
from rest_framework.generics import GenericAPIView
@@ -18,6 +22,7 @@ from rest_framework.response import Response
from rest_framework.views import APIView
from six import text_type, iteritems
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.lib.api.authentication import OAuth2AuthenticationAllowInactiveUser
from openedx.core.lib.api.permissions import IsUserInUrl
@@ -338,3 +343,41 @@ class PaginatedAPIView(APIView):
"""
assert self.paginator is not None
return self.paginator.get_paginated_response(data)
def get_course_key(request, course_id=None):
if not course_id:
return CourseKey.from_string(request.GET.get('course_id'))
return CourseKey.from_string(course_id)
def verify_course_exists(view_func):
"""
A decorator to wrap a view function that takes `course_key` as a parameter.
Raises:
An API error if the `course_key` is invalid, or if no `CourseOverview` exists for the given key.
"""
@wraps(view_func)
def wrapped_function(self, request, **kwargs):
"""
Wraps the given view_function.
"""
try:
course_key = get_course_key(request, kwargs.get('course_id'))
except InvalidKeyError:
raise self.api_error(
status_code=status.HTTP_404_NOT_FOUND,
developer_message='The provided course key cannot be parsed.',
error_code='invalid_course_key'
)
if not CourseOverview.get_from_id_if_exists(course_key):
raise self.api_error(
status_code=status.HTTP_404_NOT_FOUND,
developer_message=u"Requested grade for unknown course {course}".format(course=text_type(course_key)),
error_code='course_does_not_exist'
)
return view_func(self, request, **kwargs)
return wrapped_function