def _validate_program(self): """ Verify that selected mode is available for program and all courses in the program """ program = self.cleaned_data.get(self.Fields.PROGRAM) if not program: return course_runs = get_course_runs_from_program(program) try: client = CourseCatalogApiClient(self._user) available_modes = client.get_common_course_modes(course_runs) course_mode = self.cleaned_data.get(self.Fields.COURSE_MODE) except (HttpClientError, HttpServerError): raise ValidationError( ValidationMessages.FAILED_TO_OBTAIN_COURSE_MODES.format( program_title=program.get("title"))) if not course_mode: raise ValidationError( ValidationMessages.COURSE_WITHOUT_COURSE_MODE) if course_mode not in available_modes: raise ValidationError( ValidationMessages.COURSE_MODE_NOT_AVAILABLE.format( mode=course_mode, program_title=program.get("title"), modes=", ".join(available_modes)))
def clean_program(self): """ Clean program. Try obtaining program treating form value as program UUID or title. Returns: dict: Program information if program found """ program_id = self.cleaned_data[self.Fields.PROGRAM].strip() if not program_id: return None try: client = CourseCatalogApiClient(self._user) program = client.get_program_by_uuid(program_id) or client.get_program_by_title(program_id) except MultipleProgramMatchError as exc: raise ValidationError(ValidationMessages.MULTIPLE_PROGRAM_MATCH.format(program_count=exc.programs_matched)) except (HttpClientError, HttpServerError): raise ValidationError(ValidationMessages.INVALID_PROGRAM_ID.format(program_id=program_id)) if not program: raise ValidationError(ValidationMessages.INVALID_PROGRAM_ID.format(program_id=program_id)) if program['status'] != ProgramStatuses.ACTIVE: raise ValidationError( ValidationMessages.PROGRAM_IS_INACTIVE.format(program_id=program_id, status=program['status']) ) return program
def courses(self, request, pk=None): # pylint: disable=invalid-name """ Retrieve the list of courses contained within this catalog. Only courses with active course runs are returned. A course run is considered active if it is currently open for enrollment, or will open in the future. --- serializer: serializers.CourseSerializerExcludingClosedRuns """ page = request.GET.get('page', 1) catalog_api = CourseCatalogApiClient(request.user) courses = catalog_api.get_paginated_catalog_courses(pk, page) # if API returned an empty response, that means pagination has ended. # An empty response can also means that there was a problem fetching data from catalog API. if not courses: logger.error( "Unable to fetch API response for catalog courses from endpoint '/catalog/%s/courses?page=%s'.", pk, page, ) raise NotFound("The resource you are looking for does not exist.") serializer = serializers.EnterpriseCatalogCoursesReadOnlySerializer( courses) # Add enterprise related context for the courses. serializer.update_enterprise_courses(request, pk) return get_paginated_response(serializer.data, request)
def setUp(self): super(TestCourseCatalogApi, self).setUp() self.user_mock = mock.Mock(spec=User) self.get_data_mock = self._make_patch(self._make_catalog_api_location("get_edx_api_data")) self.catalog_api_config_mock = self._make_patch(self._make_catalog_api_location("CatalogIntegration")) self.jwt_builder_mock = self._make_patch(self._make_catalog_api_location("JwtBuilder")) self.api = CourseCatalogApiClient(self.user_mock)
def setUp(self): super(TestCourseCatalogApi, self).setUp() self.user_mock = mock.Mock(spec=User) self.get_data_mock = self._make_patch( self._make_catalog_api_location("get_edx_api_data")) self.catalog_api_config_mock = self._make_patch( self._make_catalog_api_location("CatalogIntegration")) self.api_factory_mock = self._make_patch( self._make_catalog_api_location("course_discovery_api_client")) self.api = CourseCatalogApiClient(self.user_mock)
def notify_enrolled_learners(cls, enterprise_customer, request, course_id, users): """ Notify learners about a course in which they've been enrolled. Args: enterprise_customer: The EnterpriseCustomer being linked to request: The HTTP request that's being processed course_id: The specific course the learners were enrolled in users: An iterable of the users or pending users who were enrolled """ course_details = CourseCatalogApiClient(request.user).get_course_run(course_id) if not course_details: logging.warning( _( "Course details were not found for course key {} - Course Catalog API returned nothing. " "Proceeding with enrollment, but notifications won't be sent" ).format(course_id) ) return course_url = course_details.get('marketing_url') if course_url is None: # If we didn't get a useful path to the course on a marketing site from the catalog API, # then we should build a path to the course on the LMS directly. course_url = get_reversed_url_by_site( request, enterprise_customer.site, 'about_course', args=(course_id,), ) course_name = course_details.get('title') try: course_start = parse_lms_api_datetime(course_details.get('start')) except (TypeError, ValueError): course_start = None logging.exception( "None or empty value passed as course start date.\nCourse Details:\n{course_details}".format( course_details=course_details, ) ) with mail.get_connection() as email_conn: for user in users: send_email_notification_message( user=user, enrolled_in={ 'name': course_name, 'url': course_url, 'type': 'course', 'start': course_start, }, enterprise_customer=enterprise_customer, email_connection=email_conn )
def list(self, request): """ DRF view to list all catalogs. Arguments: request (HttpRequest): Current request Returns: (Response): DRF response object containing course catalogs. """ # fetch all course catalogs. catalog_api = CourseCatalogApiClient(request.user) catalogs = catalog_api.get_all_catalogs() serializer = self.serializer_class(catalogs, many=True) return Response(serializer.data)
def get_catalog_options(self): """ Retrieve a list of catalog ID and name pairs. Once retrieved, these name pairs can be used directly as a value for the `choices` argument to a ChoiceField. """ catalog_api = CourseCatalogApiClient(self.user) catalogs = catalog_api.get_all_catalogs() # order catalogs by name. catalogs = sorted(catalogs, key=lambda catalog: catalog.get('name', '').lower()) return BLANK_CHOICE_DASH + [ (catalog['id'], catalog['name'],) for catalog in catalogs ]
def get_course_runs(user, enterprise_customer): """ List the course runs the given enterprise customer has in its catalog. Args: user: A Django user requesting the course list enterprise_customer: The given Enterprise Customer Returns: iterable: An iterable containing the details of each course run. """ catalog_id = enterprise_customer.catalog client = CourseCatalogApiClient(user) catalog_courses = client.get_catalog_courses(catalog_id) LOGGER.info('Retrieving course list for catalog %s', catalog_id) for course in catalog_courses: course_key = course.get('key') course_details = client.get_course_details(course_key) for run in course_details.get('course_runs', []): yield get_complete_course_run_details(course_details, run)
def retrieve(self, request, pk=None): # pylint: disable=invalid-name """ DRF view to get catalog details. Arguments: request (HttpRequest): Current request pk (int): Course catalog identifier Returns: (Response): DRF response object containing course catalogs. """ # fetch course catalog for the given catalog id. catalog_api = CourseCatalogApiClient(request.user) catalog = catalog_api.get_catalog(pk) if not catalog: logger.error( "Unable to fetch API response for given catalog from endpoint '/catalog/%s/'.", pk) raise NotFound("The resource you are looking for does not exist.") serializer = self.serializer_class(catalog) return Response(serializer.data)
def test_raise_error_missing_get_edx_api_data(self, *args): # pylint: disable=unused-argument message = 'To parse a Catalog API response, this package must be installed in an Open edX environment.' with raises(NotConnectedToOpenEdX) as excinfo: CourseCatalogApiClient(mock.Mock(spec=User)) assert message == str(excinfo.value)
def test_raise_error_missing_catalog_integration(self, *args): # pylint: disable=unused-argument message = 'To get a CatalogIntegration object, this package must be installed in an Open edX environment.' with raises(NotConnectedToOpenEdX) as excinfo: CourseCatalogApiClient(mock.Mock(spec=User)) assert message == str(excinfo.value)
def get_enterprise_course_enrollment_page(self, request, enterprise_customer, course_details, course_modes): """ Render enterprise specific course track selection page. """ platform_name = configuration_helpers.get_value( 'PLATFORM_NAME', settings.PLATFORM_NAME) course_start_date = '' if course_details['start']: course_start_date = parse( course_details['start']).strftime('%B %d, %Y') try: effort_hours = int(course_details['effort'].split(':')[0]) except (AttributeError, ValueError): course_effort = '' else: course_effort = self.context_data['effort_hours_text'].format( hours=effort_hours) course_run = CourseCatalogApiClient(request.user).get_course_run( course_details['course_id']) course_modes = self.set_final_prices(course_modes, request) premium_modes = [mode for mode in course_modes if mode['premium']] try: organization = organizations_helpers.get_organization( course_details['org']) organization_logo = organization['logo'].url organization_name = organization['name'] except (TypeError, ValidationError, ValueError): organization_logo = None organization_name = None context_data = { 'page_title': self.context_data['page_title'], 'LANGUAGE_CODE': get_language_from_request(request), 'platform_name': platform_name, 'course_id': course_details['course_id'], 'course_name': course_details['name'], 'course_organization': course_details['org'], 'course_short_description': course_details['short_description'] or '', 'course_pacing': self.pacing_options.get(course_details['pacing'], ''), 'course_start_date': course_start_date, 'course_image_uri': course_details['media']['course_image']['uri'], 'enterprise_customer': enterprise_customer, 'enterprise_welcome_text': self.context_data['enterprise_welcome_text'].format( enterprise_customer_name=enterprise_customer.name, platform_name=platform_name, strong_start='<strong>', strong_end='</strong>', ), 'confirmation_text': self.context_data['confirmation_text'], 'starts_at_text': self.context_data['starts_at_text'], 'view_course_details_text': self.context_data['view_course_details_text'], 'select_mode_text': self.context_data['select_mode_text'], 'price_text': self.context_data['price_text'], 'continue_link_text': self.context_data['continue_link_text'], 'course_modes': filter_audit_course_modes(enterprise_customer, course_modes), 'course_effort': course_effort, 'level_text': self.context_data['level_text'], 'effort_text': self.context_data['effort_text'], 'course_overview': course_details['overview'], 'organization_logo': organization_logo, 'organization_name': organization_name, 'course_level_type': course_run.get('level_type', ''), 'close_modal_button_text': self.context_data['close_modal_button_text'], 'premium_modes': premium_modes, } return render(request, 'enterprise/enterprise_course_enrollment_page.html', context=context_data)