feat: Add Program Enrollments API View

This commit is contained in:
ansabgillani
2021-11-17 10:28:59 +05:00
committed by Ansab Gillani
parent 5c4042e6f0
commit 92442637e8
4 changed files with 291 additions and 49 deletions

View File

@@ -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]

View File

@@ -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(),

View File

@@ -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):

View File

@@ -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