""" ProgramEnrollment Views """ from ccx_keys.locator import CCXLocator from django.conf import settings from django.core.management import call_command from django.db import transaction from edx_api_doc_tools import path_parameter, query_parameter, schema 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 organizations.models import Organization from rest_framework import status from rest_framework.exceptions import PermissionDenied from rest_framework.generics import RetrieveAPIView from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView from common.djangoapps.student.roles import CourseInstructorRole, CourseStaffRole, UserBasedRole from common.djangoapps.util.query import read_replica_or_default from lms.djangoapps.program_enrollments.api import ( fetch_program_course_enrollments, fetch_program_enrollments, fetch_program_enrollments_by_student, get_provider_slug, get_saml_provider_for_organization, iter_program_course_grades, write_program_course_enrollments, write_program_enrollments ) from lms.djangoapps.program_enrollments.constants import ( ProgramCourseOperationStatuses, ProgramEnrollmentStatuses, ProgramOperationStatuses ) from lms.djangoapps.program_enrollments.exceptions import ProviderDoesNotExistException from openedx.core.apidocs import cursor_paginate_serializer from openedx.core.djangoapps.catalog.utils import ( get_programs, get_programs_by_type, get_programs_for_organization, normalize_program_type ) from openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, PaginatedAPIView from .constants import ENABLE_ENROLLMENT_RESET_FLAG, MAX_ENROLLMENT_RECORDS from .serializers import ( CourseRunOverviewListSerializer, CourseRunOverviewSerializer, ProgramCourseEnrollmentRequestSerializer, ProgramCourseEnrollmentSerializer, ProgramCourseGradeSerializer, ProgramEnrollmentCreateRequestSerializer, ProgramEnrollmentSerializer, ProgramEnrollmentUpdateRequestSerializer ) from .utils import ( ProgramCourseSpecificViewMixin, ProgramEnrollmentPagination, ProgramSpecificViewMixin, UserProgramCourseEnrollmentPagination, UserProgramSpecificViewMixin, get_enrollment_http_code, get_enrollment_overviews, get_enrollments_for_courses_in_program, verify_course_exists_and_in_program, verify_program_exists, verify_user_enrolled_in_program ) class EnrollmentWriteMixin: """ Common functionality for viewsets with enrollment-writing POST/PATCH/PUT methods. Provides a `handle_write_request` utility method, which depends on the definitions of `serializer_class_by_write_method`, `ok_write_statuses`, and `perform_enrollment_write`. """ create_update_by_write_method = { 'POST': (True, False), 'PATCH': (False, True), 'PUT': (True, True), } # Set in subclasses serializer_class_by_write_method = "set-me-to-a-dict-with-http-method-keys" ok_write_statuses = "set-me-to-a-set" def handle_write_request(self): """ Create/modify program enrollments. Returns: Response """ serializer_class = self.serializer_class_by_write_method[self.request.method] serializer = serializer_class(data=self.request.data, many=True) serializer.is_valid(raise_exception=True) num_requests = len(self.request.data) if num_requests > MAX_ENROLLMENT_RECORDS: return Response( '{} enrollments requested, but limit is {}.'.format( MAX_ENROLLMENT_RECORDS, num_requests ), status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, ) create, update = self.create_update_by_write_method[self.request.method] results = self.perform_enrollment_write( serializer.validated_data, create, update ) http_code = get_enrollment_http_code( results.values(), self.ok_write_statuses ) return Response(status=http_code, data=results, content_type='application/json') def perform_enrollment_write(self, enrollment_requests, create, update): """ Perform the write operation. Implemented in subclasses. Arguments: enrollment_requests: list[dict] create (bool) update (bool) Returns: dict[str: str] Map from external keys to enrollment write statuses. """ raise NotImplementedError() class ProgramEnrollmentsView( EnrollmentWriteMixin, DeveloperErrorViewMixin, ProgramSpecificViewMixin, PaginatedAPIView, ): """ A view for Create/Read/Update methods on Program Enrollment data. 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: * 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_uuid}/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" }, ], } Create ========== 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 to enroll with the following schema: { 'status': A choice of the following statuses: ['enrolled', 'pending', 'canceled', 'suspended', 'ended'], student_key: string representation of a learner in partner systems, 'curriculum_uuid': string representation of a curriculum } Example: [ { "status": "enrolled", "external_user_key": "123", "curriculum_uuid": "2d7de549-b09e-4e50-835d-4c5c5080c566" },{ "status": "canceled", "external_user_key": "456", "curriculum_uuid": "2d7de549-b09e-4e50-835d-4c5c5080c566" },{ "status": "pending", "external_user_key": "789", "curriculum_uuid": "2d7de549-b09e-4e50-835d-4c5c5080c566" },{ "status": "suspended", "external_user_key": "abc", "curriculum_uuid": "2d7de549-b09e-4e50-835d-4c5c5080c566" }, ] Returns: * Response Body: {: } 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' * 'canceled' * 'suspended' * 'ended' * 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', 'canceled', 'suspended', or 'ended' was entered * 200: OK - All students were successfully enrolled. * Example json response: { '123': 'enrolled', '456': 'pending', '789': 'canceled, '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': 'conflict', '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 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', 'canceled', 'suspended', 'ended', ], student_key: string representation of a learner in partner systems } Example: [ { "status": "enrolled", "external_user_key": "123", },{ "status": "canceled", "external_user_key": "456", },{ "status": "pending", "external_user_key": "789", },{ "status": "suspended", "external_user_key": "abc", }, ] Returns: * Response Body: {: } 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' * 'canceled' * 'suspended' * 'ended' * 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', 'canceled', 'suspended', 'ended' was entered * 'not-in-program' - the user is not in the program and cannot be updated * 200: OK - All students were successfully enrolled. * Example json response: { '123': 'enrolled', '456': 'pending', '789': 'canceled, '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, BearerAuthenticationAllowInactiveUser, SessionAuthenticationAllowInactiveUser, ) permission_classes = (permissions.JWT_RESTRICTED_APPLICATION_OR_USER_ACCESS,) pagination_class = ProgramEnrollmentPagination # Overridden from `EnrollmentWriteMixin` serializer_class_by_write_method = { 'POST': ProgramEnrollmentCreateRequestSerializer, 'PATCH': ProgramEnrollmentUpdateRequestSerializer, 'PUT': ProgramEnrollmentCreateRequestSerializer, } ok_write_statuses = ProgramOperationStatuses.__OK__ @verify_program_exists def get(self, request, program_uuid=None): # lint-amnesty, pylint: disable=unused-argument """ Defines the GET list endpoint for ProgramEnrollment objects. """ enrollments = fetch_program_enrollments( self.program_uuid ).using(read_replica_or_default()) paginated_enrollments = self.paginate_queryset(enrollments) serializer = ProgramEnrollmentSerializer(paginated_enrollments, many=True) return self.get_paginated_response(serializer.data) @verify_program_exists def post(self, request, program_uuid=None): # lint-amnesty, pylint: disable=unused-argument """ Create program enrollments for a list of learners """ return self.handle_write_request() @verify_program_exists def patch(self, request, program_uuid=None): # lint-amnesty, pylint: disable=unused-argument """ Update program enrollments for a list of learners """ return self.handle_write_request() @verify_program_exists def put(self, request, program_uuid=None): # lint-amnesty, pylint: disable=unused-argument """ Create/update program enrollments for a list of learners """ return self.handle_write_request() def perform_enrollment_write(self, enrollment_requests, create, update): """ Perform the program enrollment write operation. Overridden from `EnrollmentWriteMixin`. Arguments: enrollment_requests: list[dict] create (bool) update (bool) Returns: dict[str: str] Map from external keys to enrollment write statuses. """ return write_program_enrollments( self.program_uuid, enrollment_requests, create=create, update=update ) class ProgramCourseEnrollmentsView( EnrollmentWriteMixin, DeveloperErrorViewMixin, ProgramCourseSpecificViewMixin, PaginatedAPIView, ): """ A view for enrolling students in a course through a program, modifying program course enrollments, and listing program course enrollments. Path: ``/api/program_enrollments/v1/programs/{program_uuid}/courses/{course_id}/enrollments/`` Accepts: [GET, POST, PATCH, PUT] For GET requests, the path can contain an optional `page_size?=N` query parameter. The default page size is 100. ------------------------------------------------------------------------------------ POST, PATCH, PUT ------------------------------------------------------------------------------------ **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, BearerAuthenticationAllowInactiveUser, SessionAuthenticationAllowInactiveUser, ) permission_classes = (permissions.JWT_RESTRICTED_APPLICATION_OR_USER_ACCESS,) pagination_class = ProgramEnrollmentPagination # Overridden from `EnrollmentWriteMixin` serializer_class_by_write_method = { 'POST': ProgramCourseEnrollmentRequestSerializer, 'PATCH': ProgramCourseEnrollmentRequestSerializer, 'PUT': ProgramCourseEnrollmentRequestSerializer, } ok_write_statuses = ProgramCourseOperationStatuses.__OK__ @verify_course_exists_and_in_program def get(self, request, program_uuid=None, course_id=None): """ Get a list of students enrolled in a course within a program. """ enrollments = fetch_program_course_enrollments( program_uuid, course_id ).select_related( 'program_enrollment' ).using(read_replica_or_default()) paginated_enrollments = self.paginate_queryset(enrollments) serializer = ProgramCourseEnrollmentSerializer(paginated_enrollments, many=True) return self.get_paginated_response(serializer.data) @verify_course_exists_and_in_program def post(self, request, program_uuid=None, course_id=None): # lint-amnesty, pylint: disable=unused-argument """ Enroll a list of students in a course in a program """ return self.handle_write_request() @verify_course_exists_and_in_program def patch(self, request, program_uuid=None, course_id=None): # lint-amnesty, pylint: disable=unused-argument """ Modify the program course enrollments of a list of learners """ return self.handle_write_request() @verify_course_exists_and_in_program def put(self, request, program_uuid=None, course_id=None): # lint-amnesty, pylint: disable=unused-argument """ Create or Update the program course enrollments of a list of learners """ return self.handle_write_request() def perform_enrollment_write(self, enrollment_requests, create, update): """ Perform the program enrollment write operation. Overridden from `EnrollmentWriteMixin`. Arguments: enrollment_requests: list[dict] create (bool) update (bool) Returns: dict[str: str] Map from external keys to enrollment write statuses. """ return write_program_course_enrollments( self.program_uuid, self.course_key, enrollment_requests, create=create, update=update, ) class ProgramCourseGradesView( DeveloperErrorViewMixin, ProgramCourseSpecificViewMixin, PaginatedAPIView, ): """ A view for retrieving a paginated list of grades for all students enrolled in a given courserun through a given program. Path: ``/api/program_enrollments/v1/programs/{program_uuid}/courses/{course_id}/grades/`` Accepts: [GET] For GET requests, the path can contain an optional `page_size?=N` query parameter. The default page size is 100. ------------------------------------------------------------------------------------ GET ------------------------------------------------------------------------------------ **Returns** * 200: OK - Contains a paginated set of program courserun grades. * 204: No Content - No grades to return * 207: Mixed result - Contains mixed list of program courserun grades and grade-fetching errors * 422: All failed - Contains list of grade-fetching errors * 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/207/422 response code, the response will include a paginated data set. The `results` section of the response consists of a list of grade records, where each successfully loaded record contains: * student_key: The identifier of the student enrolled in the program and course. * letter_grade: A letter grade as defined in grading policy (e.g. 'A' 'B' 'C' for 6.002x) or None. * passed: Boolean representing whether the course has been passed according to the course's grading policy. * percent: A float representing the overall grade for the course. and failed-to-load records contain: * student_key * error: error message from grades Exception **Example** 207 Multi-Status { "next": null, "previous": "http://example.com/api/program_enrollments/v1/programs/ {program_uuid}/courses/{course_id}/grades/?cursor=abcd", "results": [; { "student_key": "01709bffeae2807b6a7317", "letter_grade": "Pass", "percent": 0.95, "passed": true }, { "student_key": "2cfe15e3380a52e7198237", "error": "Timeout while calculating grade" }, ... ], } """ authentication_classes = ( JwtAuthentication, BearerAuthenticationAllowInactiveUser, SessionAuthenticationAllowInactiveUser, ) permission_classes = (permissions.JWT_RESTRICTED_APPLICATION_OR_USER_ACCESS,) pagination_class = ProgramEnrollmentPagination @verify_course_exists_and_in_program def get(self, request, program_uuid=None, course_id=None): # lint-amnesty, pylint: disable=unused-argument """ Defines the GET list endpoint for ProgramCourseGrade objects. """ grade_results = list(iter_program_course_grades( self.program_uuid, self.course_key, self.paginate_queryset )) serializer = ProgramCourseGradeSerializer(grade_results, many=True) response_code = self._calc_response_code(grade_results) return self.get_paginated_response(serializer.data, status_code=response_code) @staticmethod def _calc_response_code(grade_results): """ Returns HTTP status code appropriate for list of results, which may be grades or errors. Arguments: enrollment_grade_results: list[BaseProgramCourseGrade] Returns: int * 200 for all success * 207 for mixed result * 422 for all failure * 204 for empty """ if not grade_results: return status.HTTP_204_NO_CONTENT if all(result.is_error for result in grade_results): return status.HTTP_422_UNPROCESSABLE_ENTITY if any(result.is_error for result in grade_results): return status.HTTP_207_MULTI_STATUS return status.HTTP_200_OK class UserProgramReadOnlyAccessView(DeveloperErrorViewMixin, PaginatedAPIView): """ A view for checking the currently logged-in user's program read only access There are three major categories of users this API is differentiating. See the table below. -------------------------------------------------------------------------------------------- | User Type | API Returns | -------------------------------------------------------------------------------------------- | edX staff | All programs | -------------------------------------------------------------------------------------------- | course staff | All programs containing the courses of which the user is course staff | -------------------------------------------------------------------------------------------- | learner | All programs the learner is enrolled in | -------------------------------------------------------------------------------------------- Path: `/api/program_enrollments/v1/programs/enrollments/` Returns: * 200: OK - Contains a list of all programs in which the user has read only acccess to. * 401: The requesting user is not authenticated. The list will be a list of objects with the following keys: * `uuid` - the identifier of the program in which the user has read only access to. * `slug` - the string from which a link to the corresponding program page can be constructed. Example: [ { 'uuid': '00000000-1111-2222-3333-444444444444', 'slug': 'deadbeef' }, { 'uuid': '00000000-1111-2222-3333-444444444445', 'slug': 'undead-cattle' } ] """ authentication_classes = ( JwtAuthentication, BearerAuthenticationAllowInactiveUser, SessionAuthenticationAllowInactiveUser, ) permission_classes = (IsAuthenticated,) DEFAULT_PROGRAM_TYPE = 'masters' def get(self, request): """ How to respond to a GET request to this endpoint """ request_user = request.user programs = [] requested_program_type = normalize_program_type(request.GET.get('type', self.DEFAULT_PROGRAM_TYPE)) if request_user.is_staff: programs = get_programs_by_type(request.site, requested_program_type) else: program_dict = {} # Check if the user is a course staff of any course which is a part of a program. for staff_program in self.get_programs_user_is_course_staff_for(request_user, requested_program_type): program_dict.setdefault(staff_program['uuid'], staff_program) # Now get the program enrollments for user purely as a learner add to the list for learner_program in self._get_enrolled_programs_from_model(request_user): program_dict.setdefault(learner_program['uuid'], learner_program) programs = list(program_dict.values()) programs_in_which_user_has_access = [ {'uuid': program['uuid'], 'slug': program['marketing_slug']} for program in programs ] return Response(programs_in_which_user_has_access, status.HTTP_200_OK) def _get_enrolled_programs_from_model(self, user): """ Return the Program Enrollments linked to the learner within the data model. """ program_enrollments = fetch_program_enrollments_by_student( user=user, program_enrollment_statuses=ProgramEnrollmentStatuses.__ACTIVE__, ) uuids = [enrollment.program_uuid for enrollment in program_enrollments] return get_programs(uuids=uuids) or [] def get_course_keys_user_is_staff_for(self, user): """ Return all the course keys the user is course instructor or course staff role for """ # Get all the courses of which the user is course staff for. If None, return false def filter_ccx(course_access): """ CCXs cannot be edited in Studio and should not be filtered """ return not isinstance(course_access.course_id, CCXLocator) instructor_courses = UserBasedRole(user, CourseInstructorRole.ROLE).courses_with_role() staff_courses = UserBasedRole(user, CourseStaffRole.ROLE).courses_with_role() all_courses = list(filter(filter_ccx, instructor_courses | staff_courses)) course_keys = {} for course_access in all_courses: if course_access.course_id is not None: course_keys[course_access.course_id] = course_access.course_id return list(course_keys.values()) def get_programs_user_is_course_staff_for(self, user, program_type_filter): """ Return a list of programs the user is course staff for. This function would take a list of course runs the user is staff of, and then try to get the Masters program associated with each course_runs. """ program_dict = {} for course_key in self.get_course_keys_user_is_staff_for(user): course_run_programs = get_programs(course=course_key) for course_run_program in course_run_programs: if course_run_program and course_run_program.get('type').lower() == program_type_filter: program_dict[course_run_program['uuid']] = course_run_program return program_dict.values() class UserProgramCourseEnrollmentView( DeveloperErrorViewMixin, UserProgramSpecificViewMixin, PaginatedAPIView, ): """ A view for getting data associated with a user's course enrollments as part of a program enrollment. For full documentation, see the `program_enrollments` section of http://$LMS_BASE_URL/api-docs/. """ authentication_classes = ( JwtAuthentication, BearerAuthenticationAllowInactiveUser, SessionAuthenticationAllowInactiveUser, ) permission_classes = (IsAuthenticated,) serializer_class = CourseRunOverviewSerializer pagination_class = UserProgramCourseEnrollmentPagination @schema( parameters=[ path_parameter('username', str, description=( 'The username of the user for which enrollment overviews will be fetched. ' 'For now, this must be the requesting user; otherwise, 403 will be returned. ' 'In the future, global staff users may be able to supply other usernames.' )), path_parameter('program_uuid', str, description=( 'UUID of a program. ' 'Enrollments will be returned for course runs in this program.' )), query_parameter('page_size', int, description=( 'Number of results to return per page. ' 'Defaults to 10. Maximum is 25.' )), ], responses={ 200: cursor_paginate_serializer(CourseRunOverviewSerializer), 401: 'The requester is not authenticated.', 403: ( 'The requester cannot access the specified program and/or ' 'the requester may not retrieve this data for the specified user.' ), 404: 'The requested program does not exist.' }, ) @verify_program_exists @verify_user_enrolled_in_program def get(self, request, username, program_uuid): # lint-amnesty, pylint: disable=unused-argument """ Get an overview of each of a user's course enrollments associated with a program. This endpoint exists to get an overview of each course-run enrollment that a user has for course-runs within a given program. Fields included are the title, upcoming due dates, etc. This API endpoint is intended for use with the [Program Learner Portal MFE](https://github.com/edx/frontend-app-learner-portal-programs). It is important to note that the set of enrollments that this endpoint returns is different than a user's set of *program-course-run enrollments*. Specifically, this endpoint may include course runs that are *within* the specified program but were not *enrolled in* via the specified program. **Example Response:** ```json { "next": null, "previous": null, "results": [ { "course_run_id": "edX+AnimalsX+Aardvarks", "display_name": "Astonishing Aardvarks", "course_run_url": "https://courses.edx.org/courses/course-v1:edX+AnimalsX+Aardvarks/course/", "start_date": "2017-02-05T05:00:00Z", "end_date": "2018-02-05T05:00:00Z", "course_run_status": "completed" "emails_enabled": true, "due_dates": [ { "name": "Introduction: What even is an aardvark?", "url": "https://courses.edx.org/courses/course-v1:edX+AnimalsX+Aardvarks/jump_to/ block-v1:edX+AnimalsX+Aardvarks+type@chapter+block@1414ffd5143b4b508f739b563ab468b7", "date": "2017-05-01T05:00:00Z" }, { "name": "Quiz: Aardvark or Anteater?", "url": "https://courses.edx.org/courses/course-v1:edX+AnimalsX+Aardvarks/jump_to/ block-v1:edX+AnimalsX+Aardvarks+type@sequential+block@edx_introduction", "date": "2017-03-05T00:00:00Z" } ], "micromasters_title": "Animals", "certificate_download_url": "https://courses.edx.org/certificates/123" }, { "course_run_id": "edX+AnimalsX+Baboons", "display_name": "Breathtaking Baboons", "course_run_url": "https://courses.edx.org/courses/course-v1:edX+AnimalsX+Baboons/course/", "start_date": "2018-02-05T05:00:00Z", "end_date": null, "course_run_status": "in_progress" "emails_enabled": false, "due_dates": [], "micromasters_title": "Animals", "certificate_download_url": "https://courses.edx.org/certificates/123", "resume_course_run_url": "https://courses.edx.org/courses/course-v1:edX+AnimalsX+Baboons/jump_to/ block-v1:edX+AnimalsX+Baboons+type@sequential+block@edx_introduction" } ] } ``` """ if request.user.username != username: # TODO: Should this be case-insensitive? raise PermissionDenied() enrollments = get_enrollments_for_courses_in_program( self.request.user, self.program ) paginated_enrollments = self.paginate_queryset(enrollments) paginated_enrollment_overviews = get_enrollment_overviews( user=self.request.user, program=self.program, enrollments=paginated_enrollments, request=self.request, ) serializer = CourseRunOverviewSerializer(paginated_enrollment_overviews, many=True) return self.get_paginated_response(serializer.data) class ProgramCourseEnrollmentOverviewView( DeveloperErrorViewMixin, UserProgramSpecificViewMixin, RetrieveAPIView, ): """ A view for getting data associated with a user's course enrollments as part of a program enrollment. Path: ``/api/program_enrollments/v1/programs/{program_uuid}/overview/`` DEPRECATED: This is deprecated in favor of the new UserProgramCourseEnrollmentView, which is paginated. It will be removed in a follow-up to MST-126 after the Programs Learner Portal has been updated to use UserProgramCourseEnrollmentView. """ authentication_classes = ( JwtAuthentication, BearerAuthenticationAllowInactiveUser, SessionAuthenticationAllowInactiveUser, ) permission_classes = (IsAuthenticated,) serializer_class = CourseRunOverviewListSerializer @verify_program_exists @verify_user_enrolled_in_program def get_object(self): """ Defines the GET endpoint for overviews of course enrollments for a user as part of a program. """ enrollments = get_enrollments_for_courses_in_program( self.request.user, self.program ) enrollment_overviews = get_enrollment_overviews( user=self.request.user, program=self.program, enrollments=enrollments, request=self.request, ) return {'course_runs': enrollment_overviews} class EnrollmentDataResetView(APIView): """ Resets enrollments and users for a given organization and set of programs. Note, this will remove ALL users from the input organization. Path: ``/api/program_enrollments/v1/integration-reset/`` Accepts: [POST] ------------------------------------------------------------------------------------ POST ------------------------------------------------------------------------------------ **Returns** * 200: OK - Enrollments and users sucessfully deleted * 400: Bad Requeset - Program does not match the requested organization * 401: Unauthorized - The requesting user is not authenticated. * 404: Not Found - A requested program does not exist. **Response** """ authentication_classes = ( JwtAuthentication, BearerAuthenticationAllowInactiveUser, SessionAuthenticationAllowInactiveUser, ) permission_classes = (permissions.JWT_RESTRICTED_APPLICATION_OR_USER_ACCESS,) @transaction.atomic def post(self, request): """ Reset enrollment and user data for organization """ if not settings.FEATURES.get(ENABLE_ENROLLMENT_RESET_FLAG): return Response('reset not enabled on this environment', status.HTTP_501_NOT_IMPLEMENTED) try: org_key = request.data['organization'] except KeyError: return Response("missing required body content 'organization'", status.HTTP_400_BAD_REQUEST) try: organization = Organization.objects.get(short_name=org_key) except Organization.DoesNotExist: return Response(f'organization {org_key} not found', status.HTTP_404_NOT_FOUND) try: provider = get_saml_provider_for_organization(organization) except ProviderDoesNotExistException: pass else: idp_slug = get_provider_slug(provider) call_command('remove_social_auth_users', idp_slug, force=True) programs = get_programs_for_organization(organization=organization.short_name) if programs: call_command('reset_enrollment_data', ','.join(programs), force=True) return Response('success')