Add PATCH method for program enrollments
This commit is contained in:
@@ -6,6 +6,9 @@
|
||||
PROGRAM_UUID_PATTERN = r'(?P<program_uuid>[A-Fa-f0-9-]+)'
|
||||
MAX_ENROLLMENT_RECORDS = 25
|
||||
|
||||
# The name of the key that identifies students for POST/PATCH requests
|
||||
REQUEST_STUDENT_KEY = 'student_key'
|
||||
|
||||
|
||||
class CourseEnrollmentResponseStatuses(object):
|
||||
"""
|
||||
|
||||
@@ -19,8 +19,15 @@ class ProgramEnrollmentSerializer(serializers.ModelSerializer):
|
||||
validators = []
|
||||
|
||||
def validate(self, attrs):
|
||||
enrollment = ProgramEnrollment(**attrs)
|
||||
enrollment.full_clean()
|
||||
""" This modifies self.instance in the case of updates """
|
||||
if not self.instance:
|
||||
enrollment = ProgramEnrollment(**attrs)
|
||||
enrollment.full_clean()
|
||||
else:
|
||||
for key, value in attrs.items():
|
||||
setattr(self.instance, key, value)
|
||||
self.instance.full_clean()
|
||||
|
||||
return attrs
|
||||
|
||||
def create(self, validated_data):
|
||||
|
||||
@@ -7,10 +7,10 @@ import json
|
||||
from uuid import uuid4
|
||||
|
||||
import ddt
|
||||
import mock
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.cache import cache
|
||||
from django.urls import reverse
|
||||
from django.contrib.auth.models import User
|
||||
import mock
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
@@ -18,18 +18,17 @@ from six import text_type
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from lms.djangoapps.courseware.tests.factories import GlobalStaffFactory
|
||||
from lms.djangoapps.program_enrollments.api.v1.constants import MAX_ENROLLMENT_RECORDS, REQUEST_STUDENT_KEY
|
||||
from lms.djangoapps.program_enrollments.api.v1.constants import CourseEnrollmentResponseStatuses as CourseStatuses
|
||||
from lms.djangoapps.program_enrollments.models import ProgramCourseEnrollment, ProgramEnrollment
|
||||
from lms.djangoapps.program_enrollments.tests.factories import ProgramCourseEnrollmentFactory, ProgramEnrollmentFactory
|
||||
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.djangoapps.catalog.tests.factories import CourseFactory
|
||||
from openedx.core.djangoapps.catalog.tests.factories import OrganizationFactory as CatalogOrganizationFactory
|
||||
from openedx.core.djangoapps.catalog.tests.factories import ProgramFactory
|
||||
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
|
||||
from openedx.core.djangolib.testing.utils import CacheIsolationMixin
|
||||
from student.tests.factories import UserFactory, CourseEnrollmentFactory
|
||||
from student.tests.factories import CourseEnrollmentFactory, UserFactory
|
||||
|
||||
|
||||
class ListViewTestMixin(object):
|
||||
@@ -703,10 +702,14 @@ class ProgramEnrollmentViewPostTests(APITestCase):
|
||||
global_staff = GlobalStaffFactory.create(username='global-staff', password='password')
|
||||
self.client.login(username=global_staff.username, password='password')
|
||||
|
||||
def tearDown(self):
|
||||
super(ProgramEnrollmentViewPostTests, self).tearDown()
|
||||
ProgramEnrollment.objects.all().delete()
|
||||
|
||||
def student_enrollment(self, enrollment_status, external_user_key=None):
|
||||
return {
|
||||
REQUEST_STUDENT_KEY: external_user_key or str(uuid4().hex[0:10]),
|
||||
'status': enrollment_status,
|
||||
'external_user_key': external_user_key or str(uuid4().hex[0:10]),
|
||||
'curriculum_uuid': str(uuid4())
|
||||
}
|
||||
|
||||
@@ -719,7 +722,7 @@ class ProgramEnrollmentViewPostTests(APITestCase):
|
||||
curriculum_uuids = [curriculum_uuid, curriculum_uuid, uuid4()]
|
||||
post_data = [
|
||||
{
|
||||
'external_user_key': e,
|
||||
REQUEST_STUDENT_KEY: e,
|
||||
'status': s,
|
||||
'curriculum_uuid': str(c)
|
||||
}
|
||||
@@ -735,10 +738,10 @@ class ProgramEnrollmentViewPostTests(APITestCase):
|
||||
):
|
||||
response = self.client.post(url, json.dumps(post_data), content_type='application/json')
|
||||
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
|
||||
for i in range(3):
|
||||
enrollment = ProgramEnrollment.objects.filter(external_user_key=external_user_keys[i])[0]
|
||||
enrollment = ProgramEnrollment.objects.get(external_user_key=external_user_keys[i])
|
||||
|
||||
self.assertEqual(enrollment.external_user_key, external_user_keys[i])
|
||||
self.assertEqual(enrollment.program_uuid, program_key)
|
||||
@@ -753,7 +756,7 @@ class ProgramEnrollmentViewPostTests(APITestCase):
|
||||
post_data = [
|
||||
{
|
||||
'status': 'enrolled',
|
||||
'external_user_key': 'abc1',
|
||||
REQUEST_STUDENT_KEY: 'abc1',
|
||||
'curriculum_uuid': str(curriculum_uuid)
|
||||
}
|
||||
]
|
||||
@@ -770,9 +773,9 @@ class ProgramEnrollmentViewPostTests(APITestCase):
|
||||
):
|
||||
response = self.client.post(url, json.dumps(post_data), content_type='application/json')
|
||||
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
|
||||
enrollment = ProgramEnrollment.objects.first()
|
||||
enrollment = ProgramEnrollment.objects.get(external_user_key='abc1')
|
||||
|
||||
self.assertEqual(enrollment.external_user_key, 'abc1')
|
||||
self.assertEqual(enrollment.program_uuid, program_key)
|
||||
@@ -783,7 +786,7 @@ class ProgramEnrollmentViewPostTests(APITestCase):
|
||||
def test_enrollment_payload_limit(self):
|
||||
|
||||
post_data = []
|
||||
for _ in range(26):
|
||||
for _ in range(MAX_ENROLLMENT_RECORDS + 1):
|
||||
post_data += self.student_enrollment('enrolled')
|
||||
|
||||
url = reverse('programs_api:v1:program_enrollments', args=[uuid4()])
|
||||
@@ -794,7 +797,7 @@ class ProgramEnrollmentViewPostTests(APITestCase):
|
||||
return_value=None
|
||||
):
|
||||
response = self.client.post(url, json.dumps(post_data), content_type='application/json')
|
||||
self.assertEqual(response.status_code, 413)
|
||||
self.assertEqual(response.status_code, status.HTTP_413_REQUEST_ENTITY_TOO_LARGE)
|
||||
|
||||
def test_duplicate_enrollment(self):
|
||||
post_data = [
|
||||
@@ -812,7 +815,7 @@ class ProgramEnrollmentViewPostTests(APITestCase):
|
||||
):
|
||||
response = self.client.post(url, json.dumps(post_data), content_type='application/json')
|
||||
|
||||
self.assertEqual(response.status_code, 207)
|
||||
self.assertEqual(response.status_code, status.HTTP_207_MULTI_STATUS)
|
||||
self.assertEqual(response.data, {
|
||||
'001': 'duplicated',
|
||||
'002': 'enrolled',
|
||||
@@ -833,7 +836,7 @@ class ProgramEnrollmentViewPostTests(APITestCase):
|
||||
content_type='application/json'
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 422)
|
||||
self.assertEqual(response.status_code, status.HTTP_422_UNPROCESSABLE_ENTITY)
|
||||
|
||||
def test_unauthenticated(self):
|
||||
self.client.logout()
|
||||
@@ -847,7 +850,7 @@ class ProgramEnrollmentViewPostTests(APITestCase):
|
||||
content_type='application/json'
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 401)
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
def test_program_unauthorized(self):
|
||||
student = UserFactory.create(username='student', password='password')
|
||||
@@ -862,7 +865,7 @@ class ProgramEnrollmentViewPostTests(APITestCase):
|
||||
json.dumps(post_data),
|
||||
content_type='application/json'
|
||||
)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
|
||||
def test_program_not_found(self):
|
||||
post_data = [
|
||||
@@ -874,7 +877,7 @@ class ProgramEnrollmentViewPostTests(APITestCase):
|
||||
json.dumps(post_data),
|
||||
content_type='application/json'
|
||||
)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
def test_partially_valid_enrollment(self):
|
||||
|
||||
@@ -892,8 +895,224 @@ class ProgramEnrollmentViewPostTests(APITestCase):
|
||||
):
|
||||
response = self.client.post(url, json.dumps(post_data), content_type='application/json')
|
||||
|
||||
self.assertEqual(response.status_code, 207)
|
||||
self.assertEqual(response.status_code, status.HTTP_207_MULTI_STATUS)
|
||||
self.assertEqual(response.data, {
|
||||
'001': 'invalid-status',
|
||||
'003': 'pending',
|
||||
})
|
||||
|
||||
|
||||
class ProgramEnrollmentViewPatchTests(APITestCase):
|
||||
"""
|
||||
Tests for the ProgramEnrollment view PATCH method.
|
||||
"""
|
||||
def setUp(self):
|
||||
super(ProgramEnrollmentViewPatchTests, self).setUp()
|
||||
|
||||
self.program_uuid = '00000000-1111-2222-3333-444444444444'
|
||||
self.curriculum_uuid = 'aaaaaaaa-1111-2222-3333-444444444444'
|
||||
self.other_curriculum_uuid = 'bbbbbbbb-1111-2222-3333-444444444444'
|
||||
|
||||
self.course_id = CourseKey.from_string('course-v1:edX+ToyX+Toy_Course')
|
||||
_ = CourseOverviewFactory.create(id=self.course_id)
|
||||
|
||||
self.password = 'password'
|
||||
self.student = UserFactory.create(username='student', password=self.password)
|
||||
self.global_staff = GlobalStaffFactory.create(username='global-staff', password=self.password)
|
||||
|
||||
self.client.login(username=self.global_staff.username, password=self.password)
|
||||
|
||||
def student_enrollment(self, enrollment_status, external_user_key=None):
|
||||
return {
|
||||
'status': enrollment_status,
|
||||
REQUEST_STUDENT_KEY: external_user_key or str(uuid4().hex[0:10]),
|
||||
}
|
||||
|
||||
def test_successfully_patched_program_enrollment(self):
|
||||
enrollments = {}
|
||||
for i in xrange(4):
|
||||
user_key = 'user-{}'.format(i)
|
||||
instance = ProgramEnrollment.objects.create(
|
||||
program_uuid=self.program_uuid,
|
||||
curriculum_uuid=self.curriculum_uuid,
|
||||
user=None,
|
||||
status='pending',
|
||||
external_user_key=user_key,
|
||||
)
|
||||
enrollments[user_key] = instance
|
||||
|
||||
post_data = [
|
||||
{REQUEST_STUDENT_KEY: 'user-1', 'status': 'withdrawn'},
|
||||
{REQUEST_STUDENT_KEY: 'user-2', 'status': 'suspended'},
|
||||
{REQUEST_STUDENT_KEY: 'user-3', 'status': 'enrolled'},
|
||||
]
|
||||
|
||||
url = reverse('programs_api:v1:program_enrollments', args=[self.program_uuid])
|
||||
with mock.patch('lms.djangoapps.program_enrollments.api.v1.views.get_programs', autospec=True):
|
||||
response = self.client.patch(url, json.dumps(post_data), content_type='application/json')
|
||||
|
||||
for enrollment in enrollments.values():
|
||||
enrollment.refresh_from_db()
|
||||
|
||||
expected_statuses = {
|
||||
'user-0': 'pending',
|
||||
'user-1': 'withdrawn',
|
||||
'user-2': 'suspended',
|
||||
'user-3': 'enrolled',
|
||||
}
|
||||
for user_key, enrollment in enrollments.items():
|
||||
assert expected_statuses[user_key] == enrollment.status
|
||||
|
||||
expected_response = {
|
||||
'user-1': 'withdrawn',
|
||||
'user-2': 'suspended',
|
||||
'user-3': 'enrolled',
|
||||
}
|
||||
assert status.HTTP_200_OK == response.status_code
|
||||
assert expected_response == response.data
|
||||
|
||||
def test_enrollment_payload_limit(self):
|
||||
patch_data = []
|
||||
for _ in range(MAX_ENROLLMENT_RECORDS + 1):
|
||||
patch_data += self.student_enrollment('enrolled')
|
||||
|
||||
url = reverse('programs_api:v1:program_enrollments', args=[uuid4()])
|
||||
with mock.patch('lms.djangoapps.program_enrollments.api.v1.views.get_programs', autospec=True):
|
||||
response = self.client.patch(url, json.dumps(patch_data), content_type='application/json')
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_413_REQUEST_ENTITY_TOO_LARGE)
|
||||
|
||||
def test_unauthenticated(self):
|
||||
self.client.logout()
|
||||
patch_data = [
|
||||
self.student_enrollment('enrolled')
|
||||
]
|
||||
url = reverse('programs_api:v1:program_enrollments', args=[uuid4()])
|
||||
response = self.client.patch(
|
||||
url,
|
||||
json.dumps(patch_data),
|
||||
content_type='application/json'
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
def test_program_unauthorized(self):
|
||||
self.client.login(username=self.student.username, password=self.password)
|
||||
|
||||
patch_data = [
|
||||
self.student_enrollment('enrolled')
|
||||
]
|
||||
url = reverse('programs_api:v1:program_enrollments', args=[uuid4()])
|
||||
response = self.client.patch(
|
||||
url,
|
||||
json.dumps(patch_data),
|
||||
content_type='application/json'
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
|
||||
def test_program_not_found(self):
|
||||
patch_data = [
|
||||
self.student_enrollment('enrolled')
|
||||
]
|
||||
url = reverse('programs_api:v1:program_enrollments', args=[uuid4()])
|
||||
response = self.client.patch(
|
||||
url,
|
||||
json.dumps(patch_data),
|
||||
content_type='application/json'
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
def test_unprocessable_enrollment(self):
|
||||
url = reverse('programs_api:v1:program_enrollments', args=[uuid4()])
|
||||
|
||||
with mock.patch('lms.djangoapps.program_enrollments.api.v1.views.get_programs', autospec=True):
|
||||
response = self.client.patch(
|
||||
url,
|
||||
json.dumps([{'status': 'enrolled'}]),
|
||||
content_type='application/json'
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_422_UNPROCESSABLE_ENTITY)
|
||||
|
||||
def test_duplicate_enrollment(self):
|
||||
enrollments = {}
|
||||
for i in xrange(4):
|
||||
user_key = 'user-{}'.format(i)
|
||||
instance = ProgramEnrollment.objects.create(
|
||||
program_uuid=self.program_uuid,
|
||||
curriculum_uuid=self.curriculum_uuid,
|
||||
user=None,
|
||||
status='pending',
|
||||
external_user_key=user_key,
|
||||
)
|
||||
enrollments[user_key] = instance
|
||||
|
||||
patch_data = [
|
||||
self.student_enrollment('enrolled', 'user-1'),
|
||||
self.student_enrollment('enrolled', 'user-2'),
|
||||
self.student_enrollment('enrolled', 'user-1'),
|
||||
]
|
||||
|
||||
url = reverse('programs_api:v1:program_enrollments', args=[self.program_uuid])
|
||||
with mock.patch('lms.djangoapps.program_enrollments.api.v1.views.get_programs', autospec=True):
|
||||
response = self.client.patch(url, json.dumps(patch_data), content_type='application/json')
|
||||
|
||||
for enrollment in enrollments.values():
|
||||
enrollment.refresh_from_db()
|
||||
|
||||
expected_statuses = {
|
||||
'user-0': 'pending',
|
||||
'user-1': 'pending',
|
||||
'user-2': 'enrolled',
|
||||
'user-3': 'pending',
|
||||
}
|
||||
for user_key, enrollment in enrollments.items():
|
||||
assert expected_statuses[user_key] == enrollment.status
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_207_MULTI_STATUS)
|
||||
self.assertEqual(response.data, {
|
||||
'user-1': 'duplicated',
|
||||
'user-2': 'enrolled',
|
||||
})
|
||||
|
||||
def test_partially_valid_enrollment(self):
|
||||
enrollments = {}
|
||||
for i in xrange(4):
|
||||
user_key = 'user-{}'.format(i)
|
||||
instance = ProgramEnrollment.objects.create(
|
||||
program_uuid=self.program_uuid,
|
||||
curriculum_uuid=self.curriculum_uuid,
|
||||
user=None,
|
||||
status='pending',
|
||||
external_user_key=user_key,
|
||||
)
|
||||
enrollments[user_key] = instance
|
||||
|
||||
patch_data = [
|
||||
self.student_enrollment('new', 'user-1'),
|
||||
self.student_enrollment('withdrawn', 'user-3'),
|
||||
self.student_enrollment('enrolled', 'user-who-is-not-in-program'),
|
||||
]
|
||||
|
||||
url = reverse('programs_api:v1:program_enrollments', args=[self.program_uuid])
|
||||
with mock.patch('lms.djangoapps.program_enrollments.api.v1.views.get_programs', autospec=True):
|
||||
response = self.client.patch(url, json.dumps(patch_data), content_type='application/json')
|
||||
|
||||
for enrollment in enrollments.values():
|
||||
enrollment.refresh_from_db()
|
||||
|
||||
expected_statuses = {
|
||||
'user-0': 'pending',
|
||||
'user-1': 'pending',
|
||||
'user-2': 'pending',
|
||||
'user-3': 'withdrawn',
|
||||
}
|
||||
for user_key, enrollment in enrollments.items():
|
||||
assert expected_statuses[user_key] == enrollment.status
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_207_MULTI_STATUS)
|
||||
self.assertEqual(response.data, {
|
||||
'user-1': 'invalid-status',
|
||||
'user-3': 'withdrawn',
|
||||
'user-who-is-not-in-program': 'not-in-program',
|
||||
})
|
||||
|
||||
@@ -17,7 +17,11 @@ from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.pagination import CursorPagination
|
||||
from rest_framework.response import Response
|
||||
|
||||
from lms.djangoapps.program_enrollments.api.v1.constants import CourseEnrollmentResponseStatuses, MAX_ENROLLMENT_RECORDS
|
||||
from lms.djangoapps.program_enrollments.api.v1.constants import (
|
||||
CourseEnrollmentResponseStatuses,
|
||||
MAX_ENROLLMENT_RECORDS,
|
||||
REQUEST_STUDENT_KEY,
|
||||
)
|
||||
from lms.djangoapps.program_enrollments.api.v1.serializers import (
|
||||
ProgramCourseEnrollmentListSerializer,
|
||||
ProgramCourseEnrollmentRequestSerializer,
|
||||
@@ -133,7 +137,7 @@ class ProgramEnrollmentsView(DeveloperErrorViewMixin, PaginatedAPIView):
|
||||
* The request body will be a list of one or more students to enroll with the following schema:
|
||||
{
|
||||
'status': A choice of the following statuses: ['enrolled', 'pending', 'withdrawn', 'suspended'],
|
||||
'external_user_key': string representation of a learner in partner systems,
|
||||
student_key: string representation of a learner in partner systems,
|
||||
'curriculum_uuid': string representation of a curriculum
|
||||
}
|
||||
Example:
|
||||
@@ -191,6 +195,70 @@ class ProgramEnrollmentsView(DeveloperErrorViewMixin, PaginatedAPIView):
|
||||
* 404: NOT FOUND - The requested program does not exist.
|
||||
* 413: PAYLOAD TOO LARGE - Over 25 students supplied
|
||||
* 422: Unprocesable Entity - None of the students were successfully listed.
|
||||
|
||||
Update
|
||||
==========
|
||||
Path: `/api/program_enrollments/v1/programs/{program_uuid}/enrollments/`
|
||||
Where the program_uuid will be the uuid for a program.
|
||||
|
||||
Request body:
|
||||
* The request body will be a list of one or more students with their updated enrollment status:
|
||||
{
|
||||
'status': A choice of the following statuses: ['enrolled', 'pending', 'withdrawn', 'suspended'],
|
||||
student_key: string representation of a learner in partner systems
|
||||
}
|
||||
Example:
|
||||
[
|
||||
{
|
||||
"status": "enrolled",
|
||||
"external_user_key": "123",
|
||||
},{
|
||||
"status": "withdrawn",
|
||||
"external_user_key": "456",
|
||||
},{
|
||||
"status": "pending",
|
||||
"external_user_key": "789",
|
||||
},{
|
||||
"status": "suspended",
|
||||
"external_user_key": "abc",
|
||||
},
|
||||
]
|
||||
|
||||
Returns:
|
||||
* Response Body: {<external_user_key>: <status>} with as many keys as there were in the request body
|
||||
* external_user_key - string representation of a learner in partner systems
|
||||
* status - the learner's registration status
|
||||
* success statuses:
|
||||
* 'enrolled'
|
||||
* 'pending'
|
||||
* 'withdrawn'
|
||||
* 'suspended'
|
||||
* failure statuses:
|
||||
* 'duplicated' - the request body listed the same learner twice
|
||||
* 'conflict' - there is an existing enrollment for that learner, curriculum and program combo
|
||||
* 'invalid-status' - a status other than 'enrolled', 'pending', 'withdrawn', 'suspended' was entered
|
||||
* 'not-in-program' - the user is not in the program and cannot be updated
|
||||
* 201: CREATED - All students were successfully enrolled.
|
||||
* Example json response:
|
||||
{
|
||||
'123': 'enrolled',
|
||||
'456': 'pending',
|
||||
'789': 'withdrawn,
|
||||
'abc': 'suspended'
|
||||
}
|
||||
* 207: MULTI-STATUS - Some students were successfully enrolled while others were not.
|
||||
Details are included in the JSON response data.
|
||||
* Example json response:
|
||||
{
|
||||
'123': 'duplicated',
|
||||
'456': 'not-in-program',
|
||||
'789': 'invalid-status,
|
||||
'abc': 'suspended'
|
||||
}
|
||||
* 403: FORBIDDEN - The requesting user lacks access to enroll students in the given program.
|
||||
* 404: NOT FOUND - The requested program does not exist.
|
||||
* 413: PAYLOAD TOO LARGE - Over 25 students supplied
|
||||
* 422: Unprocesable Entity - None of the students were successfully updated.
|
||||
"""
|
||||
authentication_classes = (
|
||||
JwtAuthentication,
|
||||
@@ -213,38 +281,20 @@ class ProgramEnrollmentsView(DeveloperErrorViewMixin, PaginatedAPIView):
|
||||
@verify_program_exists
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""
|
||||
This is the POST for ProgramEnrollments
|
||||
Create program enrollments for a list of learners
|
||||
"""
|
||||
if len(request.data) > 25:
|
||||
if len(request.data) > MAX_ENROLLMENT_RECORDS:
|
||||
return Response(
|
||||
status=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
program_uuid = kwargs['program_uuid']
|
||||
student_data = OrderedDict((
|
||||
row.get('external_user_key'),
|
||||
{
|
||||
'program_uuid': program_uuid,
|
||||
'curriculum_uuid': row.get('curriculum_uuid'),
|
||||
'status': row.get('status'),
|
||||
'external_user_key': row.get('external_user_key'),
|
||||
})
|
||||
for row in request.data
|
||||
)
|
||||
|
||||
key_counter = Counter([enrollment.get('external_user_key') for enrollment in request.data])
|
||||
student_data = self._request_data_by_student_key(request, program_uuid)
|
||||
|
||||
response_data = {}
|
||||
for student_key, count in key_counter.items():
|
||||
if count > 1:
|
||||
response_data[student_key] = CourseEnrollmentResponseStatuses.DUPLICATED
|
||||
student_data.pop(student_key)
|
||||
|
||||
existing_enrollments = ProgramEnrollment.bulk_read_by_student_key(program_uuid, student_data)
|
||||
for enrollment in existing_enrollments:
|
||||
response_data[enrollment.external_user_key] = CourseEnrollmentResponseStatuses.CONFLICT
|
||||
student_data.pop(enrollment.external_user_key)
|
||||
response_data.update(self._remove_duplicate_entries(request, student_data))
|
||||
response_data.update(self._remove_existing_entries(program_uuid, student_data))
|
||||
|
||||
enrollments_to_create = {}
|
||||
|
||||
@@ -268,27 +318,106 @@ class ProgramEnrollmentsView(DeveloperErrorViewMixin, PaginatedAPIView):
|
||||
status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
)
|
||||
|
||||
for enrollment_serializer in enrollments_to_create.values():
|
||||
# create the model
|
||||
# TODO: make this a bulk save - https://openedx.atlassian.net/browse/EDUCATOR-4305
|
||||
for (student_key, _), enrollment_serializer in enrollments_to_create.items():
|
||||
enrollment_serializer.save()
|
||||
# TODO: make this a bulk save
|
||||
|
||||
if not enrollments_to_create:
|
||||
return self._get_created_or_updated_response(request, enrollments_to_create, response_data)
|
||||
|
||||
@verify_program_exists
|
||||
def patch(self, request, **kwargs):
|
||||
"""
|
||||
Modify the program enrollments for a list of learners
|
||||
"""
|
||||
if len(request.data) > MAX_ENROLLMENT_RECORDS:
|
||||
return Response(
|
||||
status=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
data=response_data,
|
||||
status=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
if len(request.data) != len(enrollments_to_create):
|
||||
return Response(
|
||||
status=status.HTTP_207_MULTI_STATUS,
|
||||
data=response_data,
|
||||
content_type='application/json',
|
||||
)
|
||||
program_uuid = kwargs['program_uuid']
|
||||
student_data = self._request_data_by_student_key(request, program_uuid)
|
||||
|
||||
response_data = {}
|
||||
response_data.update(self._remove_duplicate_entries(request, student_data))
|
||||
|
||||
existing_enrollments = {
|
||||
enrollment.external_user_key: enrollment
|
||||
for enrollment in
|
||||
ProgramEnrollment.bulk_read_by_student_key(program_uuid, student_data)
|
||||
}
|
||||
|
||||
enrollments_to_create = {}
|
||||
|
||||
for external_user_key in student_data.keys():
|
||||
if external_user_key not in existing_enrollments:
|
||||
student_data.pop(external_user_key)
|
||||
response_data[external_user_key] = CourseEnrollmentResponseStatuses.NOT_IN_PROGRAM
|
||||
|
||||
for external_user_key, enrollment in existing_enrollments.items():
|
||||
student = {key: value for key, value in student_data[external_user_key].items() if key == 'status'}
|
||||
enrollment_serializer = ProgramEnrollmentSerializer(enrollment, data=student, partial=True)
|
||||
if enrollment_serializer.is_valid():
|
||||
enrollments_to_create[(external_user_key, enrollment.curriculum_uuid)] = enrollment_serializer
|
||||
enrollment_serializer.save()
|
||||
response_data[external_user_key] = student['status']
|
||||
else:
|
||||
serializer_is_invalid = enrollment_serializer.errors['status'][0].code == 'invalid_choice'
|
||||
if 'status' in enrollment_serializer.errors and serializer_is_invalid:
|
||||
response_data[external_user_key] = CourseEnrollmentResponseStatuses.INVALID_STATUS
|
||||
|
||||
return self._get_created_or_updated_response(request, enrollments_to_create, response_data, status.HTTP_200_OK)
|
||||
|
||||
def _remove_duplicate_entries(self, request, student_data):
|
||||
""" Helper method to remove duplicate entries (based on student key) from request data. """
|
||||
result = {}
|
||||
key_counter = Counter([enrollment.get(REQUEST_STUDENT_KEY) for enrollment in request.data])
|
||||
for student_key, count in key_counter.items():
|
||||
if count > 1:
|
||||
result[student_key] = CourseEnrollmentResponseStatuses.DUPLICATED
|
||||
student_data.pop(student_key)
|
||||
return result
|
||||
|
||||
def _request_data_by_student_key(self, request, program_uuid):
|
||||
"""
|
||||
Helper method that returns an OrderedDict of rows from request.data,
|
||||
keyed by the `external_user_key`.
|
||||
"""
|
||||
return OrderedDict((
|
||||
row.get(REQUEST_STUDENT_KEY),
|
||||
{
|
||||
'program_uuid': program_uuid,
|
||||
'curriculum_uuid': row.get('curriculum_uuid'),
|
||||
'status': row.get('status'),
|
||||
'external_user_key': row.get(REQUEST_STUDENT_KEY),
|
||||
})
|
||||
for row in request.data
|
||||
)
|
||||
|
||||
def _remove_existing_entries(self, program_uuid, student_data):
|
||||
""" Helper method to remove entries that have existing ProgramEnrollment records. """
|
||||
result = {}
|
||||
existing_enrollments = ProgramEnrollment.bulk_read_by_student_key(program_uuid, student_data)
|
||||
for enrollment in existing_enrollments:
|
||||
result[enrollment.external_user_key] = CourseEnrollmentResponseStatuses.CONFLICT
|
||||
student_data.pop(enrollment.external_user_key)
|
||||
return result
|
||||
|
||||
def _get_created_or_updated_response(
|
||||
self, request, created_or_updated_data, response_data, default_status=status.HTTP_201_CREATED
|
||||
):
|
||||
"""
|
||||
Helper method to determine an appropirate HTTP response status code.
|
||||
"""
|
||||
response_status = default_status
|
||||
|
||||
if not created_or_updated_data:
|
||||
response_status = status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
elif len(request.data) != len(created_or_updated_data):
|
||||
response_status = status.HTTP_207_MULTI_STATUS
|
||||
|
||||
return Response(
|
||||
status=status.HTTP_201_CREATED,
|
||||
status=response_status,
|
||||
data=response_data,
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user