Add a GET endpoint to list ProgramCourseEnrollment data.
This commit is contained in:
committed by
Alex Dusenbery
parent
44359938c4
commit
403fb52e93
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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__)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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
|
||||
),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user