Add a GET API endpoint for program enrollments.
This commit is contained in:
committed by
Alex Dusenbery
parent
c8f8e05616
commit
20f0bc03d4
@@ -20,7 +20,6 @@ from lms.djangoapps.grades.api.v1.utils import (
|
||||
USER_MODEL,
|
||||
CourseEnrollmentPagination,
|
||||
GradeViewMixin,
|
||||
PaginatedAPIView,
|
||||
get_course_key,
|
||||
verify_course_exists
|
||||
)
|
||||
@@ -43,7 +42,7 @@ 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, view_auth_classes
|
||||
from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, PaginatedAPIView, 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
|
||||
|
||||
@@ -82,39 +82,6 @@ class CourseEnrollmentPagination(CursorPagination):
|
||||
return self.page_size
|
||||
|
||||
|
||||
class PaginatedAPIView(APIView):
|
||||
"""
|
||||
An `APIView` class enhanced with the pagination methods of `GenericAPIView`.
|
||||
"""
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
@property
|
||||
def paginator(self):
|
||||
"""
|
||||
The paginator instance associated with the view, or `None`.
|
||||
"""
|
||||
if not hasattr(self, '_paginator'):
|
||||
if self.pagination_class is None:
|
||||
self._paginator = None
|
||||
else:
|
||||
self._paginator = self.pagination_class()
|
||||
return self._paginator
|
||||
|
||||
def paginate_queryset(self, queryset):
|
||||
"""
|
||||
Return a single page of results, or `None` if pagination is disabled.
|
||||
"""
|
||||
if self.paginator is None:
|
||||
return None
|
||||
return self.paginator.paginate_queryset(queryset, self.request, view=self)
|
||||
|
||||
def get_paginated_response(self, data):
|
||||
"""
|
||||
Return a paginated style `Response` object for the given output data.
|
||||
"""
|
||||
assert self.paginator is not None
|
||||
return self.paginator.get_paginated_response(data)
|
||||
|
||||
|
||||
class GradeViewMixin(DeveloperErrorViewMixin):
|
||||
"""
|
||||
Mixin class for Grades related views.
|
||||
|
||||
@@ -14,7 +14,6 @@ from lms.djangoapps.grades.api.serializers import GradingPolicySerializer
|
||||
from lms.djangoapps.grades.api.v1.utils import (
|
||||
CourseEnrollmentPagination,
|
||||
GradeViewMixin,
|
||||
PaginatedAPIView,
|
||||
get_course_key,
|
||||
verify_course_exists
|
||||
)
|
||||
@@ -22,6 +21,7 @@ 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 xmodule.modulestore.django import modulestore
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
0
lms/djangoapps/program_enrollments/api/__init__.py
Normal file
0
lms/djangoapps/program_enrollments/api/__init__.py
Normal file
12
lms/djangoapps/program_enrollments/api/urls.py
Normal file
12
lms/djangoapps/program_enrollments/api/urls.py
Normal file
@@ -0,0 +1,12 @@
|
||||
"""
|
||||
Grades API URLs.
|
||||
"""
|
||||
|
||||
from django.conf.urls import include, url
|
||||
|
||||
|
||||
app_name = 'lms.djangoapps.program_enrollments'
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^v1/', include('program_enrollments.api.v1.urls', namespace='v1'))
|
||||
]
|
||||
42
lms/djangoapps/program_enrollments/api/v1/serializers.py
Normal file
42
lms/djangoapps/program_enrollments/api/v1/serializers.py
Normal file
@@ -0,0 +1,42 @@
|
||||
"""
|
||||
API Serializers
|
||||
"""
|
||||
from rest_framework import serializers
|
||||
|
||||
from lms.djangoapps.program_enrollments.models import ProgramEnrollment
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class ProgramEnrollmentSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serializer for Program Enrollments
|
||||
"""
|
||||
|
||||
class Meta(object):
|
||||
model = ProgramEnrollment
|
||||
fields = ('user', 'external_user_key', 'program_uuid', 'curriculum_uuid', 'status')
|
||||
validators = []
|
||||
|
||||
def validate(self, attrs):
|
||||
enrollment = ProgramEnrollment(**attrs)
|
||||
enrollment.full_clean()
|
||||
return attrs
|
||||
|
||||
def create(self, validated_data):
|
||||
return ProgramEnrollment.objects.create(**validated_data)
|
||||
|
||||
|
||||
class ProgramEnrollmentListSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for listing enrollments in a program.
|
||||
"""
|
||||
student_key = serializers.CharField(source='external_user_key')
|
||||
status = serializers.CharField()
|
||||
account_exists = serializers.SerializerMethodField()
|
||||
curriculum_uuid = serializers.UUIDField()
|
||||
|
||||
class Meta(object):
|
||||
model = ProgramEnrollment
|
||||
|
||||
def get_account_exists(self, obj):
|
||||
return bool(obj.user)
|
||||
22
lms/djangoapps/program_enrollments/api/v1/tests/factories.py
Normal file
22
lms/djangoapps/program_enrollments/api/v1/tests/factories.py
Normal file
@@ -0,0 +1,22 @@
|
||||
"""
|
||||
Factories for Program Enrollment tests.
|
||||
"""
|
||||
from uuid import uuid4
|
||||
|
||||
import factory
|
||||
from factory.django import DjangoModelFactory
|
||||
|
||||
from lms.djangoapps.program_enrollments import models
|
||||
from student.tests.factories import UserFactory
|
||||
|
||||
|
||||
class ProgramEnrollmentFactory(DjangoModelFactory):
|
||||
""" A Factory for the ProgramEnrollment model. """
|
||||
class Meta(object):
|
||||
model = models.ProgramEnrollment
|
||||
|
||||
user = factory.SubFactory(UserFactory)
|
||||
external_user_key = None
|
||||
program_uuid = uuid4()
|
||||
curriculum_uuid = uuid4()
|
||||
status = 'enrolled'
|
||||
@@ -0,0 +1,40 @@
|
||||
"""
|
||||
Unit tests for ProgramEnrollment serializers.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from lms.djangoapps.program_enrollments.models import ProgramEnrollment
|
||||
from lms.djangoapps.program_enrollments.api.v1.serializers import ProgramEnrollmentSerializer
|
||||
from student.tests.factories import UserFactory
|
||||
|
||||
|
||||
class ProgramEnrollmentSerializerTests(TestCase):
|
||||
"""
|
||||
Tests for the ProgramEnrollment serializer.
|
||||
"""
|
||||
def setUp(self):
|
||||
"""
|
||||
Set up the test data used in the specific tests
|
||||
"""
|
||||
super(ProgramEnrollmentSerializerTests, self).setUp()
|
||||
self.user = UserFactory.create()
|
||||
self.enrollment = ProgramEnrollment.objects.create(
|
||||
user=self.user,
|
||||
external_user_key='abc',
|
||||
program_uuid=uuid4(),
|
||||
curriculum_uuid=uuid4(),
|
||||
status='enrolled'
|
||||
)
|
||||
self.serializer = ProgramEnrollmentSerializer(instance=self.enrollment)
|
||||
|
||||
def test_serializer_contains_expected_fields(self):
|
||||
data = self.serializer.data
|
||||
|
||||
self.assertEqual(
|
||||
set(data.keys()),
|
||||
{'user', 'external_user_key', 'program_uuid', 'curriculum_uuid', 'status'}
|
||||
)
|
||||
173
lms/djangoapps/program_enrollments/api/v1/tests/test_views.py
Normal file
173
lms/djangoapps/program_enrollments/api/v1/tests/test_views.py
Normal file
@@ -0,0 +1,173 @@
|
||||
"""
|
||||
Unit tests for ProgramEnrollment views.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import mock
|
||||
|
||||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
from six import text_type
|
||||
|
||||
from lms.djangoapps.courseware.tests.factories import GlobalStaffFactory
|
||||
from lms.djangoapps.program_enrollments.models import ProgramEnrollment
|
||||
from student.tests.factories import UserFactory
|
||||
|
||||
from .factories import ProgramEnrollmentFactory
|
||||
|
||||
|
||||
class ProgramEnrollmentListTest(APITestCase):
|
||||
"""
|
||||
Tests for GET calls to the Program Enrollments API.
|
||||
"""
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(ProgramEnrollmentListTest, cls).setUpClass()
|
||||
cls.program_uuid = '00000000-1111-2222-3333-444444444444'
|
||||
cls.curriculum_uuid = 'aaaaaaaa-1111-2222-3333-444444444444'
|
||||
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()
|
||||
|
||||
def create_enrollments(self):
|
||||
"""
|
||||
Helper method for creating program enrollment records.
|
||||
"""
|
||||
for i in xrange(2):
|
||||
user_key = 'user-{}'.format(i)
|
||||
ProgramEnrollmentFactory.create(
|
||||
program_uuid=self.program_uuid,
|
||||
curriculum_uuid=self.curriculum_uuid,
|
||||
user=None,
|
||||
status='pending',
|
||||
external_user_key=user_key,
|
||||
)
|
||||
|
||||
for i in xrange(2, 4):
|
||||
user_key = 'user-{}'.format(i)
|
||||
ProgramEnrollmentFactory.create(
|
||||
program_uuid=self.program_uuid, curriculum_uuid=self.curriculum_uuid, external_user_key=user_key,
|
||||
)
|
||||
|
||||
self.addCleanup(self.destroy_enrollments)
|
||||
|
||||
def destroy_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)
|
||||
response = self.client.get(self.get_url(self.program_uuid))
|
||||
assert status.HTTP_404_NOT_FOUND == response.status_code
|
||||
mock_get_programs.assert_called_once_with(uuid=self.program_uuid)
|
||||
|
||||
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))
|
||||
assert status.HTTP_403_FORBIDDEN == response.status_code
|
||||
|
||||
def test_401_if_anonymous(self):
|
||||
response = self.client.get(self.get_url(self.program_uuid))
|
||||
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))
|
||||
|
||||
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_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))
|
||||
|
||||
assert status.HTTP_200_OK == response.status_code
|
||||
expected = {
|
||||
'next': None,
|
||||
'previous': None,
|
||||
'results': [
|
||||
{
|
||||
'student_key': 'user-0', 'status': 'pending', 'account_exists': False,
|
||||
'curriculum_uuid': text_type(self.curriculum_uuid),
|
||||
},
|
||||
{
|
||||
'student_key': 'user-1', 'status': 'pending', 'account_exists': False,
|
||||
'curriculum_uuid': text_type(self.curriculum_uuid),
|
||||
},
|
||||
{
|
||||
'student_key': 'user-2', 'status': 'enrolled', 'account_exists': True,
|
||||
'curriculum_uuid': text_type(self.curriculum_uuid),
|
||||
},
|
||||
{
|
||||
'student_key': 'user-3', 'status': 'enrolled', 'account_exists': True,
|
||||
'curriculum_uuid': text_type(self.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_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)
|
||||
|
||||
assert status.HTTP_200_OK == response.status_code
|
||||
expected_results = [
|
||||
{
|
||||
'student_key': 'user-0', 'status': 'pending', 'account_exists': False,
|
||||
'curriculum_uuid': text_type(self.curriculum_uuid),
|
||||
},
|
||||
{
|
||||
'student_key': 'user-1', 'status': 'pending', 'account_exists': False,
|
||||
'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) 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-2', 'status': 'enrolled', 'account_exists': True,
|
||||
'curriculum_uuid': text_type(self.curriculum_uuid),
|
||||
},
|
||||
{
|
||||
'student_key': 'user-3', 'status': 'enrolled', 'account_exists': True,
|
||||
'curriculum_uuid': text_type(self.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) in next_response.data['previous']
|
||||
assert '?cursor=' in next_response.data['previous']
|
||||
15
lms/djangoapps/program_enrollments/api/v1/urls.py
Normal file
15
lms/djangoapps/program_enrollments/api/v1/urls.py
Normal file
@@ -0,0 +1,15 @@
|
||||
""" Program Enrollments API v1 URLs. """
|
||||
from django.conf.urls import url
|
||||
|
||||
from lms.djangoapps.program_enrollments.api.v1.views import ProgramEnrollmentsView
|
||||
|
||||
|
||||
app_name = 'lms.djangoapps.program_enrollments'
|
||||
|
||||
urlpatterns = [
|
||||
url(
|
||||
r'^programs/{program_key}/enrollments/$'.format(program_key=r'(?P<program_key>[0-9a-fA-F-]+)'),
|
||||
ProgramEnrollmentsView.as_view(),
|
||||
name='program_enrollments'
|
||||
),
|
||||
]
|
||||
126
lms/djangoapps/program_enrollments/api/v1/views.py
Normal file
126
lms/djangoapps/program_enrollments/api/v1/views.py
Normal file
@@ -0,0 +1,126 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
ProgramEnrollment Views
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from functools import wraps
|
||||
|
||||
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 rest_framework import status
|
||||
from rest_framework.pagination import CursorPagination
|
||||
|
||||
from lms.djangoapps.program_enrollments.api.v1.serializers import ProgramEnrollmentListSerializer
|
||||
from lms.djangoapps.program_enrollments.models import ProgramEnrollment
|
||||
from openedx.core.djangoapps.catalog.utils import get_programs
|
||||
from openedx.core.lib.api.authentication import OAuth2AuthenticationAllowInactiveUser
|
||||
from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, PaginatedAPIView
|
||||
|
||||
|
||||
def verify_program_exists(view_func):
|
||||
"""
|
||||
Raises:
|
||||
An API error if the `program_key` kwarg in the wrapped function
|
||||
does not exist in the catalog programs cache.
|
||||
"""
|
||||
@wraps(view_func)
|
||||
def wrapped_function(self, request, **kwargs):
|
||||
"""
|
||||
Wraps the given view_function.
|
||||
"""
|
||||
program_uuid = kwargs['program_key']
|
||||
program = get_programs(uuid=program_uuid)
|
||||
if not program:
|
||||
raise self.api_error(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
developer_message='no program exists with given key',
|
||||
error_code='program_does_not_exist'
|
||||
)
|
||||
return view_func(self, request, **kwargs)
|
||||
return wrapped_function
|
||||
|
||||
|
||||
class ProgramEnrollmentPagination(CursorPagination):
|
||||
"""
|
||||
Pagination class for Program Enrollments.
|
||||
"""
|
||||
ordering = 'id'
|
||||
page_size = 100
|
||||
page_size_query_param = 'page_size'
|
||||
|
||||
def get_page_size(self, request):
|
||||
"""
|
||||
Get the page size based on the defined page size parameter if defined.
|
||||
"""
|
||||
try:
|
||||
page_size_string = request.query_params[self.page_size_query_param]
|
||||
return int(page_size_string)
|
||||
except (KeyError, ValueError):
|
||||
pass
|
||||
|
||||
return self.page_size
|
||||
|
||||
|
||||
class ProgramEnrollmentsView(DeveloperErrorViewMixin, PaginatedAPIView):
|
||||
"""
|
||||
A view for Create/Read/Update methods on Program Enrollment data.
|
||||
|
||||
Path: `/api/program_enrollments/v1/programs/{program_key}/enrollments/`
|
||||
The path can contain an optional `page_size?=N` query parameter. The default page size is 100.
|
||||
|
||||
Returns:
|
||||
* 200: OK - Contains a paginated set of program enrollment data.
|
||||
* 401: The requesting user is not authenticated.
|
||||
* 403: The requesting user lacks access for the given program.
|
||||
* 404: The requested program 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 enrollment records, where each record contains the following keys:
|
||||
* student_key: The identifier of the student enrolled in the program.
|
||||
* status: The student's 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_key}/enrollments/?curor=abcd",
|
||||
"results": [
|
||||
{
|
||||
"student_key": "user-0", "status": "pending",
|
||||
"account_exists": False, "curriculum_uuid": "00000000-1111-2222-3333-444444444444"
|
||||
},
|
||||
{
|
||||
"student_key": "user-1", "status": "pending",
|
||||
"account_exists": False, "curriculum_uuid": "00000001-1111-2222-3333-444444444444"
|
||||
},
|
||||
{
|
||||
"student_key": "user-2", "status": "enrolled",
|
||||
"account_exists": True, "curriculum_uuid": "00000002-1111-2222-3333-444444444444"
|
||||
},
|
||||
{
|
||||
"student_key": "user-3", "status": "enrolled",
|
||||
"account_exists": True, "curriculum_uuid": "00000003-1111-2222-3333-444444444444"
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
"""
|
||||
authentication_classes = (
|
||||
JwtAuthentication,
|
||||
OAuth2AuthenticationAllowInactiveUser,
|
||||
SessionAuthenticationAllowInactiveUser,
|
||||
)
|
||||
permission_classes = (permissions.JWT_RESTRICTED_APPLICATION_OR_USER_ACCESS,)
|
||||
pagination_class = ProgramEnrollmentPagination
|
||||
|
||||
@verify_program_exists
|
||||
def get(self, request, program_key=None):
|
||||
enrollments = ProgramEnrollment.objects.filter(program_uuid=program_key)
|
||||
paginated_enrollments = self.paginate_queryset(enrollments)
|
||||
serializer = ProgramEnrollmentListSerializer(paginated_enrollments, many=True)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
@@ -6,6 +6,8 @@ from __future__ import unicode_literals
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
from openedx.core.djangoapps.plugins.constants import ProjectType, PluginURLs
|
||||
|
||||
|
||||
class ProgramEnrollmentsConfig(AppConfig):
|
||||
"""
|
||||
@@ -14,5 +16,11 @@ class ProgramEnrollmentsConfig(AppConfig):
|
||||
name = 'lms.djangoapps.program_enrollments'
|
||||
|
||||
plugin_app = {
|
||||
'url_config': {},
|
||||
PluginURLs.CONFIG: {
|
||||
ProjectType.LMS: {
|
||||
PluginURLs.NAMESPACE: 'programs_api',
|
||||
PluginURLs.REGEX: 'api/program_enrollments/',
|
||||
PluginURLs.RELATIVE_PATH: 'api.urls',
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ Django model specifications for the Program Enrollments API
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from model_utils.models import TimeStampedModel
|
||||
from opaque_keys.edx.django.models import CourseKeyField
|
||||
from simple_history.models import HistoricalRecords
|
||||
@@ -29,6 +31,7 @@ class ProgramEnrollment(TimeStampedModel): # pylint: disable=model-missing-unic
|
||||
|
||||
class Meta(object):
|
||||
app_label = "program_enrollments"
|
||||
unique_together = ('external_user_key', 'program_uuid', 'curriculum_uuid')
|
||||
|
||||
# A student enrolled in a given (program, curriculum) should always
|
||||
# have a non-null ``user`` or ``external_user_key`` field (or both).
|
||||
@@ -51,6 +54,10 @@ class ProgramEnrollment(TimeStampedModel): # pylint: disable=model-missing-unic
|
||||
status = models.CharField(max_length=9, choices=STATUSES)
|
||||
historical_records = HistoricalRecords()
|
||||
|
||||
def clean(self):
|
||||
if not (self.user or self.external_user_key):
|
||||
raise ValidationError(_('One of user or external_user_key must not be null.'))
|
||||
|
||||
@classmethod
|
||||
def retire_user(cls, user_id):
|
||||
"""
|
||||
|
||||
@@ -21,10 +21,12 @@ class ProgramEnrollmentModelTests(TestCase):
|
||||
"""
|
||||
super(ProgramEnrollmentModelTests, self).setUp()
|
||||
self.user = UserFactory.create()
|
||||
self.program_uuid = uuid4()
|
||||
self.other_program_uuid = uuid4()
|
||||
self.enrollment = ProgramEnrollment.objects.create(
|
||||
user=self.user,
|
||||
external_user_key='abc',
|
||||
program_uuid=uuid4(),
|
||||
program_uuid=self.program_uuid,
|
||||
curriculum_uuid=uuid4(),
|
||||
status='enrolled'
|
||||
)
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
ProgramEnrollment Views
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
@@ -15,6 +15,7 @@ from rest_framework.mixins import RetrieveModelMixin, UpdateModelMixin
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.request import clone_request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from six import text_type, iteritems
|
||||
|
||||
from openedx.core.lib.api.authentication import OAuth2AuthenticationAllowInactiveUser
|
||||
@@ -304,3 +305,36 @@ class LazySequence(Sequence):
|
||||
self.iterable,
|
||||
self.est_len,
|
||||
)
|
||||
|
||||
|
||||
class PaginatedAPIView(APIView):
|
||||
"""
|
||||
An `APIView` class enhanced with the pagination methods of `GenericAPIView`.
|
||||
"""
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
@property
|
||||
def paginator(self):
|
||||
"""
|
||||
The paginator instance associated with the view, or `None`.
|
||||
"""
|
||||
if not hasattr(self, '_paginator'):
|
||||
if self.pagination_class is None:
|
||||
self._paginator = None
|
||||
else:
|
||||
self._paginator = self.pagination_class()
|
||||
return self._paginator
|
||||
|
||||
def paginate_queryset(self, queryset):
|
||||
"""
|
||||
Return a single page of results, or `None` if pagination is disabled.
|
||||
"""
|
||||
if self.paginator is None:
|
||||
return None
|
||||
return self.paginator.paginate_queryset(queryset, self.request, view=self)
|
||||
|
||||
def get_paginated_response(self, data):
|
||||
"""
|
||||
Return a paginated style `Response` object for the given output data.
|
||||
"""
|
||||
assert self.paginator is not None
|
||||
return self.paginator.get_paginated_response(data)
|
||||
|
||||
Reference in New Issue
Block a user