feat: Add Program Enrollments API View
This commit is contained in:
committed by
Ansab Gillani
parent
5c4042e6f0
commit
92442637e8
@@ -648,7 +648,7 @@ class SupportViewLinkProgramEnrollmentsTests(SupportViewTestCase):
|
||||
'0001,learner-01,apple,orange\n0002,learner-02,purple', # extra fields
|
||||
'\t0001 , \t learner-01 \n 0002 , learner-02 ', # whitespace
|
||||
)
|
||||
@patch('lms.djangoapps.support.views.program_enrollments.link_program_enrollments')
|
||||
@patch('lms.djangoapps.support.views.utils.link_program_enrollments')
|
||||
def test_text(self, text, mocked_link):
|
||||
self.client.post(self.url, data={
|
||||
'program_uuid': self.program_uuid,
|
||||
@@ -1189,3 +1189,183 @@ class FeatureBasedEnrollmentSupportApiViewTests(SupportViewTestCase):
|
||||
)
|
||||
data = json.loads(response.content.decode('utf-8'))
|
||||
assert data == {}
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class LinkProgramEnrollmentSupportAPIViewTests(SupportViewTestCase):
|
||||
"""
|
||||
Tests for the link_program_enrollments support view.
|
||||
"""
|
||||
_url = reverse("support:link_program_enrollments_details")
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Make the user support staff.
|
||||
"""
|
||||
super().setUp()
|
||||
SupportStaffRole().add_users(self.user)
|
||||
self.program_uuid = str(uuid4())
|
||||
self.username_pair_text = '0001,user-0001\n0002,user-02'
|
||||
|
||||
def _setup_user_from_username(self, username):
|
||||
"""
|
||||
Setup a user from the passed in username.
|
||||
If username passed in is falsy, return None
|
||||
"""
|
||||
created_user = None
|
||||
if username:
|
||||
created_user = UserFactory(username=username, password=self.PASSWORD)
|
||||
return created_user
|
||||
|
||||
def _setup_enrollments(self, external_user_key, linked_user=None):
|
||||
"""
|
||||
Create enrollments for testing linking.
|
||||
The enrollments can be created with already linked edX user.
|
||||
"""
|
||||
program_enrollment = ProgramEnrollmentFactory.create(
|
||||
external_user_key=external_user_key,
|
||||
program_uuid=self.program_uuid,
|
||||
user=linked_user
|
||||
)
|
||||
course_enrollment = None
|
||||
if linked_user:
|
||||
course_enrollment = CourseEnrollmentFactory.create(
|
||||
course_id=self.course.id,
|
||||
user=linked_user,
|
||||
mode=CourseMode.MASTERS,
|
||||
is_active=True
|
||||
)
|
||||
program_course_enrollment = ProgramCourseEnrollmentFactory.create(
|
||||
program_enrollment=program_enrollment,
|
||||
course_key=self.course.id,
|
||||
course_enrollment=course_enrollment,
|
||||
status='active'
|
||||
)
|
||||
return program_enrollment, program_course_enrollment
|
||||
|
||||
def test_invalid_uuid(self):
|
||||
"""
|
||||
Tests if enrollment linkages are refused for an invalid uuid
|
||||
"""
|
||||
response = self.client.post(self._url, data={
|
||||
'program_uuid': 'notauuid',
|
||||
'username_pair_text': self.username_pair_text,
|
||||
})
|
||||
msg = "Supplied program UUID 'notauuid' is not a valid UUID."
|
||||
data = json.loads(response.content.decode('utf-8'))
|
||||
assert data['errors'] == [msg]
|
||||
|
||||
@ddt.data(
|
||||
('program_uuid', ''),
|
||||
('', 'username_pair_text'),
|
||||
('', '')
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_missing_parameter(self, program_uuid, username_pair_text):
|
||||
"""
|
||||
Tests if enrollment linkages are refused for missing parameters
|
||||
"""
|
||||
error = (
|
||||
"You must provide both a program uuid "
|
||||
"and a series of lines with the format "
|
||||
"'external_user_key,lms_username'."
|
||||
)
|
||||
response = self.client.post(self._url, data={
|
||||
'program_uuid': program_uuid,
|
||||
'username_pair_text': username_pair_text
|
||||
})
|
||||
response_data = json.loads(response.content.decode('utf-8'))
|
||||
assert response_data['errors'] == [error]
|
||||
|
||||
@ddt.data(
|
||||
'0001,learner-01\n0002,learner-02', # normal
|
||||
'0001,learner-01,apple,orange\n0002,learner-02,purple', # extra fields
|
||||
'\t0001 , \t learner-01 \n 0002 , learner-02 ', # whitespace
|
||||
)
|
||||
@patch('lms.djangoapps.support.views.utils.link_program_enrollments')
|
||||
def test_username_pair_text(self, username_pair_text, mocked_link):
|
||||
"""
|
||||
Tests if enrollment linkages are created for different types of
|
||||
username_pair_text format
|
||||
"""
|
||||
response = self.client.post(self._url, data={
|
||||
'program_uuid': self.program_uuid,
|
||||
'username_pair_text': username_pair_text,
|
||||
})
|
||||
response_data = json.loads(response.content.decode('utf-8'))
|
||||
mocked_link.assert_called_once()
|
||||
mocked_link.assert_called_with(
|
||||
UUID(self.program_uuid),
|
||||
{
|
||||
'0001': 'learner-01',
|
||||
'0002': 'learner-02',
|
||||
}
|
||||
)
|
||||
success = ["('0001', 'learner-01')", "('0002', 'learner-02')"]
|
||||
assert response_data['successes'] == success
|
||||
mocked_link.reset_mock()
|
||||
|
||||
def test_invalid_username_pair_text(self):
|
||||
"""
|
||||
Tests if enrollment linkages are refused for invalid types of
|
||||
username_pair_text format
|
||||
"""
|
||||
username_pair_text = 'garbage_text'
|
||||
response = self.client.post(self._url, data={
|
||||
'program_uuid': self.program_uuid,
|
||||
'username_pair_text': username_pair_text,
|
||||
})
|
||||
msg = "All linking lines must be in the format 'external_user_key,lms_username'"
|
||||
response_data = json.loads(response.content.decode('utf-8'))
|
||||
assert response_data['errors'] == [msg]
|
||||
|
||||
@ddt.data(
|
||||
('linked_user', None),
|
||||
('linked_user', 'original_user')
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_linking_program_enrollment_with_username(self, username, original_username):
|
||||
"""
|
||||
Tests if enrollment linkages are created for valid usernames
|
||||
"""
|
||||
external_user_key = '0001'
|
||||
linked_user = self._setup_user_from_username(username)
|
||||
original_user = self._setup_user_from_username(original_username)
|
||||
program_enrollment, program_course_enrollment = self._setup_enrollments(
|
||||
external_user_key,
|
||||
original_user
|
||||
)
|
||||
response = self.client.post(self._url, data={
|
||||
'program_uuid': self.program_uuid,
|
||||
'username_pair_text': external_user_key + ',' + username
|
||||
})
|
||||
response_data = json.loads(response.content.decode('utf-8'))
|
||||
expected_success = f"('{external_user_key}', '{username}')"
|
||||
assert response_data['successes'] == [expected_success]
|
||||
program_enrollment.refresh_from_db()
|
||||
assert program_enrollment.user == linked_user
|
||||
program_course_enrollment.refresh_from_db()
|
||||
assert program_course_enrollment.course_enrollment.user == linked_user
|
||||
|
||||
@ddt.data(
|
||||
('', None),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_linking_program_enrollment_without_username(self, username, original_username):
|
||||
"""
|
||||
Tests if enrollment linkages are refused for invalid usernames
|
||||
"""
|
||||
external_user_key = '0001'
|
||||
linked_user = self._setup_user_from_username(username)
|
||||
original_user = self._setup_user_from_username(original_username)
|
||||
program_enrollment, program_course_enrollment = self._setup_enrollments(
|
||||
external_user_key,
|
||||
original_user
|
||||
)
|
||||
response = self.client.post(self._url, data={
|
||||
'program_uuid': self.program_uuid,
|
||||
'username_pair_text': external_user_key + ',' + username
|
||||
})
|
||||
response_data = json.loads(response.content.decode('utf-8'))
|
||||
error = "All linking lines must be in the format 'external_user_key,lms_username'"
|
||||
assert response_data['errors'] == [error]
|
||||
|
||||
@@ -12,7 +12,11 @@ from .views.enrollments import EnrollmentSupportListView, EnrollmentSupportView
|
||||
from .views.feature_based_enrollments import FeatureBasedEnrollmentsSupportView, FeatureBasedEnrollmentSupportAPIView
|
||||
from .views.index import index
|
||||
from .views.manage_user import ManageUserDetailView, ManageUserSupportView
|
||||
from .views.program_enrollments import LinkProgramEnrollmentSupportView, ProgramEnrollmentsInspectorView
|
||||
from .views.program_enrollments import (
|
||||
LinkProgramEnrollmentSupportView,
|
||||
LinkProgramEnrollmentSupportAPIView,
|
||||
ProgramEnrollmentsInspectorView
|
||||
)
|
||||
from .views.sso_records import SsoView
|
||||
|
||||
COURSE_ENTITLEMENTS_VIEW = EntitlementSupportView.as_view()
|
||||
@@ -45,8 +49,16 @@ urlpatterns = [
|
||||
FeatureBasedEnrollmentSupportAPIView.as_view(),
|
||||
name="feature_based_enrollment_details"
|
||||
),
|
||||
re_path(r'link_program_enrollments/?$', LinkProgramEnrollmentSupportView.as_view(),
|
||||
name='link_program_enrollments'),
|
||||
re_path(
|
||||
r'link_program_enrollments/?$',
|
||||
LinkProgramEnrollmentSupportView.as_view(),
|
||||
name='link_program_enrollments'
|
||||
),
|
||||
re_path(
|
||||
r'link_program_enrollments_details/?$',
|
||||
LinkProgramEnrollmentSupportAPIView.as_view(),
|
||||
name='link_program_enrollments_details'
|
||||
),
|
||||
re_path(
|
||||
r'program_enrollments_inspector/?$',
|
||||
ProgramEnrollmentsInspectorView.as_view(),
|
||||
|
||||
@@ -2,14 +2,15 @@
|
||||
Support tool for changing course enrollments.
|
||||
"""
|
||||
|
||||
|
||||
import csv
|
||||
from uuid import UUID
|
||||
|
||||
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
|
||||
from django.db.models import Q
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.generic import View
|
||||
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.authentication import SessionAuthentication
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from social_django.models import UserSocialAuth
|
||||
|
||||
from common.djangoapps.edxmako.shortcuts import render_to_response
|
||||
@@ -17,7 +18,6 @@ from common.djangoapps.third_party_auth.models import SAMLProviderConfig
|
||||
from lms.djangoapps.program_enrollments.api import (
|
||||
fetch_program_enrollments_by_student,
|
||||
get_users_by_external_keys_and_org_key,
|
||||
link_program_enrollments
|
||||
)
|
||||
from lms.djangoapps.program_enrollments.exceptions import (
|
||||
BadOrganizationShortNameException,
|
||||
@@ -26,6 +26,7 @@ from lms.djangoapps.program_enrollments.exceptions import (
|
||||
from lms.djangoapps.support.decorators import require_support_permission
|
||||
from lms.djangoapps.support.serializers import ProgramEnrollmentSerializer, serialize_user_info
|
||||
from lms.djangoapps.verify_student.services import IDVerificationService
|
||||
from lms.djangoapps.support.views.utils import validate_and_link_program_enrollments
|
||||
|
||||
TEMPLATE_PATH = 'support/link_program_enrollments.html'
|
||||
DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%S'
|
||||
@@ -60,7 +61,7 @@ class LinkProgramEnrollmentSupportView(View):
|
||||
"""
|
||||
program_uuid = request.POST.get('program_uuid', '').strip()
|
||||
text = request.POST.get('text', '')
|
||||
successes, errors = self._validate_and_link(program_uuid, text)
|
||||
successes, errors = validate_and_link_program_enrollments(program_uuid, text)
|
||||
return render_to_response(
|
||||
TEMPLATE_PATH,
|
||||
{
|
||||
@@ -71,48 +72,48 @@ class LinkProgramEnrollmentSupportView(View):
|
||||
}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _validate_and_link(program_uuid_string, linkage_text):
|
||||
"""
|
||||
Validate arguments, and if valid, call `link_program_enrollments`.
|
||||
|
||||
Returns: (successes, errors)
|
||||
where successes and errors are both list[str]
|
||||
class LinkProgramEnrollmentSupportAPIView(APIView):
|
||||
"""
|
||||
Support-only API View for linking learner enrollments by support staff.
|
||||
"""
|
||||
authentication_classes = (
|
||||
JwtAuthentication, SessionAuthentication
|
||||
)
|
||||
permission_classes = (
|
||||
IsAuthenticated,
|
||||
)
|
||||
|
||||
@method_decorator(require_support_permission)
|
||||
def post(self, request):
|
||||
"""
|
||||
if not (program_uuid_string and linkage_text):
|
||||
error = (
|
||||
"You must provide both a program uuid "
|
||||
"and a series of lines with the format "
|
||||
"'external_user_key,lms_username'."
|
||||
)
|
||||
return [], [error]
|
||||
try:
|
||||
program_uuid = UUID(program_uuid_string)
|
||||
except ValueError:
|
||||
return [], [
|
||||
f"Supplied program UUID '{program_uuid_string}' is not a valid UUID."
|
||||
]
|
||||
reader = csv.DictReader(
|
||||
linkage_text.splitlines(), fieldnames=('external_key', 'username')
|
||||
)
|
||||
ext_key_to_username = {
|
||||
(item.get('external_key') or '').strip(): (item['username'] or '').strip()
|
||||
for item in reader
|
||||
Links learner enrollments by support staff
|
||||
* Example Request:
|
||||
- POST / support / link_program_enrollments_details/
|
||||
* Sample Payload
|
||||
{
|
||||
program_uuid: < program_uuid > ,
|
||||
username_pair_text: 'external_user_key,lms_username'
|
||||
}
|
||||
* Example Response:
|
||||
{
|
||||
program_uuid: < program_uuid>,
|
||||
username_pair_text: 'external_user_key,lms_username'
|
||||
successes: 'Success messages if Linkages are created',
|
||||
errors: 'Error messages if there is no linkages'
|
||||
}
|
||||
"""
|
||||
|
||||
program_uuid = request.POST.get('program_uuid', '').strip()
|
||||
username_pair_text = request.POST.get('username_pair_text', '')
|
||||
successes, errors = validate_and_link_program_enrollments(program_uuid, username_pair_text)
|
||||
data = {
|
||||
'successes': successes,
|
||||
'errors': errors,
|
||||
'program_uuid': program_uuid,
|
||||
'username_pair_text': username_pair_text,
|
||||
}
|
||||
if not (all(ext_key_to_username.keys()) and all(ext_key_to_username.values())):
|
||||
return [], [
|
||||
"All linking lines must be in the format 'external_user_key,lms_username'"
|
||||
]
|
||||
link_errors = link_program_enrollments(
|
||||
program_uuid, ext_key_to_username
|
||||
)
|
||||
successes = [
|
||||
str(item)
|
||||
for item in ext_key_to_username.items()
|
||||
if item not in link_errors
|
||||
]
|
||||
errors = list(link_errors.values())
|
||||
return successes, errors
|
||||
return Response(data)
|
||||
|
||||
|
||||
class ProgramEnrollmentsInspectorView(View):
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
"""
|
||||
Various utility methods used by support app views.
|
||||
"""
|
||||
import csv
|
||||
from uuid import UUID
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
@@ -8,6 +11,9 @@ from opaque_keys.edx.keys import CourseKey
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from openedx.features.content_type_gating.models import ContentTypeGatingConfig
|
||||
from openedx.features.course_duration_limits.models import CourseDurationLimitConfig
|
||||
from lms.djangoapps.program_enrollments.api import (
|
||||
link_program_enrollments
|
||||
)
|
||||
|
||||
|
||||
def get_course_duration_info(course_key):
|
||||
@@ -42,3 +48,46 @@ def get_course_duration_info(course_key):
|
||||
|
||||
except (ObjectDoesNotExist, InvalidKeyError):
|
||||
return {}
|
||||
|
||||
|
||||
def validate_and_link_program_enrollments(program_uuid_string, linkage_text):
|
||||
"""
|
||||
Validate arguments, and if valid, call `link_program_enrollments`.
|
||||
|
||||
Returns: (successes, errors)
|
||||
where successes and errors are both list[str]
|
||||
"""
|
||||
if not (program_uuid_string and linkage_text):
|
||||
error = (
|
||||
"You must provide both a program uuid "
|
||||
"and a series of lines with the format "
|
||||
"'external_user_key,lms_username'."
|
||||
)
|
||||
return [], [error]
|
||||
try:
|
||||
program_uuid = UUID(program_uuid_string)
|
||||
except ValueError:
|
||||
return [], [
|
||||
f"Supplied program UUID '{program_uuid_string}' is not a valid UUID."
|
||||
]
|
||||
reader = csv.DictReader(
|
||||
linkage_text.splitlines(), fieldnames=('external_key', 'username')
|
||||
)
|
||||
ext_key_to_username = {
|
||||
(item.get('external_key') or '').strip(): (item['username'] or '').strip()
|
||||
for item in reader
|
||||
}
|
||||
if not (all(ext_key_to_username.keys()) and all(ext_key_to_username.values())):
|
||||
return [], [
|
||||
"All linking lines must be in the format 'external_user_key,lms_username'"
|
||||
]
|
||||
link_errors = link_program_enrollments(
|
||||
program_uuid, ext_key_to_username
|
||||
)
|
||||
successes = [
|
||||
str(item)
|
||||
for item in ext_key_to_username.items()
|
||||
if item[0] not in link_errors.keys()
|
||||
]
|
||||
errors = [message for message in link_errors.values()] # lint-amnesty, pylint: disable=unnecessary-comprehension
|
||||
return successes, errors
|
||||
|
||||
Reference in New Issue
Block a user