From 360159c64274447d6c0e4f23c15634f6afb6e74f Mon Sep 17 00:00:00 2001 From: Deborah Kaplan Date: Wed, 10 Jul 2024 14:29:53 -0400 Subject: [PATCH] feat: linting before touching all these files (#35108) * feat: linting before touching all these files All these files are old enough, relative either to our current linting rules or our current linter automation, that modifying anything in them either makes the linter cranky or wants to reformat the entire file. Rather than mixing cleanup with code changes, this commit just lints this set of files to our current standards. --- lms/djangoapps/certificates/api.py | 242 ++++----- lms/djangoapps/certificates/tests/test_api.py | 474 +++++++--------- .../djangoapps/catalog/tests/test_utils.py | 510 ++++++++---------- openedx/core/djangoapps/catalog/utils.py | 201 ++++--- .../credentials/tests/test_utils.py | 77 +-- openedx/core/djangoapps/credentials/utils.py | 14 +- openedx/core/djangoapps/programs/utils.py | 461 ++++++++-------- 7 files changed, 888 insertions(+), 1091 deletions(-) diff --git a/lms/djangoapps/certificates/api.py b/lms/djangoapps/certificates/api.py index ec790a4315c1..b7fe8aaca6b6 100644 --- a/lms/djangoapps/certificates/api.py +++ b/lms/djangoapps/certificates/api.py @@ -7,10 +7,8 @@ certificates models or any other certificates modules. """ - import logging from datetime import datetime -from pytz import UTC from django.conf import settings from django.contrib.auth import get_user_model @@ -18,18 +16,18 @@ from django.db.models import Q from eventtracking import tracker from opaque_keys.edx.django.models import CourseKeyField +from opaque_keys.edx.keys import CourseKey from organizations.api import get_course_organization_id +from pytz import UTC from common.djangoapps.course_modes.models import CourseMode from common.djangoapps.student.api import is_user_enrolled_in_course from common.djangoapps.student.models import CourseEnrollment from lms.djangoapps.branding import api as branding_api -from lms.djangoapps.certificates.generation_handler import ( - generate_certificate_task as _generate_certificate_task, - is_on_certificate_allowlist as _is_on_certificate_allowlist -) from lms.djangoapps.certificates.config import AUTO_CERTIFICATE_GENERATION as _AUTO_CERTIFICATE_GENERATION from lms.djangoapps.certificates.data import CertificateStatuses +from lms.djangoapps.certificates.generation_handler import generate_certificate_task as _generate_certificate_task +from lms.djangoapps.certificates.generation_handler import is_on_certificate_allowlist as _is_on_certificate_allowlist from lms.djangoapps.certificates.models import ( CertificateAllowlist, CertificateDateOverride, @@ -41,16 +39,13 @@ ExampleCertificateSet, GeneratedCertificate, ) -from lms.djangoapps.certificates.utils import ( - get_certificate_url as _get_certificate_url, - has_html_certificates_enabled as _has_html_certificates_enabled, - should_certificate_be_visible as _should_certificate_be_visible, - certificate_status as _certificate_status, - certificate_status_for_student as _certificate_status_for_student, -) +from lms.djangoapps.certificates.utils import certificate_status as _certificate_status +from lms.djangoapps.certificates.utils import certificate_status_for_student as _certificate_status_for_student +from lms.djangoapps.certificates.utils import get_certificate_url as _get_certificate_url +from lms.djangoapps.certificates.utils import has_html_certificates_enabled as _has_html_certificates_enabled +from lms.djangoapps.certificates.utils import should_certificate_be_visible as _should_certificate_be_visible from lms.djangoapps.instructor import access from lms.djangoapps.utils import _get_key -from opaque_keys.edx.keys import CourseKey from openedx.core.djangoapps.content.course_overviews.api import get_course_overview_or_none from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from xmodule.data import CertificatesDisplayBehaviors # lint-amnesty, pylint: disable=wrong-import-order @@ -83,8 +78,8 @@ def _format_certificate_for_user(username, cert): "is_passing": CertificateStatuses.is_passing_status(cert.status), "is_pdf_certificate": bool(cert.download_url), "download_url": ( - cert.download_url or get_certificate_url(cert.user.id, cert.course_id, uuid=cert.verify_uuid, - user_certificate=cert) + cert.download_url + or get_certificate_url(cert.user.id, cert.course_id, uuid=cert.verify_uuid, user_certificate=cert) if cert.status == CertificateStatuses.downloadable else None ), @@ -139,10 +134,7 @@ def get_certificate_for_user(username, course_key, format_results=True): the GeneratedCertificate object itself. """ try: - cert = GeneratedCertificate.eligible_certificates.get( - user__username=username, - course_id=course_key - ) + cert = GeneratedCertificate.eligible_certificates.get(user__username=username, course_id=course_key) except GeneratedCertificate.DoesNotExist: return None @@ -178,13 +170,8 @@ def get_certificates_for_user_by_course_keys(user, course_keys): Course keys for courses for which the user does not have a certificate will be omitted. """ - certs = GeneratedCertificate.eligible_certificates.filter( - user=user, course_id__in=course_keys - ) - return { - cert.course_id: _format_certificate_for_user(user.username, cert) - for cert in certs - } + certs = GeneratedCertificate.eligible_certificates.filter(user=user, course_id__in=course_keys) + return {cert.course_id: _format_certificate_for_user(user.username, cert) for cert in certs} def get_recently_modified_certificates(course_keys=None, start_date=None, end_date=None, user_ids=None): @@ -195,30 +182,28 @@ def get_recently_modified_certificates(course_keys=None, start_date=None, end_da cert_filter_args = {} if course_keys: - cert_filter_args['course_id__in'] = course_keys + cert_filter_args["course_id__in"] = course_keys if start_date: - cert_filter_args['modified_date__gte'] = start_date + cert_filter_args["modified_date__gte"] = start_date if end_date: - cert_filter_args['modified_date__lte'] = end_date + cert_filter_args["modified_date__lte"] = end_date if user_ids: - cert_filter_args['user__id__in'] = user_ids + cert_filter_args["user__id__in"] = user_ids # Include certificates with a CertificateDateOverride modified within the # given time range. if start_date or end_date: certs_with_modified_overrides = get_certs_with_modified_overrides(course_keys, start_date, end_date, user_ids) - return GeneratedCertificate.objects.filter( - **cert_filter_args - ).union( - certs_with_modified_overrides - ).order_by( - 'modified_date' + return ( + GeneratedCertificate.objects.filter(**cert_filter_args) + .union(certs_with_modified_overrides) + .order_by("modified_date") ) - return GeneratedCertificate.objects.filter(**cert_filter_args).order_by('modified_date') + return GeneratedCertificate.objects.filter(**cert_filter_args).order_by("modified_date") def get_certs_with_modified_overrides(course_keys=None, start_date=None, end_date=None, user_ids=None): @@ -229,9 +214,9 @@ def get_certs_with_modified_overrides(course_keys=None, start_date=None, end_dat """ override_filter_args = {} if start_date: - override_filter_args['history_date__gte'] = start_date + override_filter_args["history_date__gte"] = start_date if end_date: - override_filter_args['history_date__lte'] = end_date + override_filter_args["history_date__lte"] = end_date # Get the HistoricalCertificateDateOverrides that have entries within the # given date range. We check the history table to catch deleted overrides @@ -239,16 +224,16 @@ def get_certs_with_modified_overrides(course_keys=None, start_date=None, end_dat overrides = CertificateDateOverride.history.filter(**override_filter_args) # Get the associated GeneratedCertificate ids. - override_cert_ids = overrides.values_list('generated_certificate', flat=True) + override_cert_ids = overrides.values_list("generated_certificate", flat=True) # Build the args for the GeneratedCertificate query. First, filter by all # certs identified in override_cert_ids; then by the other arguments passed, # if present. - cert_filter_args = {'pk__in': override_cert_ids} + cert_filter_args = {"pk__in": override_cert_ids} if course_keys: - cert_filter_args['course_id__in'] = course_keys + cert_filter_args["course_id__in"] = course_keys if user_ids: - cert_filter_args['user__id__in'] = user_ids + cert_filter_args["user__id__in"] = user_ids return GeneratedCertificate.objects.filter(**cert_filter_args) @@ -287,14 +272,17 @@ def certificate_downloadable_status(student, course_key): # If the certificate status is an error user should view that status is "generating". # On the back-end, need to monitor those errors and re-submit the task. + # pylint: disable=simplifiable-if-expression response_data = { - 'is_downloadable': False, - 'is_generating': True if current_status['status'] in [CertificateStatuses.generating, # pylint: disable=simplifiable-if-expression - CertificateStatuses.error] else False, - 'is_unverified': True if current_status['status'] == CertificateStatuses.unverified else False, # pylint: disable=simplifiable-if-expression - 'download_url': None, - 'uuid': None, + "is_downloadable": False, + "is_generating": ( + True if current_status["status"] in [CertificateStatuses.generating, CertificateStatuses.error] else False + ), + "is_unverified": (True if current_status["status"] == CertificateStatuses.unverified else False), + "download_url": None, + "uuid": None, } + # pylint: enable=simplifiable-if-expression course_overview = get_course_overview_or_none(course_key) @@ -306,28 +294,28 @@ def certificate_downloadable_status(student, course_key): display_behavior_is_valid = True if ( - not certificates_viewable_for_course(course_overview) and - CertificateStatuses.is_passing_status(current_status['status']) and - display_behavior_is_valid and - course_overview.certificate_available_date + not certificates_viewable_for_course(course_overview) + and CertificateStatuses.is_passing_status(current_status["status"]) + and display_behavior_is_valid + and course_overview.certificate_available_date ): - response_data['earned_but_not_available'] = True - response_data['certificate_available_date'] = course_overview.certificate_available_date + response_data["earned_but_not_available"] = True + response_data["certificate_available_date"] = course_overview.certificate_available_date may_view_certificate = _should_certificate_be_visible( course_overview.certificates_display_behavior, course_overview.certificates_show_before_end, course_overview.has_ended(), course_overview.certificate_available_date, - course_overview.self_paced + course_overview.self_paced, ) - if current_status['status'] == CertificateStatuses.downloadable and may_view_certificate: - response_data['is_downloadable'] = True - response_data['download_url'] = current_status['download_url'] or get_certificate_url( - student.id, course_key, current_status['uuid'] + if current_status["status"] == CertificateStatuses.downloadable and may_view_certificate: + response_data["is_downloadable"] = True + response_data["download_url"] = current_status["download_url"] or get_certificate_url( + student.id, course_key, current_status["uuid"] ) - response_data['is_pdf_certificate'] = bool(current_status['download_url']) - response_data['uuid'] = current_status['uuid'] + response_data["is_pdf_certificate"] = bool(current_status["download_url"]) + response_data["uuid"] = current_status["uuid"] return response_data @@ -356,11 +344,14 @@ def set_cert_generation_enabled(course_key, is_enabled): """ CertificateGenerationCourseSetting.set_self_generation_enabled_for_course(course_key, is_enabled) - cert_event_type = 'enabled' if is_enabled else 'disabled' - event_name = '.'.join(['edx', 'certificate', 'generation', cert_event_type]) - tracker.emit(event_name, { - 'course_id': str(course_key), - }) + cert_event_type = "enabled" if is_enabled else "disabled" + event_name = ".".join(["edx", "certificate", "generation", cert_event_type]) + tracker.emit( + event_name, + { + "course_id": str(course_key), + }, + ) if is_enabled: log.info("Enabled self-generated certificates for course '%s'.", str(course_key)) else: @@ -407,8 +398,8 @@ def has_self_generated_certificates_enabled(course_key): """ return ( - CertificateGenerationConfiguration.current().enabled and - CertificateGenerationCourseSetting.is_self_generation_enabled_for_course(course_key) + CertificateGenerationConfiguration.current().enabled + and CertificateGenerationCourseSetting.is_self_generation_enabled_for_course(course_key) ) @@ -458,10 +449,10 @@ def get_active_web_certificate(course, is_preview_mode=None): """ Retrieves the active web certificate configuration for the specified course """ - certificates = getattr(course, 'certificates', {}) - configurations = certificates.get('certificates', []) + certificates = getattr(course, "certificates", {}) + configurations = certificates.get("certificates", []) for config in configurations: - if config.get('is_active') or is_preview_mode: + if config.get("is_active") or is_preview_mode: return config return None @@ -478,32 +469,19 @@ def get_certificate_template(course_key, mode, language): active_templates = CertificateTemplate.objects.filter(is_active=True) if org_id and mode: # get template by org, mode, and key - org_mode_and_key_templates = active_templates.filter( - organization_id=org_id, - mode=mode, - course_key=course_key - ) + org_mode_and_key_templates = active_templates.filter(organization_id=org_id, mode=mode, course_key=course_key) template = _get_language_specific_template_or_default(language, org_mode_and_key_templates) # since no template matched that course_key, only consider templates with empty course_key empty_course_key_templates = active_templates.filter(course_key=CourseKeyField.Empty) if not template and org_id and mode: # get template by org and mode - org_and_mode_templates = empty_course_key_templates.filter( - organization_id=org_id, - mode=mode - ) + org_and_mode_templates = empty_course_key_templates.filter(organization_id=org_id, mode=mode) template = _get_language_specific_template_or_default(language, org_and_mode_templates) if not template and org_id: # get template by only org - org_templates = empty_course_key_templates.filter( - organization_id=org_id, - mode=None - ) + org_templates = empty_course_key_templates.filter(organization_id=org_id, mode=None) template = _get_language_specific_template_or_default(language, org_templates) if not template and mode: # get template by only mode - mode_templates = empty_course_key_templates.filter( - organization_id=None, - mode=mode - ) + mode_templates = empty_course_key_templates.filter(organization_id=None, mode=mode) template = _get_language_specific_template_or_default(language, mode_templates) return template if template else None @@ -515,10 +493,10 @@ def _get_language_specific_template_or_default(language, templates): """ two_letter_language = _get_two_letter_language_code(language) - language_or_default_templates = list(templates.filter(Q(language=two_letter_language) - | Q(language=None) | Q(language=''))) - language_specific_template = _get_language_specific_template(two_letter_language, - language_or_default_templates) + language_or_default_templates = list( + templates.filter(Q(language=two_letter_language) | Q(language=None) | Q(language="")) + ) + language_specific_template = _get_language_specific_template(two_letter_language, language_or_default_templates) if language_specific_template: return language_specific_template else: @@ -537,7 +515,7 @@ def _get_all_languages_or_default_template(templates): Returns the first template that isn't language specific """ for template in templates: - if template.language == '': + if template.language == "": return template return templates[0] if templates else None @@ -550,8 +528,8 @@ def _get_two_letter_language_code(language_code): """ if language_code is None: return None - elif language_code == '': - return '' + elif language_code == "": + return "" else: return language_code[:2] @@ -560,7 +538,7 @@ def get_asset_url_by_slug(asset_slug): """ Returns certificate template asset url for given asset_slug. """ - asset_url = '' + asset_url = "" try: template_asset = CertificateTemplateAsset.objects.get(asset_slug=asset_slug) asset_url = template_asset.asset.url @@ -592,17 +570,17 @@ def get_certificate_footer_context(): # get Terms of Service and Honor Code page url terms_of_service_and_honor_code = branding_api.get_tos_and_honor_code_url() if terms_of_service_and_honor_code != branding_api.EMPTY_URL: - data.update({'company_tos_url': terms_of_service_and_honor_code}) + data.update({"company_tos_url": terms_of_service_and_honor_code}) # get Privacy Policy page url privacy_policy = branding_api.get_privacy_url() if privacy_policy != branding_api.EMPTY_URL: - data.update({'company_privacy_url': privacy_policy}) + data.update({"company_privacy_url": privacy_policy}) # get About page url about = branding_api.get_about_url() if about != branding_api.EMPTY_URL: - data.update({'company_about_url': about}) + data.update({"company_about_url": about}) return data @@ -631,7 +609,7 @@ def certificates_viewable_for_course(course): course.certificates_show_before_end, course.has_ended(), course.certificate_available_date, - course.self_paced + course.self_paced, ) @@ -650,9 +628,9 @@ def create_or_update_certificate_allowlist_entry(user, course_key, notes, enable user=user, course_id=course_key, defaults={ - 'allowlist': enabled, - 'notes': notes, - } + "allowlist": enabled, + "notes": notes, + }, ) log.info(f"Updated the allowlist of course {course_key} with student {user.id} and enabled={enabled}") @@ -675,7 +653,7 @@ def remove_allowlist_entry(user, course_key): certificate = get_certificate_for_user(user.username, course_key, False) if certificate: log.info(f"Invalidating certificate for student {user.id} in course {course_key} before allowlist removal.") - certificate.invalidate(source='allowlist_removal') + certificate.invalidate(source="allowlist_removal") log.info(f"Removing student {user.id} from the allowlist in course {course_key}.") allowlist_entry.delete() @@ -735,10 +713,10 @@ def create_certificate_invalidation_entry(certificate, user_requesting_invalidat certificate_invalidation, __ = CertificateInvalidation.objects.update_or_create( generated_certificate=certificate, defaults={ - 'active': True, - 'invalidated_by': user_requesting_invalidation, - 'notes': notes, - } + "active": True, + "invalidated_by": user_requesting_invalidation, + "notes": notes, + }, ) return certificate_invalidation @@ -772,10 +750,7 @@ def get_enrolled_allowlisted_users(course_key): - are allowlisted in this course run """ users = CourseEnrollment.objects.users_enrolled_in(course_key) - return users.filter( - certificateallowlist__course_id=course_key, - certificateallowlist__allowlist=True - ) + return users.filter(certificateallowlist__course_id=course_key, certificateallowlist__allowlist=True) def get_enrolled_allowlisted_not_passing_users(course_key): @@ -787,8 +762,7 @@ def get_enrolled_allowlisted_not_passing_users(course_key): """ users = get_enrolled_allowlisted_users(course_key) return users.exclude( - generatedcertificate__course_id=course_key, - generatedcertificate__status__in=CertificateStatuses.PASSED_STATUSES + generatedcertificate__course_id=course_key, generatedcertificate__status__in=CertificateStatuses.PASSED_STATUSES ) @@ -796,10 +770,10 @@ def certificate_info_for_user(user, course_id, grade, user_is_allowlisted, user_ """ Returns the certificate info for a user for grade report. """ - certificate_is_delivered = 'N' - certificate_type = 'N/A' + certificate_is_delivered = "N" + certificate_type = "N/A" status = _certificate_status(user_certificate) - certificate_generated = status['status'] == CertificateStatuses.downloadable + certificate_generated = status["status"] == CertificateStatuses.downloadable course_overview = get_course_overview_or_none(course_id) if not course_overview: return None @@ -808,18 +782,17 @@ def certificate_info_for_user(user, course_id, grade, user_is_allowlisted, user_ course_overview.certificates_show_before_end, course_overview.has_ended(), course_overview.certificate_available_date, - course_overview.self_paced + course_overview.self_paced, ) enrollment_mode, __ = CourseEnrollment.enrollment_mode_for_user(user, course_id) mode_is_verified = enrollment_mode in CourseMode.VERIFIED_MODES user_is_verified = grade is not None and mode_is_verified - eligible_for_certificate = 'Y' if (user_is_allowlisted or user_is_verified or certificate_generated) \ - else 'N' + eligible_for_certificate = "Y" if (user_is_allowlisted or user_is_verified or certificate_generated) else "N" if certificate_generated and can_have_certificate: - certificate_is_delivered = 'Y' - certificate_type = status['mode'] + certificate_is_delivered = "Y" + certificate_type = status["mode"] return [eligible_for_certificate, certificate_is_delivered, certificate_type] @@ -854,11 +827,11 @@ def can_show_certificate_message(course, student, course_grade, certificates_ena has_passed_or_is_allowlisted = _has_passed_or_is_allowlisted(course, student, course_grade) return ( - (auto_cert_gen_enabled or certificates_enabled_for_course) and - has_active_enrollment and - certificates_are_viewable and - has_passed_or_is_allowlisted and - (not is_beta_tester) + (auto_cert_gen_enabled or certificates_enabled_for_course) + and has_active_enrollment + and certificates_are_viewable + and has_passed_or_is_allowlisted + and (not is_beta_tester) ) @@ -935,20 +908,15 @@ def invalidate_certificate(user_id, course_key_or_id, source): """ course_key = _get_key(course_key_or_id, CourseKey) if _is_on_certificate_allowlist(user_id, course_key): - log.info(f'User {user_id} is on the allowlist for {course_key}. The certificate will not be invalidated.') + log.info(f"User {user_id} is on the allowlist for {course_key}. The certificate will not be invalidated.") return False try: - generated_certificate = GeneratedCertificate.objects.get( - user=user_id, - course_id=course_key - ) + generated_certificate = GeneratedCertificate.objects.get(user=user_id, course_id=course_key) generated_certificate.invalidate(source=source) except ObjectDoesNotExist: log.warning( - 'Invalidation failed because a certificate for user %d in course %s does not exist.', - user_id, - course_key + "Invalidation failed because a certificate for user %d in course %s does not exist.", user_id, course_key ) return False diff --git a/lms/djangoapps/certificates/tests/test_api.py b/lms/djangoapps/certificates/tests/test_api.py index c766d4250bd9..21d04a7e3da6 100644 --- a/lms/djangoapps/certificates/tests/test_api.py +++ b/lms/djangoapps/certificates/tests/test_api.py @@ -1,6 +1,5 @@ """Tests for the certificates Python API. """ - import uuid from contextlib import contextmanager from datetime import datetime, timedelta @@ -20,17 +19,10 @@ from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.locator import CourseLocator from testfixtures import LogCapture -from xmodule.data import CertificatesDisplayBehaviors -from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, SharedModuleStoreTestCase -from xmodule.modulestore.tests.factories import CourseFactory from common.djangoapps.course_modes.models import CourseMode from common.djangoapps.course_modes.tests.factories import CourseModeFactory -from common.djangoapps.student.tests.factories import ( - CourseEnrollmentFactory, - GlobalStaffFactory, - UserFactory -) +from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, GlobalStaffFactory, UserFactory from common.djangoapps.util.testing import EventTestMixin from lms.djangoapps.certificates.api import ( auto_certificate_generation_enabled, @@ -38,8 +30,8 @@ can_be_added_to_allowlist, can_show_certificate_available_date_field, can_show_certificate_message, - certificate_status_for_student, certificate_downloadable_status, + certificate_status_for_student, clear_pii_from_certificate_records_for_user, create_certificate_invalidation_entry, create_or_update_certificate_allowlist_entry, @@ -69,41 +61,45 @@ ) from lms.djangoapps.certificates.tests.factories import ( CertificateAllowlistFactory, + CertificateInvalidationFactory, GeneratedCertificateFactory, - CertificateInvalidationFactory ) from lms.djangoapps.certificates.tests.test_generation_handler import ID_VERIFIED_METHOD, PASSING_GRADE_METHOD from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory from openedx.core.djangoapps.site_configuration.tests.test_util import with_site_configuration +from xmodule.data import CertificatesDisplayBehaviors +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, SharedModuleStoreTestCase +from xmodule.modulestore.tests.factories import CourseFactory -CAN_GENERATE_METHOD = 'lms.djangoapps.certificates.generation_handler._can_generate_regular_certificate' -BETA_TESTER_METHOD = 'lms.djangoapps.certificates.api.access.is_beta_tester' -CERTS_VIEWABLE_METHOD = 'lms.djangoapps.certificates.api.certificates_viewable_for_course' -PASSED_OR_ALLOWLISTED_METHOD = 'lms.djangoapps.certificates.api._has_passed_or_is_allowlisted' +CAN_GENERATE_METHOD = "lms.djangoapps.certificates.generation_handler._can_generate_regular_certificate" +BETA_TESTER_METHOD = "lms.djangoapps.certificates.api.access.is_beta_tester" +CERTS_VIEWABLE_METHOD = "lms.djangoapps.certificates.api.certificates_viewable_for_course" +PASSED_OR_ALLOWLISTED_METHOD = "lms.djangoapps.certificates.api._has_passed_or_is_allowlisted" FEATURES_WITH_CERTS_ENABLED = settings.FEATURES.copy() -FEATURES_WITH_CERTS_ENABLED['CERTIFICATES_HTML_VIEW'] = True +FEATURES_WITH_CERTS_ENABLED["CERTIFICATES_HTML_VIEW"] = True class WebCertificateTestMixin: """ Mixin with helpers for testing Web Certificates. """ + def _setup_course_certificate(self): """ Creates certificate configuration for course """ certificates = [ { - 'id': 1, - 'name': 'Test Certificate Name', - 'description': 'Test Certificate Description', - 'course_title': 'tes_course_title', - 'signatories': [], - 'version': 1, - 'is_active': True + "id": 1, + "name": "Test Certificate Name", + "description": "Test Certificate Description", + "course_title": "tes_course_title", + "signatories": [], + "version": 1, + "is_active": True, } ] - self.course.certificates = {'certificates': certificates} + self.course.certificates = {"certificates": certificates} self.course.cert_html_view_enabled = True self.course.save() self.store.update_item(self.course, self.user.id) @@ -111,8 +107,9 @@ def _setup_course_certificate(self): @ddt.ddt class CertificateDownloadableStatusTests(WebCertificateTestMixin, ModuleStoreTestCase): - """Tests for the `certificate_downloadable_status` helper function. """ - ENABLED_SIGNALS = ['course_published'] + """Tests for the `certificate_downloadable_status` helper function.""" + + ENABLED_SIGNALS = ["course_published"] def setUp(self): super().setUp() @@ -120,19 +117,16 @@ def setUp(self): self.student = UserFactory() self.student_no_cert = UserFactory() self.course = CourseFactory.create( - org='edx', - number='verified', - display_name='Verified Course', + org="edx", + number="verified", + display_name="Verified Course", end=datetime.now(pytz.UTC), self_paced=False, - certificate_available_date=datetime.now(pytz.UTC) - timedelta(days=2) + certificate_available_date=datetime.now(pytz.UTC) - timedelta(days=2), ) GeneratedCertificateFactory.create( - user=self.student, - course_id=self.course.id, - status=CertificateStatuses.downloadable, - mode='verified' + user=self.student, course_id=self.course.id, status=CertificateStatuses.downloadable, mode="verified" ) self.request_factory = RequestFactory() @@ -140,41 +134,38 @@ def setUp(self): def test_cert_status_with_generating(self): cert_user = UserFactory() GeneratedCertificateFactory.create( - user=cert_user, - course_id=self.course.id, - status=CertificateStatuses.generating, - mode='verified' + user=cert_user, course_id=self.course.id, status=CertificateStatuses.generating, mode="verified" ) - assert certificate_downloadable_status(cert_user, self.course.id) ==\ - {'is_downloadable': False, - 'is_generating': True, - 'is_unverified': False, - 'download_url': None, - 'uuid': None} + assert certificate_downloadable_status(cert_user, self.course.id) == { + "is_downloadable": False, + "is_generating": True, + "is_unverified": False, + "download_url": None, + "uuid": None, + } def test_cert_status_with_error(self): cert_user = UserFactory() GeneratedCertificateFactory.create( - user=cert_user, - course_id=self.course.id, - status=CertificateStatuses.error, - mode='verified' + user=cert_user, course_id=self.course.id, status=CertificateStatuses.error, mode="verified" ) - assert certificate_downloadable_status(cert_user, self.course.id) ==\ - {'is_downloadable': False, - 'is_generating': True, - 'is_unverified': False, - 'download_url': None, - 'uuid': None} + assert certificate_downloadable_status(cert_user, self.course.id) == { + "is_downloadable": False, + "is_generating": True, + "is_unverified": False, + "download_url": None, + "uuid": None, + } def test_without_cert(self): - assert certificate_downloadable_status(self.student_no_cert, self.course.id) ==\ - {'is_downloadable': False, - 'is_generating': False, - 'is_unverified': False, - 'download_url': None, - 'uuid': None} + assert certificate_downloadable_status(self.student_no_cert, self.course.id) == { + "is_downloadable": False, + "is_generating": False, + "is_unverified": False, + "download_url": None, + "uuid": None, + } def verify_downloadable_pdf_cert(self): """ @@ -186,44 +177,46 @@ def verify_downloadable_pdf_cert(self): user=cert_user, course_id=self.course.id, status=CertificateStatuses.downloadable, - mode='verified', - download_url='www.google.com', + mode="verified", + download_url="www.google.com", ) - assert certificate_downloadable_status(cert_user, self.course.id) ==\ - {'is_downloadable': True, - 'is_generating': False, - 'is_unverified': False, - 'download_url': 'www.google.com', - 'is_pdf_certificate': True, - 'uuid': cert.verify_uuid} + assert certificate_downloadable_status(cert_user, self.course.id) == { + "is_downloadable": True, + "is_generating": False, + "is_unverified": False, + "download_url": "www.google.com", + "is_pdf_certificate": True, + "uuid": cert.verify_uuid, + } - @patch.dict(settings.FEATURES, {'CERTIFICATES_HTML_VIEW': True}) + @patch.dict(settings.FEATURES, {"CERTIFICATES_HTML_VIEW": True}) def test_pdf_cert_with_html_enabled(self): self.verify_downloadable_pdf_cert() def test_pdf_cert_with_html_disabled(self): self.verify_downloadable_pdf_cert() - @patch.dict(settings.FEATURES, {'CERTIFICATES_HTML_VIEW': True}) + @patch.dict(settings.FEATURES, {"CERTIFICATES_HTML_VIEW": True}) def test_with_downloadable_web_cert(self): cert_status = certificate_status_for_student(self.student, self.course.id) - assert certificate_downloadable_status(self.student, self.course.id) ==\ - {'is_downloadable': True, - 'is_generating': False, - 'is_unverified': False, - 'download_url': f'/certificates/{cert_status["uuid"]}', - 'is_pdf_certificate': False, - 'uuid': cert_status['uuid']} + assert certificate_downloadable_status(self.student, self.course.id) == { + "is_downloadable": True, + "is_generating": False, + "is_unverified": False, + "download_url": f'/certificates/{cert_status["uuid"]}', + "is_pdf_certificate": False, + "uuid": cert_status["uuid"], + } @ddt.data( (False, timedelta(days=2), False, True), (False, -timedelta(days=2), True, None), - (True, timedelta(days=2), True, None) + (True, timedelta(days=2), True, None), ) @ddt.unpack - @patch.dict(settings.FEATURES, {'CERTIFICATES_HTML_VIEW': True}) - @patch.dict(settings.FEATURES, {'ENABLE_V2_CERT_DISPLAY_SETTINGS': False}) + @patch.dict(settings.FEATURES, {"CERTIFICATES_HTML_VIEW": True}) + @patch.dict(settings.FEATURES, {"ENABLE_V2_CERT_DISPLAY_SETTINGS": False}) def test_cert_api_return_v1(self, self_paced, cert_avail_delta, cert_downloadable_status, earned_but_not_available): """ Test 'downloadable status' @@ -236,8 +229,8 @@ def test_cert_api_return_v1(self, self_paced, cert_avail_delta, cert_downloadabl self._setup_course_certificate() downloadable_status = certificate_downloadable_status(self.student, self.course.id) - assert downloadable_status['is_downloadable'] == cert_downloadable_status - assert downloadable_status.get('earned_but_not_available') == earned_but_not_available + assert downloadable_status["is_downloadable"] == cert_downloadable_status + assert downloadable_status.get("earned_but_not_available") == earned_but_not_available @ddt.data( (True, timedelta(days=2), CertificatesDisplayBehaviors.END_WITH_DATE, True, None), @@ -249,15 +242,15 @@ def test_cert_api_return_v1(self, self_paced, cert_avail_delta, cert_downloadabl (False, timedelta(days=2), CertificatesDisplayBehaviors.END_WITH_DATE, False, True), ) @ddt.unpack - @patch.dict(settings.FEATURES, {'CERTIFICATES_HTML_VIEW': True}) - @patch.dict(settings.FEATURES, {'ENABLE_V2_CERT_DISPLAY_SETTINGS': True}) + @patch.dict(settings.FEATURES, {"CERTIFICATES_HTML_VIEW": True}) + @patch.dict(settings.FEATURES, {"ENABLE_V2_CERT_DISPLAY_SETTINGS": True}) def test_cert_api_return_v2( self, self_paced, cert_avail_delta, certificates_display_behavior, cert_downloadable_status, - earned_but_not_available + earned_but_not_available, ): """ Test 'downloadable status' @@ -271,36 +264,26 @@ def test_cert_api_return_v2( self._setup_course_certificate() downloadable_status = certificate_downloadable_status(self.student, self.course.id) - assert downloadable_status['is_downloadable'] == cert_downloadable_status - assert downloadable_status.get('earned_but_not_available') == earned_but_not_available + assert downloadable_status["is_downloadable"] == cert_downloadable_status + assert downloadable_status.get("earned_but_not_available") == earned_but_not_available @ddt.ddt class CertificateIsInvalid(WebCertificateTestMixin, ModuleStoreTestCase): - """Tests for the `is_certificate_invalid` helper function. """ + """Tests for the `is_certificate_invalid` helper function.""" def setUp(self): super().setUp() self.student = UserFactory() - self.course = CourseFactory.create( - org='edx', - number='verified', - display_name='Verified Course' - ) - self.course_overview = CourseOverviewFactory.create( - id=self.course.id - ) + self.course = CourseFactory.create(org="edx", number="verified", display_name="Verified Course") + self.course_overview = CourseOverviewFactory.create(id=self.course.id) self.global_staff = GlobalStaffFactory() self.request_factory = RequestFactory() def test_method_with_no_certificate(self): - """ Test the case when there is no certificate for a user for a specific course. """ - course = CourseFactory.create( - org='edx', - number='honor', - display_name='Course 1' - ) + """Test the case when there is no certificate for a user for a specific course.""" + course = CourseFactory.create(org="edx", number="honor", display_name="Course 1") # Also check query count for 'is_certificate_invalid' method. with self.assertNumQueries(1): assert not is_certificate_invalidated(self.student, course.id) @@ -315,8 +298,8 @@ def test_method_with_no_certificate(self): CertificateStatuses.unavailable, ) def test_method_with_invalidated_cert(self, status): - """ Verify that if certificate is marked as invalid than method will return - True. """ + """Verify that if certificate is marked as invalid than method will return + True.""" generated_cert = self._generate_cert(status) self._invalidate_certificate(generated_cert, True) assert is_certificate_invalidated(self.student, self.course.id) @@ -331,8 +314,8 @@ def test_method_with_invalidated_cert(self, status): CertificateStatuses.unavailable, ) def test_method_with_inactive_invalidated_cert(self, status): - """ Verify that if certificate is valid but it's invalidated status is - false than method will return false. """ + """Verify that if certificate is valid but it's invalidated status is + false than method will return false.""" generated_cert = self._generate_cert(status) self._invalidate_certificate(generated_cert, False) assert not is_certificate_invalidated(self.student, self.course.id) @@ -347,42 +330,36 @@ def test_method_with_inactive_invalidated_cert(self, status): CertificateStatuses.unavailable, ) def test_method_with_all_statues(self, status): - """ Verify method return True if certificate has valid status but it is - marked as invalid in CertificateInvalidation table. """ + """Verify method return True if certificate has valid status but it is + marked as invalid in CertificateInvalidation table.""" certificate = self._generate_cert(status) CertificateInvalidationFactory.create( - generated_certificate=certificate, - invalidated_by=self.global_staff, - active=True + generated_certificate=certificate, invalidated_by=self.global_staff, active=True ) # Also check query count for 'is_certificate_invalid' method. with self.assertNumQueries(2): assert is_certificate_invalidated(self.student, self.course.id) def _invalidate_certificate(self, certificate, active): - """ Dry method to mark certificate as invalid. """ + """Dry method to mark certificate as invalid.""" CertificateInvalidationFactory.create( - generated_certificate=certificate, - invalidated_by=self.global_staff, - active=active + generated_certificate=certificate, invalidated_by=self.global_staff, active=active ) # Invalidate user certificate certificate.invalidate() assert not certificate.is_valid() def _generate_cert(self, status): - """ Dry method to generate certificate. """ + """Dry method to generate certificate.""" return GeneratedCertificateFactory.create( - user=self.student, - course_id=self.course.id, - status=status, - mode='verified' + user=self.student, course_id=self.course.id, status=status, mode="verified" ) class CertificateGetTests(SharedModuleStoreTestCase): - """Tests for the `test_get_certificate_for_user` helper function. """ + """Tests for the `test_get_certificate_for_user` helper function.""" + now = timezone.now() @classmethod @@ -394,31 +371,25 @@ def setUpClass(cls): cls.student = UserFactory() cls.student_no_cert = UserFactory() cls.uuid = uuid.uuid4().hex - cls.nonexistent_course_id = CourseKey.from_string('course-v1:some+fake+course') + cls.nonexistent_course_id = CourseKey.from_string("course-v1:some+fake+course") cls.web_cert_course = CourseFactory.create( - org='edx', - number='verified_1', - display_name='Verified Course 1', - cert_html_view_enabled=True + org="edx", number="verified_1", display_name="Verified Course 1", cert_html_view_enabled=True ) cls.pdf_cert_course = CourseFactory.create( - org='edx', - number='verified_2', - display_name='Verified Course 2', - cert_html_view_enabled=False + org="edx", number="verified_2", display_name="Verified Course 2", cert_html_view_enabled=False ) cls.no_cert_course = CourseFactory.create( - org='edx', - number='verified_3', - display_name='Verified Course 3', + org="edx", + number="verified_3", + display_name="Verified Course 3", ) # certificate for the first course GeneratedCertificateFactory.create( user=cls.student, course_id=cls.web_cert_course.id, status=CertificateStatuses.downloadable, - mode='verified', - download_url='www.google.com', + mode="verified", + download_url="www.google.com", grade="0.88", verify_uuid=cls.uuid, ) @@ -427,16 +398,14 @@ def setUpClass(cls): user=cls.student, course_id=cls.pdf_cert_course.id, status=CertificateStatuses.downloadable, - mode='honor', - download_url='www.gmail.com', + mode="honor", + download_url="www.gmail.com", grade="0.99", verify_uuid=cls.uuid, ) # certificate for a course that will be deleted GeneratedCertificateFactory.create( - user=cls.student, - course_id=cls.nonexistent_course_id, - status=CertificateStatuses.downloadable + user=cls.student, course_id=cls.nonexistent_course_id, status=CertificateStatuses.downloadable ) @classmethod @@ -450,14 +419,14 @@ def test_get_certificate_for_user(self): """ cert = get_certificate_for_user(self.student.username, self.web_cert_course.id) - assert cert['username'] == self.student.username - assert cert['course_key'] == self.web_cert_course.id - assert cert['created'] == self.now - assert cert['type'] == CourseMode.VERIFIED - assert cert['status'] == CertificateStatuses.downloadable - assert cert['grade'] == '0.88' - assert cert['is_passing'] is True - assert cert['download_url'] == 'www.google.com' + assert cert["username"] == self.student.username + assert cert["course_key"] == self.web_cert_course.id + assert cert["created"] == self.now + assert cert["type"] == CourseMode.VERIFIED + assert cert["status"] == CertificateStatuses.downloadable + assert cert["grade"] == "0.88" + assert cert["is_passing"] is True + assert cert["download_url"] == "www.google.com" def test_get_certificate_for_user_id(self): """ @@ -469,7 +438,7 @@ def test_get_certificate_for_user_id(self): assert cert.course_id == self.web_cert_course.id assert cert.mode == CourseMode.VERIFIED assert cert.status == CertificateStatuses.downloadable - assert cert.grade == '0.88' + assert cert.grade == "0.88" def test_get_certificates_for_user(self): """ @@ -477,22 +446,22 @@ def test_get_certificates_for_user(self): """ certs = get_certificates_for_user(self.student.username) assert len(certs) == 2 - assert certs[0]['username'] == self.student.username - assert certs[1]['username'] == self.student.username - assert certs[0]['course_key'] == self.web_cert_course.id - assert certs[1]['course_key'] == self.pdf_cert_course.id - assert certs[0]['created'] == self.now - assert certs[1]['created'] == self.now - assert certs[0]['type'] == CourseMode.VERIFIED - assert certs[1]['type'] == CourseMode.HONOR - assert certs[0]['status'] == CertificateStatuses.downloadable - assert certs[1]['status'] == CertificateStatuses.downloadable - assert certs[0]['is_passing'] is True - assert certs[1]['is_passing'] is True - assert certs[0]['grade'] == '0.88' - assert certs[1]['grade'] == '0.99' - assert certs[0]['download_url'] == 'www.google.com' - assert certs[1]['download_url'] == 'www.gmail.com' + assert certs[0]["username"] == self.student.username + assert certs[1]["username"] == self.student.username + assert certs[0]["course_key"] == self.web_cert_course.id + assert certs[1]["course_key"] == self.pdf_cert_course.id + assert certs[0]["created"] == self.now + assert certs[1]["created"] == self.now + assert certs[0]["type"] == CourseMode.VERIFIED + assert certs[1]["type"] == CourseMode.HONOR + assert certs[0]["status"] == CertificateStatuses.downloadable + assert certs[1]["status"] == CertificateStatuses.downloadable + assert certs[0]["is_passing"] is True + assert certs[1]["is_passing"] is True + assert certs[0]["grade"] == "0.88" + assert certs[1]["grade"] == "0.99" + assert certs[0]["download_url"] == "www.google.com" + assert certs[1]["download_url"] == "www.gmail.com" def test_get_certificates_for_user_by_course_keys(self): """ @@ -505,9 +474,9 @@ def test_get_certificates_for_user_by_course_keys(self): ) assert set(certs.keys()) == {self.web_cert_course.id} cert = certs[self.web_cert_course.id] - assert cert['username'] == self.student.username - assert cert['course_key'] == self.web_cert_course.id - assert cert['download_url'] == 'www.google.com' + assert cert["username"] == self.student.username + assert cert["course_key"] == self.web_cert_course.id + assert cert["download_url"] == "www.google.com" def test_no_certificate_for_user(self): """ @@ -521,45 +490,27 @@ def test_no_certificates_for_user(self): """ assert not get_certificates_for_user(self.student_no_cert.username) - @patch.dict(settings.FEATURES, {'CERTIFICATES_HTML_VIEW': True}) + @patch.dict(settings.FEATURES, {"CERTIFICATES_HTML_VIEW": True}) def test_get_web_certificate_url(self): """ Test the get_certificate_url with a web cert course """ - expected_url = reverse( - 'certificates:render_cert_by_uuid', - kwargs=dict(certificate_uuid=self.uuid) - ) - cert_url = get_certificate_url( - user_id=self.student.id, - course_id=self.web_cert_course.id, - uuid=self.uuid - ) + expected_url = reverse("certificates:render_cert_by_uuid", kwargs=dict(certificate_uuid=self.uuid)) + cert_url = get_certificate_url(user_id=self.student.id, course_id=self.web_cert_course.id, uuid=self.uuid) assert expected_url == cert_url - expected_url = reverse( - 'certificates:render_cert_by_uuid', - kwargs=dict(certificate_uuid=self.uuid) - ) + expected_url = reverse("certificates:render_cert_by_uuid", kwargs=dict(certificate_uuid=self.uuid)) - cert_url = get_certificate_url( - user_id=self.student.id, - course_id=self.web_cert_course.id, - uuid=self.uuid - ) + cert_url = get_certificate_url(user_id=self.student.id, course_id=self.web_cert_course.id, uuid=self.uuid) assert expected_url == cert_url - @patch.dict(settings.FEATURES, {'CERTIFICATES_HTML_VIEW': True}) + @patch.dict(settings.FEATURES, {"CERTIFICATES_HTML_VIEW": True}) def test_get_pdf_certificate_url(self): """ Test the get_certificate_url with a pdf cert course """ - cert_url = get_certificate_url( - user_id=self.student.id, - course_id=self.pdf_cert_course.id, - uuid=self.uuid - ) - assert 'www.gmail.com' == cert_url + cert_url = get_certificate_url(user_id=self.student.id, course_id=self.pdf_cert_course.id, uuid=self.uuid) + assert "www.gmail.com" == cert_url def test_get_certificate_with_deleted_course(self): """ @@ -570,7 +521,7 @@ def test_get_certificate_with_deleted_course(self): @ddt.ddt class GenerateUserCertificatesTest(ModuleStoreTestCase): - """Tests for generating certificates for students. """ + """Tests for generating certificates for students.""" def setUp(self): super().setUp() @@ -585,15 +536,15 @@ def setUp(self): mode=CourseMode.VERIFIED, ) - @patch.dict(settings.FEATURES, {'CERTIFICATES_HTML_VIEW': False}) + @patch.dict(settings.FEATURES, {"CERTIFICATES_HTML_VIEW": False}) def test_cert_url_empty_with_invalid_certificate(self): """ Test certificate url is empty if html view is not enabled and certificate is not yet generated """ url = get_certificate_url(self.user.id, self.course_run_key) - assert url == '' + assert url == "" - @patch.dict(settings.FEATURES, {'CERTIFICATES_HTML_VIEW': True}) + @patch.dict(settings.FEATURES, {"CERTIFICATES_HTML_VIEW": True}) def test_generation(self): """ Test that a cert is successfully generated @@ -609,7 +560,7 @@ def test_generation(self): assert cert.status == CertificateStatuses.downloadable assert cert.mode == CourseMode.VERIFIED - @patch.dict(settings.FEATURES, {'CERTIFICATES_HTML_VIEW': True}) + @patch.dict(settings.FEATURES, {"CERTIFICATES_HTML_VIEW": True}) @ddt.data(True, False) def test_generation_unverified(self, enable_idv_requirement): """ @@ -630,16 +581,13 @@ def test_generation_unverified(self, enable_idv_requirement): else: assert cert.status == CertificateStatuses.downloadable - @patch.dict(settings.FEATURES, {'CERTIFICATES_HTML_VIEW': True}) + @patch.dict(settings.FEATURES, {"CERTIFICATES_HTML_VIEW": True}) def test_generation_notpassing(self): """ Test that a cert is successfully generated with a status of notpassing """ GeneratedCertificateFactory( - user=self.user, - course_id=self.course_run_key, - status=CertificateStatuses.unavailable, - mode=CourseMode.AUDIT + user=self.user, course_id=self.course_run_key, status=CertificateStatuses.unavailable, mode=CourseMode.AUDIT ) with mock.patch(PASSING_GRADE_METHOD, return_value=False): @@ -653,12 +601,12 @@ def test_generation_notpassing(self): @ddt.ddt class CertificateGenerationEnabledTest(EventTestMixin, TestCase): - """Test enabling/disabling self-generated certificates for a course. """ + """Test enabling/disabling self-generated certificates for a course.""" - COURSE_KEY = CourseLocator(org='test', course='test', run='test') + COURSE_KEY = CourseLocator(org="test", course="test", run="test") def setUp(self): # pylint: disable=arguments-differ - super().setUp('lms.djangoapps.certificates.api.tracker') + super().setUp("lms.djangoapps.certificates.api.tracker") # Since model-based configuration is cached, we need # to clear the cache before each test. @@ -670,7 +618,7 @@ def setUp(self): # pylint: disable=arguments-differ (False, True, False), (True, None, False), (True, False, False), - (True, True, True) + (True, True, True), ) @ddt.unpack def test_cert_generation_enabled(self, is_feature_enabled, is_course_enabled, expect_enabled): @@ -679,8 +627,8 @@ def test_cert_generation_enabled(self, is_feature_enabled, is_course_enabled, ex if is_course_enabled is not None: set_cert_generation_enabled(self.COURSE_KEY, is_course_enabled) - cert_event_type = 'enabled' if is_course_enabled else 'disabled' - event_name = '.'.join(['edx', 'certificate', 'generation', cert_event_type]) + cert_event_type = "enabled" if is_course_enabled else "disabled" + event_name = ".".join(["edx", "certificate", "generation", cert_event_type]) self.assert_event_emitted( event_name, course_id=str(self.COURSE_KEY), @@ -709,27 +657,27 @@ def test_setting_is_course_specific(self): self._assert_enabled_for_course(self.COURSE_KEY, True) # Should be disabled for another course - other_course = CourseLocator(org='other', course='other', run='other') + other_course = CourseLocator(org="other", course="other", run="other") self._assert_enabled_for_course(other_course, False) def _assert_enabled_for_course(self, course_key, expect_enabled): - """Check that self-generated certificates are enabled or disabled for the course. """ + """Check that self-generated certificates are enabled or disabled for the course.""" actual_enabled = has_self_generated_certificates_enabled(course_key) assert expect_enabled == actual_enabled @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) class CertificatesBrandingTest(ModuleStoreTestCase): - """Test certificates branding. """ + """Test certificates branding.""" - COURSE_KEY = CourseLocator(org='test', course='test', run='test') + COURSE_KEY = CourseLocator(org="test", course="test", run="test") configuration = { - 'logo_image_url': 'test_site/images/header-logo.png', - 'SITE_NAME': 'test_site.localhost', - 'urls': { - 'ABOUT': 'test-site/about', - 'PRIVACY': 'test-site/privacy', - 'TOS_AND_HONOR': 'test-site/tos-and-honor', + "logo_image_url": "test_site/images/header-logo.png", + "SITE_NAME": "test_site.localhost", + "urls": { + "ABOUT": "test-site/about", + "PRIVACY": "test-site/privacy", + "TOS_AND_HONOR": "test-site/tos-and-honor", }, } @@ -744,13 +692,10 @@ def test_certificate_header_data(self): data = get_certificate_header_context(is_secure=True) # Make sure there are not unexpected keys in dict returned by 'get_certificate_header_context' - self.assertCountEqual( - list(data.keys()), - ['logo_src', 'logo_url'] - ) - assert self.configuration['logo_image_url'] in data['logo_src'] + self.assertCountEqual(list(data.keys()), ["logo_src", "logo_url"]) + assert self.configuration["logo_image_url"] in data["logo_src"] - assert self.configuration['SITE_NAME'] in data['logo_url'] + assert self.configuration["SITE_NAME"] in data["logo_url"] @with_site_configuration(configuration=configuration) def test_certificate_footer_data(self): @@ -763,19 +708,17 @@ def test_certificate_footer_data(self): data = get_certificate_footer_context() # Make sure there are not unexpected keys in dict returned by 'get_certificate_footer_context' - self.assertCountEqual( - list(data.keys()), - ['company_about_url', 'company_privacy_url', 'company_tos_url'] - ) - assert self.configuration['urls']['ABOUT'] in data['company_about_url'] - assert self.configuration['urls']['PRIVACY'] in data['company_privacy_url'] - assert self.configuration['urls']['TOS_AND_HONOR'] in data['company_tos_url'] + self.assertCountEqual(list(data.keys()), ["company_about_url", "company_privacy_url", "company_tos_url"]) + assert self.configuration["urls"]["ABOUT"] in data["company_about_url"] + assert self.configuration["urls"]["PRIVACY"] in data["company_privacy_url"] + assert self.configuration["urls"]["TOS_AND_HONOR"] in data["company_tos_url"] class CertificateAllowlistTests(ModuleStoreTestCase): """ Tests for allowlist functionality. """ + def setUp(self): super().setUp() @@ -823,10 +766,7 @@ def test_remove_allowlist_entry_with_certificate(self): """ CertificateAllowlistFactory.create(course_id=self.course_run_key, user=self.user) GeneratedCertificateFactory.create( - user=self.user, - course_id=self.course_run_key, - status=CertificateStatuses.downloadable, - mode='verified' + user=self.user, course_id=self.course_run_key, status=CertificateStatuses.downloadable, mode="verified" ) assert is_on_allowlist(self.user, self.course_run_key) @@ -862,7 +802,7 @@ def test_get_allowlist_entry_dne(self): """ expected_messages = [ f"Attempting to retrieve an allowlist entry for student {self.user.id} in course {self.course_run_key}.", - f"No allowlist entry found for student {self.user.id} in course {self.course_run_key}." + f"No allowlist entry found for student {self.user.id} in course {self.course_run_key}.", ] with LogCapture() as log: @@ -919,15 +859,10 @@ def test_can_be_added_to_allowlist_certificate_invalidated(self): invalidation list. """ certificate = GeneratedCertificateFactory.create( - user=self.user, - course_id=self.course_run_key, - status=CertificateStatuses.unavailable, - mode='verified' + user=self.user, course_id=self.course_run_key, status=CertificateStatuses.unavailable, mode="verified" ) CertificateInvalidationFactory.create( - generated_certificate=certificate, - invalidated_by=self.global_staff, - active=True + generated_certificate=certificate, invalidated_by=self.global_staff, active=True ) assert not can_be_added_to_allowlist(self.user, self.course_run_key) @@ -1005,7 +940,7 @@ def test_add_and_update(self): Test add and update of the allowlist """ u1 = UserFactory() - notes = 'blah' + notes = "blah" # Check before adding user entry = get_allowlist_entry(u1, self.course_run_key) @@ -1017,7 +952,7 @@ def test_add_and_update(self): assert entry.notes == notes # Update user - new_notes = 'really useful info' + new_notes = "really useful info" create_or_update_certificate_allowlist_entry(u1, self.course_run_key, new_notes) entry = get_allowlist_entry(u1, self.course_run_key) assert entry.notes == new_notes @@ -1027,7 +962,7 @@ def test_remove(self): Test removal from the allowlist """ u1 = UserFactory() - notes = 'I had a thought....' + notes = "I had a thought...." # Add user create_or_update_certificate_allowlist_entry(u1, self.course_run_key, notes) @@ -1044,6 +979,7 @@ class CertificateInvalidationTests(ModuleStoreTestCase): """ Tests for the certificate invalidation functionality. """ + def setUp(self): super().setUp() @@ -1065,10 +1001,7 @@ def test_create_certificate_invalidation_entry(self): invalidation entries. This is functionality the Instructor Dashboard django app relies on. """ certificate = GeneratedCertificateFactory.create( - user=self.user, - course_id=self.course_run_key, - status=CertificateStatuses.unavailable, - mode='verified' + user=self.user, course_id=self.course_run_key, status=CertificateStatuses.unavailable, mode="verified" ) result = create_certificate_invalidation_entry(certificate, self.global_staff, "Test!") @@ -1082,16 +1015,11 @@ def test_get_certificate_invalidation_entry(self): Test to verify that we can retrieve a certificate invalidation entry for a learner. """ certificate = GeneratedCertificateFactory.create( - user=self.user, - course_id=self.course_run_key, - status=CertificateStatuses.unavailable, - mode='verified' + user=self.user, course_id=self.course_run_key, status=CertificateStatuses.unavailable, mode="verified" ) invalidation = CertificateInvalidationFactory.create( - generated_certificate=certificate, - invalidated_by=self.global_staff, - active=True + generated_certificate=certificate, invalidated_by=self.global_staff, active=True ) retrieved_invalidation = get_certificate_invalidation_entry(certificate) @@ -1105,10 +1033,7 @@ def test_get_certificate_invalidation_entry_dne(self): Test to verify behavior when a certificate invalidation entry does not exist. """ certificate = GeneratedCertificateFactory.create( - user=self.user, - course_id=self.course_run_key, - status=CertificateStatuses.unavailable, - mode='verified' + user=self.user, course_id=self.course_run_key, status=CertificateStatuses.unavailable, mode="verified" ) expected_messages = [ @@ -1130,6 +1055,7 @@ class MockGeneratedCertificate: We can't import GeneratedCertificate from LMS here, so we roll our own minimal Certificate model for testing. """ + def __init__(self, user=None, course_id=None, mode=None, status=None): self.user = user self.course_id = course_id @@ -1165,24 +1091,22 @@ class CertificatesApiTestCase(TestCase): """ API tests """ + def setUp(self): super().setUp() self.course = CourseOverviewFactory.create( start=datetime(2017, 1, 1, tzinfo=pytz.UTC), end=datetime(2017, 1, 31, tzinfo=pytz.UTC), - certificate_available_date=None + certificate_available_date=None, ) self.user = UserFactory.create() self.enrollment = CourseEnrollmentFactory( user=self.user, course_id=self.course.id, is_active=True, - mode='audit', - ) - self.certificate = MockGeneratedCertificate( - user=self.user, - course_id=self.course.id + mode="audit", ) + self.certificate = MockGeneratedCertificate(user=self.user, course_id=self.course.id) @ddt.data(True, False) def test_auto_certificate_generation_enabled(self, feature_enabled): @@ -1196,9 +1120,7 @@ def test_auto_certificate_generation_enabled(self, feature_enabled): (False, False, False), # feature not enabled and instructor-paced should return False ) @ddt.unpack - def test_can_show_certificate_available_date_field( - self, feature_enabled, is_self_paced, expected_value - ): + def test_can_show_certificate_available_date_field(self, feature_enabled, is_self_paced, expected_value): self.course.self_paced = is_self_paced with configure_waffle_namespace(feature_enabled): assert expected_value == can_show_certificate_available_date_field(self.course) @@ -1210,9 +1132,7 @@ def test_can_show_certificate_available_date_field( (False, False, False), # feature not enabled and instructor-paced should return False ) @ddt.unpack - def test_available_vs_display_date( - self, feature_enabled, is_self_paced, uses_avail_date - ): + def test_available_vs_display_date(self, feature_enabled, is_self_paced, uses_avail_date): self.course.self_paced = is_self_paced with configure_waffle_namespace(feature_enabled): @@ -1245,6 +1165,7 @@ class CertificatesMessagingTestCase(ModuleStoreTestCase): """ API tests for certificate messaging """ + def setUp(self): super().setUp() self.course = CourseOverviewFactory.create() @@ -1275,6 +1196,7 @@ class CertificatesLearnerRetirementFunctionality(ModuleStoreTestCase): API tests for utility functions used as part of the learner retirement pipeline to remove PII from certificate records. """ + def setUp(self): super().setUp() self.user = UserFactory() diff --git a/openedx/core/djangoapps/catalog/tests/test_utils.py b/openedx/core/djangoapps/catalog/tests/test_utils.py index 36feb63a6738..fc85b949e80b 100644 --- a/openedx/core/djangoapps/catalog/tests/test_utils.py +++ b/openedx/core/djangoapps/catalog/tests/test_utils.py @@ -1,4 +1,5 @@ """Tests covering utilities for integrating with the catalog service.""" + # pylint: disable=missing-docstring @@ -16,6 +17,7 @@ from common.djangoapps.course_modes.helpers import CourseMode from common.djangoapps.course_modes.tests.factories import CourseModeFactory from common.djangoapps.entitlements.tests.factories import CourseEntitlementFactory +from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory from openedx.core.constants import COURSE_UNPUBLISHED from openedx.core.djangoapps.catalog.cache import ( CATALOG_COURSE_PROGRAMS_CACHE_KEY_TPL, @@ -25,7 +27,7 @@ PROGRAMS_BY_TYPE_CACHE_KEY_TPL, PROGRAMS_BY_TYPE_SLUG_CACHE_KEY_TPL, SITE_PATHWAY_IDS_CACHE_KEY_TPL, - SITE_PROGRAM_UUIDS_CACHE_KEY_TPL + SITE_PROGRAM_UUIDS_CACHE_KEY_TPL, ) from openedx.core.djangoapps.catalog.models import CatalogIntegration from openedx.core.djangoapps.catalog.tests.factories import ( @@ -33,14 +35,13 @@ CourseRunFactory, PathwayFactory, ProgramFactory, + ProgramTypeAttrsFactory, ProgramTypeFactory, - ProgramTypeAttrsFactory ) from openedx.core.djangoapps.catalog.tests.mixins import CatalogIntegrationMixin from openedx.core.djangoapps.catalog.utils import ( child_programs, course_run_keys_for_program, - is_course_run_in_program, get_course_run_details, get_course_runs, get_course_runs_for_course, @@ -53,23 +54,23 @@ get_programs_by_type, get_programs_by_type_slug, get_visible_sessions_for_entitlement, + is_course_run_in_program, normalize_program_type, ) from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory -from openedx.core.djangolib.testing.utils import CacheIsolationTestCase, skip_unless_lms -from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory from openedx.core.djangoapps.site_configuration.tests.test_util import with_site_configuration_context +from openedx.core.djangolib.testing.utils import CacheIsolationTestCase, skip_unless_lms -UTILS_MODULE = 'openedx.core.djangoapps.catalog.utils' +UTILS_MODULE = "openedx.core.djangoapps.catalog.utils" User = get_user_model() # pylint: disable=invalid-name @skip_unless_lms -@mock.patch(UTILS_MODULE + '.logger.info') -@mock.patch(UTILS_MODULE + '.logger.warning') +@mock.patch(UTILS_MODULE + ".logger.info") +@mock.patch(UTILS_MODULE + ".logger.warning") class TestGetPrograms(CacheIsolationTestCase): - ENABLED_CACHES = ['default'] + ENABLED_CACHES = ["default"] def setUp(self): super().setUp() @@ -79,36 +80,35 @@ def test_get_many(self, mock_warning, mock_info): programs = ProgramFactory.create_batch(3) # Cache details for 2 of 3 programs. - partial_programs = { - PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']): program for program in programs[:2] - } + partial_programs = {PROGRAM_CACHE_KEY_TPL.format(uuid=program["uuid"]): program for program in programs[:2]} cache.set_many(partial_programs, None) # When called before UUIDs are cached, the function should return an # empty list and log a warning. - with with_site_configuration_context(domain=self.site.name, configuration={'COURSE_CATALOG_API_URL': 'foo'}): + with with_site_configuration_context(domain=self.site.name, configuration={"COURSE_CATALOG_API_URL": "foo"}): assert get_programs(site=self.site) == [] mock_warning.assert_called_once_with( - f'Failed to get program UUIDs from the cache for site {self.site.domain}.' + f"Failed to get program UUIDs from the cache for site {self.site.domain}." ) mock_warning.reset_mock() # Cache UUIDs for all 3 programs. cache.set( SITE_PROGRAM_UUIDS_CACHE_KEY_TPL.format(domain=self.site.domain), - [program['uuid'] for program in programs], - None + [program["uuid"] for program in programs], + None, ) actual_programs = get_programs(site=self.site) # The 2 cached programs should be returned while info and warning # messages should be logged for the missing one. - assert {program['uuid'] for program in actual_programs} == \ - {program['uuid'] for program in partial_programs.values()} - mock_info.assert_called_with('Failed to get details for 1 programs. Retrying.') + assert {program["uuid"] for program in actual_programs} == { + program["uuid"] for program in partial_programs.values() + } + mock_info.assert_called_with("Failed to get details for 1 programs. Retrying.") mock_warning.assert_called_with( - 'Failed to get details for program {uuid} from the cache.'.format(uuid=programs[2]['uuid']) + "Failed to get details for program {uuid} from the cache.".format(uuid=programs[2]["uuid"]) ) mock_warning.reset_mock() @@ -117,77 +117,67 @@ def test_get_many(self, mock_warning, mock_info): # of the cache above, so all we need to do here is verify the accuracy of # the data itself. for program in actual_programs: - key = PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']) + key = PROGRAM_CACHE_KEY_TPL.format(uuid=program["uuid"]) assert program == partial_programs[key] # Cache details for all 3 programs. - all_programs = { - PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']): program for program in programs - } + all_programs = {PROGRAM_CACHE_KEY_TPL.format(uuid=program["uuid"]): program for program in programs} cache.set_many(all_programs, None) actual_programs = get_programs(site=self.site) # All 3 programs should be returned. - assert {program['uuid'] for program in actual_programs} ==\ - {program['uuid'] for program in all_programs.values()} + assert {program["uuid"] for program in actual_programs} == { + program["uuid"] for program in all_programs.values() + } assert not mock_warning.called for program in actual_programs: - key = PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']) + key = PROGRAM_CACHE_KEY_TPL.format(uuid=program["uuid"]) assert program == all_programs[key] - @mock.patch(UTILS_MODULE + '.cache') + @mock.patch(UTILS_MODULE + ".cache") def test_get_many_with_missing(self, mock_cache, mock_warning, mock_info): programs = ProgramFactory.create_batch(3) - all_programs = { - PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']): program for program in programs - } + all_programs = {PROGRAM_CACHE_KEY_TPL.format(uuid=program["uuid"]): program for program in programs} - partial_programs = { - PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']): program for program in programs[:2] - } + partial_programs = {PROGRAM_CACHE_KEY_TPL.format(uuid=program["uuid"]): program for program in programs[:2]} def fake_get_many(keys): if len(keys) == 1: - return {PROGRAM_CACHE_KEY_TPL.format(uuid=programs[-1]['uuid']): programs[-1]} + return {PROGRAM_CACHE_KEY_TPL.format(uuid=programs[-1]["uuid"]): programs[-1]} else: return partial_programs - mock_cache.get.return_value = [program['uuid'] for program in programs] + mock_cache.get.return_value = [program["uuid"] for program in programs] mock_cache.get_many.side_effect = fake_get_many - with with_site_configuration_context(domain=self.site.name, configuration={'COURSE_CATALOG_API_URL': 'foo'}): + with with_site_configuration_context(domain=self.site.name, configuration={"COURSE_CATALOG_API_URL": "foo"}): actual_programs = get_programs(site=self.site) - # All 3 cached programs should be returned. An info message should be - # logged about the one that was initially missing, but the code should - # be able to stitch together all the details. - assert {program['uuid'] for program in actual_programs} ==\ - {program['uuid'] for program in all_programs.values()} + # All 3 cached programs should be returned. An info message should be + # logged about the one that was initially missing, but the code should + # be able to stitch together all the details. + assert {program["uuid"] for program in actual_programs} == { + program["uuid"] for program in all_programs.values() + } assert not mock_warning.called - mock_info.assert_called_with('Failed to get details for 1 programs. Retrying.') + mock_info.assert_called_with("Failed to get details for 1 programs. Retrying.") for program in actual_programs: - key = PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']) + key = PROGRAM_CACHE_KEY_TPL.format(uuid=program["uuid"]) assert program == all_programs[key] def test_get_one(self, mock_warning, _mock_info): expected_program = ProgramFactory() - expected_uuid = expected_program['uuid'] + expected_uuid = expected_program["uuid"] assert get_programs(uuid=expected_uuid) is None - mock_warning.assert_called_once_with( - f'Failed to get details for program {expected_uuid} from the cache.' - ) + mock_warning.assert_called_once_with(f"Failed to get details for program {expected_uuid} from the cache.") mock_warning.reset_mock() - cache.set( - PROGRAM_CACHE_KEY_TPL.format(uuid=expected_uuid), - expected_program, - None - ) + cache.set(PROGRAM_CACHE_KEY_TPL.format(uuid=expected_uuid), expected_program, None) actual_program = get_programs(uuid=expected_uuid) assert actual_program == expected_program @@ -195,20 +185,12 @@ def test_get_one(self, mock_warning, _mock_info): def test_get_from_course(self, mock_warning, _mock_info): expected_program = ProgramFactory() - expected_course = expected_program['courses'][0]['course_runs'][0]['key'] + expected_course = expected_program["courses"][0]["course_runs"][0]["key"] assert get_programs(course=expected_course) == [] - cache.set( - COURSE_PROGRAMS_CACHE_KEY_TPL.format(course_run_id=expected_course), - [expected_program['uuid']], - None - ) - cache.set( - PROGRAM_CACHE_KEY_TPL.format(uuid=expected_program['uuid']), - expected_program, - None - ) + cache.set(COURSE_PROGRAMS_CACHE_KEY_TPL.format(course_run_id=expected_course), [expected_program["uuid"]], None) + cache.set(PROGRAM_CACHE_KEY_TPL.format(uuid=expected_program["uuid"]), expected_program, None) actual_program = get_programs(course=expected_course) assert actual_program == [expected_program] @@ -218,18 +200,10 @@ def test_get_via_uuids(self, mock_warning, _mock_info): first_program = ProgramFactory() second_program = ProgramFactory() - cache.set( - PROGRAM_CACHE_KEY_TPL.format(uuid=first_program['uuid']), - first_program, - None - ) - cache.set( - PROGRAM_CACHE_KEY_TPL.format(uuid=second_program['uuid']), - second_program, - None - ) + cache.set(PROGRAM_CACHE_KEY_TPL.format(uuid=first_program["uuid"]), first_program, None) + cache.set(PROGRAM_CACHE_KEY_TPL.format(uuid=second_program["uuid"]), second_program, None) - results = get_programs(uuids=[first_program['uuid'], second_program['uuid']]) + results = get_programs(uuids=[first_program["uuid"], second_program["uuid"]]) assert first_program in results assert second_program in results @@ -237,32 +211,28 @@ def test_get_via_uuids(self, mock_warning, _mock_info): def test_get_from_catalog_course(self, mock_warning, _mock_info): expected_program = ProgramFactory() - expected_catalog_course = expected_program['courses'][0] + expected_catalog_course = expected_program["courses"][0] - assert get_programs(catalog_course_uuid=expected_catalog_course['uuid']) == [] + assert get_programs(catalog_course_uuid=expected_catalog_course["uuid"]) == [] cache.set( - CATALOG_COURSE_PROGRAMS_CACHE_KEY_TPL.format(course_uuid=expected_catalog_course['uuid']), - [expected_program['uuid']], - None - ) - cache.set( - PROGRAM_CACHE_KEY_TPL.format(uuid=expected_program['uuid']), - expected_program, - None + CATALOG_COURSE_PROGRAMS_CACHE_KEY_TPL.format(course_uuid=expected_catalog_course["uuid"]), + [expected_program["uuid"]], + None, ) + cache.set(PROGRAM_CACHE_KEY_TPL.format(uuid=expected_program["uuid"]), expected_program, None) - actual_program = get_programs(catalog_course_uuid=expected_catalog_course['uuid']) + actual_program = get_programs(catalog_course_uuid=expected_catalog_course["uuid"]) assert actual_program == [expected_program] assert not mock_warning.called @skip_unless_lms -@mock.patch(UTILS_MODULE + '.logger.info') -@mock.patch(UTILS_MODULE + '.logger.warning') +@mock.patch(UTILS_MODULE + ".logger.info") +@mock.patch(UTILS_MODULE + ".logger.warning") class TestGetPathways(CacheIsolationTestCase): - ENABLED_CACHES = ['default'] + ENABLED_CACHES = ["default"] def setUp(self): super().setUp() @@ -272,33 +242,32 @@ def test_get_many(self, mock_warning, mock_info): pathways = PathwayFactory.create_batch(3) # Cache details for 2 of 3 programs. - partial_pathways = { - PATHWAY_CACHE_KEY_TPL.format(id=pathway['id']): pathway for pathway in pathways[:2] - } + partial_pathways = {PATHWAY_CACHE_KEY_TPL.format(id=pathway["id"]): pathway for pathway in pathways[:2]} cache.set_many(partial_pathways, None) # When called before pathways are cached, the function should return an # empty list and log a warning. assert get_pathways(self.site) == [] - mock_warning.assert_called_once_with('Failed to get credit pathway ids from the cache.') + mock_warning.assert_called_once_with("Failed to get credit pathway ids from the cache.") mock_warning.reset_mock() # Cache all 3 pathways cache.set( SITE_PATHWAY_IDS_CACHE_KEY_TPL.format(domain=self.site.domain), - [pathway['id'] for pathway in pathways], - None + [pathway["id"] for pathway in pathways], + None, ) actual_pathways = get_pathways(self.site) # The 2 cached pathways should be returned while info and warning # messages should be logged for the missing one. - assert {pathway['id'] for pathway in actual_pathways} ==\ - {pathway['id'] for pathway in partial_pathways.values()} - mock_info.assert_called_with('Failed to get details for 1 pathways. Retrying.') + assert {pathway["id"] for pathway in actual_pathways} == { + pathway["id"] for pathway in partial_pathways.values() + } + mock_info.assert_called_with("Failed to get details for 1 pathways. Retrying.") mock_warning.assert_called_with( - 'Failed to get details for credit pathway {id} from the cache.'.format(id=pathways[2]['id']) + "Failed to get details for credit pathway {id} from the cache.".format(id=pathways[2]["id"]) ) mock_warning.reset_mock() @@ -307,45 +276,38 @@ def test_get_many(self, mock_warning, mock_info): # of the cache above, so all we need to do here is verify the accuracy of # the data itself. for pathway in actual_pathways: - key = PATHWAY_CACHE_KEY_TPL.format(id=pathway['id']) + key = PATHWAY_CACHE_KEY_TPL.format(id=pathway["id"]) assert pathway == partial_pathways[key] # Cache details for all 3 pathways. - all_pathways = { - PATHWAY_CACHE_KEY_TPL.format(id=pathway['id']): pathway for pathway in pathways - } + all_pathways = {PATHWAY_CACHE_KEY_TPL.format(id=pathway["id"]): pathway for pathway in pathways} cache.set_many(all_pathways, None) actual_pathways = get_pathways(self.site) # All 3 pathways should be returned. - assert {pathway['id'] for pathway in actual_pathways} ==\ - {pathway['id'] for pathway in all_pathways.values()} + assert {pathway["id"] for pathway in actual_pathways} == {pathway["id"] for pathway in all_pathways.values()} assert not mock_warning.called for pathway in actual_pathways: - key = PATHWAY_CACHE_KEY_TPL.format(id=pathway['id']) + key = PATHWAY_CACHE_KEY_TPL.format(id=pathway["id"]) assert pathway == all_pathways[key] - @mock.patch(UTILS_MODULE + '.cache') + @mock.patch(UTILS_MODULE + ".cache") def test_get_many_with_missing(self, mock_cache, mock_warning, mock_info): pathways = PathwayFactory.create_batch(3) - all_pathways = { - PATHWAY_CACHE_KEY_TPL.format(id=pathway['id']): pathway for pathway in pathways - } + all_pathways = {PATHWAY_CACHE_KEY_TPL.format(id=pathway["id"]): pathway for pathway in pathways} - partial_pathways = { - PATHWAY_CACHE_KEY_TPL.format(id=pathway['id']): pathway for pathway in pathways[:2] - } + partial_pathways = {PATHWAY_CACHE_KEY_TPL.format(id=pathway["id"]): pathway for pathway in pathways[:2]} def fake_get_many(keys): if len(keys) == 1: - return {PATHWAY_CACHE_KEY_TPL.format(id=pathways[-1]['id']): pathways[-1]} + return {PATHWAY_CACHE_KEY_TPL.format(id=pathways[-1]["id"]): pathways[-1]} else: return partial_pathways - mock_cache.get.return_value = [pathway['id'] for pathway in pathways] + mock_cache.get.return_value = [pathway["id"] for pathway in pathways] mock_cache.get_many.side_effect = fake_get_many actual_pathways = get_pathways(self.site) @@ -353,40 +315,34 @@ def fake_get_many(keys): # All 3 cached pathways should be returned. An info message should be # logged about the one that was initially missing, but the code should # be able to stitch together all the details. - assert {pathway['id'] for pathway in actual_pathways} ==\ - {pathway['id'] for pathway in all_pathways.values()} + assert {pathway["id"] for pathway in actual_pathways} == {pathway["id"] for pathway in all_pathways.values()} assert not mock_warning.called - mock_info.assert_called_with('Failed to get details for 1 pathways. Retrying.') + mock_info.assert_called_with("Failed to get details for 1 pathways. Retrying.") for pathway in actual_pathways: - key = PATHWAY_CACHE_KEY_TPL.format(id=pathway['id']) + key = PATHWAY_CACHE_KEY_TPL.format(id=pathway["id"]) assert pathway == all_pathways[key] def test_get_one(self, mock_warning, _mock_info): expected_pathway = PathwayFactory() - expected_id = expected_pathway['id'] + expected_id = expected_pathway["id"] assert get_pathways(self.site, pathway_id=expected_id) is None - mock_warning.assert_called_once_with( - f'Failed to get details for credit pathway {expected_id} from the cache.' - ) + mock_warning.assert_called_once_with(f"Failed to get details for credit pathway {expected_id} from the cache.") mock_warning.reset_mock() - cache.set( - PATHWAY_CACHE_KEY_TPL.format(id=expected_id), - expected_pathway, - None - ) + cache.set(PATHWAY_CACHE_KEY_TPL.format(id=expected_id), expected_pathway, None) actual_pathway = get_pathways(self.site, pathway_id=expected_id) assert actual_pathway == expected_pathway assert not mock_warning.called -@mock.patch(UTILS_MODULE + '.get_api_data') +@mock.patch(UTILS_MODULE + ".get_api_data") class TestGetProgramTypes(CatalogIntegrationMixin, TestCase): """Tests covering retrieval of program types from the catalog service.""" - @override_settings(COURSE_CATALOG_API_URL='https://api.example.com/v1/') + + @override_settings(COURSE_CATALOG_API_URL="https://api.example.com/v1/") def test_get_program_types(self, mock_get_edx_api_data): """Verify get_program_types returns the expected list of program types.""" program_types = ProgramTypeFactory.create_batch(3) @@ -402,21 +358,18 @@ def test_get_program_types(self, mock_get_edx_api_data): assert data == program_types program = program_types[0] - data = get_program_types(name=program['name']) + data = get_program_types(name=program["name"]) assert data == program -@mock.patch(UTILS_MODULE + '.get_api_data') +@mock.patch(UTILS_MODULE + ".get_api_data") class TestGetCurrency(CatalogIntegrationMixin, TestCase): """Tests covering retrieval of currency data from the catalog service.""" - @override_settings(COURSE_CATALOG_API_URL='https://api.example.com/v1/') + + @override_settings(COURSE_CATALOG_API_URL="https://api.example.com/v1/") def test_get_currency_data(self, mock_get_edx_api_data): """Verify get_currency_data returns the currency data.""" - currency_data = { - "code": "CAD", - "rate": 1.257237, - "symbol": "$" - } + currency_data = {"code": "CAD", "rate": 1.257237, "symbol": "$"} mock_get_edx_api_data.return_value = currency_data # Catalog integration is disabled. @@ -429,11 +382,12 @@ def test_get_currency_data(self, mock_get_edx_api_data): assert data == currency_data -@mock.patch(UTILS_MODULE + '.get_currency_data') +@mock.patch(UTILS_MODULE + ".get_currency_data") class TestGetLocalizedPriceText(TestCase): """ Tests covering converting prices to a localized currency """ + def test_localized_string(self, mock_get_currency_data): currency_data = { "BEL": {"rate": 0.835621, "code": "EUR", "symbol": "\u20ac"}, @@ -442,20 +396,19 @@ def test_localized_string(self, mock_get_currency_data): } mock_get_currency_data.return_value = currency_data - request = RequestFactory().get('/dummy-url') - request.session = { - 'country_code': 'CA' - } - expected_result = '$20 CAD' + request = RequestFactory().get("/dummy-url") + request.session = {"country_code": "CA"} + expected_result = "$20 CAD" assert get_localized_price_text(10, request) == expected_result @skip_unless_lms -@mock.patch(UTILS_MODULE + '.get_api_data') +@mock.patch(UTILS_MODULE + ".get_api_data") class TestGetCourseRuns(CatalogIntegrationMixin, CacheIsolationTestCase): """ Tests covering retrieval of course runs from the catalog service. """ + def setUp(self): super().setUp() @@ -468,17 +421,19 @@ def assert_contract(self, call_args): """ args, kwargs = call_args - for arg in (self.catalog_integration, 'course_runs'): + for arg in (self.catalog_integration, "course_runs"): assert arg in args - assert kwargs['base_api_url'] == self.catalog_integration.get_internal_api_url() # pylint: disable=protected-access, line-too-long + assert ( + kwargs["base_api_url"] == self.catalog_integration.get_internal_api_url() + ) # pylint: disable=protected-access, line-too-long querystring = { - 'page_size': 20, - 'exclude_utm': 1, + "page_size": 20, + "exclude_utm": 1, } - assert kwargs['querystring'] == querystring + assert kwargs["querystring"] == querystring return args, kwargs @@ -493,16 +448,16 @@ def test_config_missing(self, mock_get_edx_api_data): assert not mock_get_edx_api_data.called assert data == [] - @mock.patch(UTILS_MODULE + '.logger.error') + @mock.patch(UTILS_MODULE + ".logger.error") def test_service_user_missing(self, mock_log_error, mock_get_edx_api_data): """ Verify that no errors occur when the catalog service user is missing. """ - catalog_integration = self.create_catalog_integration(service_username='nonexistent-user') + catalog_integration = self.create_catalog_integration(service_username="nonexistent-user") data = get_course_runs() mock_log_error.any_call( - 'Catalog service user with username [%s] does not exist. Course runs will not be retrieved.', + "Catalog service user with username [%s] does not exist. Course runs will not be retrieved.", catalog_integration.service_username, ) assert not mock_get_edx_api_data.called @@ -528,17 +483,18 @@ def test_get_course_runs_by_course(self, mock_get_edx_api_data): catalog_course = CourseFactory(course_runs=catalog_course_runs) mock_get_edx_api_data.return_value = catalog_course - data = get_course_runs_for_course(course_uuid=str(catalog_course['uuid'])) + data = get_course_runs_for_course(course_uuid=str(catalog_course["uuid"])) assert mock_get_edx_api_data.called assert data == catalog_course_runs @skip_unless_lms -@mock.patch(UTILS_MODULE + '.get_api_data') +@mock.patch(UTILS_MODULE + ".get_api_data") class TestGetCourseOwners(CatalogIntegrationMixin, TestCase): """ Tests covering retrieval of course runs from the catalog service. """ + def setUp(self): super().setUp() @@ -553,17 +509,18 @@ def test_get_course_owners_by_course(self, mock_get_edx_api_data): catalog_course = CourseFactory(course_runs=catalog_course_runs) mock_get_edx_api_data.return_value = catalog_course - data = get_owners_for_course(course_uuid=str(catalog_course['uuid'])) + data = get_owners_for_course(course_uuid=str(catalog_course["uuid"])) assert mock_get_edx_api_data.called - assert data == catalog_course['owners'] + assert data == catalog_course["owners"] @skip_unless_lms -@mock.patch(UTILS_MODULE + '.get_api_data') +@mock.patch(UTILS_MODULE + ".get_api_data") class TestSessionEntitlement(CatalogIntegrationMixin, TestCase): """ Test Covering data related Entitlements. """ + def setUp(self): super().setUp() @@ -578,12 +535,10 @@ def test_get_visible_sessions_for_entitlement(self, mock_get_edx_api_data): catalog_course_run = CourseRunFactory.create() catalog_course = CourseFactory(course_runs=[catalog_course_run]) mock_get_edx_api_data.return_value = catalog_course - course_key = CourseKey.from_string(catalog_course_run.get('key')) + course_key = CourseKey.from_string(catalog_course_run.get("key")) course_overview = CourseOverviewFactory.create(id=course_key, start=self.tomorrow) CourseModeFactory.create(mode_slug=CourseMode.VERIFIED, min_price=100, course_id=course_overview.id) - course_enrollment = CourseEnrollmentFactory( - user=self.user, course=course_overview, mode=CourseMode.VERIFIED - ) + course_enrollment = CourseEnrollmentFactory(user=self.user, course=course_overview, mode=CourseMode.VERIFIED) entitlement = CourseEntitlementFactory( user=self.user, enrollment_course_run=course_enrollment, mode=CourseMode.VERIFIED ) @@ -598,17 +553,15 @@ def test_get_visible_sessions_for_entitlement_expired_mode(self, mock_get_edx_ap catalog_course_run = CourseRunFactory.create() catalog_course = CourseFactory(course_runs=[catalog_course_run]) mock_get_edx_api_data.return_value = catalog_course - course_key = CourseKey.from_string(catalog_course_run.get('key')) + course_key = CourseKey.from_string(catalog_course_run.get("key")) course_overview = CourseOverviewFactory.create(id=course_key, start=self.tomorrow) CourseModeFactory.create( mode_slug=CourseMode.VERIFIED, min_price=100, course_id=course_overview.id, - expiration_datetime=now() - timedelta(days=1) - ) - course_enrollment = CourseEnrollmentFactory( - user=self.user, course=course_overview, mode=CourseMode.VERIFIED + expiration_datetime=now() - timedelta(days=1), ) + course_enrollment = CourseEnrollmentFactory(user=self.user, course=course_overview, mode=CourseMode.VERIFIED) entitlement = CourseEntitlementFactory( user=self.user, enrollment_course_run=course_enrollment, mode=CourseMode.VERIFIED ) @@ -624,17 +577,15 @@ def test_unpublished_sessions_for_entitlement_when_enrolled(self, mock_get_edx_a catalog_course_run = CourseRunFactory.create(status=COURSE_UNPUBLISHED) catalog_course = CourseFactory(course_runs=[catalog_course_run]) mock_get_edx_api_data.return_value = catalog_course - course_key = CourseKey.from_string(catalog_course_run.get('key')) + course_key = CourseKey.from_string(catalog_course_run.get("key")) course_overview = CourseOverviewFactory.create(id=course_key, start=self.tomorrow) CourseModeFactory.create( mode_slug=CourseMode.VERIFIED, min_price=100, course_id=course_overview.id, - expiration_datetime=now() - timedelta(days=1) - ) - course_enrollment = CourseEnrollmentFactory( - user=self.user, course=course_overview, mode=CourseMode.VERIFIED + expiration_datetime=now() - timedelta(days=1), ) + course_enrollment = CourseEnrollmentFactory(user=self.user, course=course_overview, mode=CourseMode.VERIFIED) entitlement = CourseEntitlementFactory( user=self.user, enrollment_course_run=course_enrollment, mode=CourseMode.VERIFIED ) @@ -650,28 +601,27 @@ def test_unpublished_sessions_for_entitlement(self, mock_get_edx_api_data): catalog_course_run = CourseRunFactory.create(status=COURSE_UNPUBLISHED) catalog_course = CourseFactory(course_runs=[catalog_course_run]) mock_get_edx_api_data.return_value = catalog_course - course_key = CourseKey.from_string(catalog_course_run.get('key')) + course_key = CourseKey.from_string(catalog_course_run.get("key")) course_overview = CourseOverviewFactory.create(id=course_key, start=self.tomorrow) CourseModeFactory.create( mode_slug=CourseMode.VERIFIED, min_price=100, course_id=course_overview.id, - expiration_datetime=now() - timedelta(days=1) - ) - entitlement = CourseEntitlementFactory( - user=self.user, mode=CourseMode.VERIFIED + expiration_datetime=now() - timedelta(days=1), ) + entitlement = CourseEntitlementFactory(user=self.user, mode=CourseMode.VERIFIED) session_entitlements = get_visible_sessions_for_entitlement(entitlement) assert not session_entitlements @skip_unless_lms -@mock.patch(UTILS_MODULE + '.get_api_data') +@mock.patch(UTILS_MODULE + ".get_api_data") class TestGetCourseRunDetails(CatalogIntegrationMixin, TestCase): """ Tests covering retrieval of information about a specific course run from the catalog service. """ + def setUp(self): super().setUp() self.catalog_integration = self.create_catalog_integration(cache_ttl=1) @@ -683,12 +633,12 @@ def test_get_course_run_details(self, mock_get_edx_api_data): """ course_run = CourseRunFactory() course_run_details = { - 'content_language': course_run['content_language'], - 'weeks_to_complete': course_run['weeks_to_complete'], - 'max_effort': course_run['max_effort'] + "content_language": course_run["content_language"], + "weeks_to_complete": course_run["weeks_to_complete"], + "max_effort": course_run["max_effort"], } mock_get_edx_api_data.return_value = course_run_details - data = get_course_run_details(course_run['key'], ['content_language', 'weeks_to_complete', 'max_effort']) + data = get_course_run_details(course_run["key"], ["content_language", "weeks_to_complete", "max_effort"]) assert mock_get_edx_api_data.called assert data == course_run_details @@ -698,87 +648,95 @@ class TestProgramCourseRunCrawling(TestCase): def setUpClass(cls): super().setUpClass() cls.grandchild_1 = { - 'title': 'grandchild 1', - 'curricula': [{'is_active': True, 'courses': [], 'programs': []}], + "title": "grandchild 1", + "curricula": [{"is_active": True, "courses": [], "programs": []}], } cls.grandchild_2 = { - 'title': 'grandchild 2', - 'curricula': [ + "title": "grandchild 2", + "curricula": [ { - 'is_active': True, - 'courses': [{ - 'course_runs': [ - {'key': 'course-run-4'}, - ], - }], - 'programs': [], + "is_active": True, + "courses": [ + { + "course_runs": [ + {"key": "course-run-4"}, + ], + } + ], + "programs": [], }, ], } cls.grandchild_3 = { - 'title': 'grandchild 3', - 'curricula': [{'is_active': False}], + "title": "grandchild 3", + "curricula": [{"is_active": False}], } cls.child_1 = { - 'title': 'child 1', - 'curricula': [{'is_active': True, 'courses': [], 'programs': [cls.grandchild_1]}], + "title": "child 1", + "curricula": [{"is_active": True, "courses": [], "programs": [cls.grandchild_1]}], } cls.child_2 = { - 'title': 'child 2', - 'curricula': [ + "title": "child 2", + "curricula": [ { - 'is_active': True, - 'courses': [{ - 'course_runs': [ - {'key': 'course-run-3'}, - ], - }], - 'programs': [cls.grandchild_2, cls.grandchild_3], + "is_active": True, + "courses": [ + { + "course_runs": [ + {"key": "course-run-3"}, + ], + } + ], + "programs": [cls.grandchild_2, cls.grandchild_3], }, ], } cls.complex_program = { - 'title': 'complex program', - 'curricula': [ + "title": "complex program", + "curricula": [ { - 'is_active': True, - 'courses': [{ - 'course_runs': [ - {'key': 'course-run-2'}, - ], - }], - 'programs': [cls.child_1, cls.child_2], + "is_active": True, + "courses": [ + { + "course_runs": [ + {"key": "course-run-2"}, + ], + } + ], + "programs": [cls.child_1, cls.child_2], }, ], } cls.simple_program = { - 'title': 'simple program', - 'curricula': [ + "title": "simple program", + "curricula": [ { - 'is_active': True, - 'courses': [{ - 'course_runs': [ - {'key': 'course-run-1'}, - ], - }], - 'programs': [cls.grandchild_1] + "is_active": True, + "courses": [ + { + "course_runs": [ + {"key": "course-run-1"}, + ], + } + ], + "programs": [cls.grandchild_1], }, ], } cls.empty_program = { - 'title': 'notice that I have a curriculum, but no programs inside it', - 'curricula': [ + "title": "notice that I have a curriculum, but no programs inside it", + "curricula": [ { - 'is_active': True, - 'courses': [], - 'programs': [], + "is_active": True, + "courses": [], + "programs": [], }, ], } def test_child_programs_no_curriculum(self): program = { - 'title': 'notice that I do not have a curriculum', + "title": "notice that I do not have a curriculum", } assert not child_programs(program) @@ -802,116 +760,104 @@ def test_course_run_keys_for_program_no_courses(self): assert set() == course_run_keys_for_program(self.empty_program) def test_course_run_keys_for_program_one_course(self): - assert {'course-run-1'} == course_run_keys_for_program(self.simple_program) + assert {"course-run-1"} == course_run_keys_for_program(self.simple_program) def test_course_run_keys_for_program_many_courses(self): expected_course_runs = { - 'course-run-2', - 'course-run-3', - 'course-run-4', + "course-run-2", + "course-run-3", + "course-run-4", } assert expected_course_runs == course_run_keys_for_program(self.complex_program) def test_is_course_run_in_program(self): - assert is_course_run_in_program('course-run-4', self.complex_program) - assert not is_course_run_in_program('course-run-5', self.complex_program) - assert not is_course_run_in_program('course-run-4', self.simple_program) + assert is_course_run_in_program("course-run-4", self.complex_program) + assert not is_course_run_in_program("course-run-5", self.complex_program) + assert not is_course_run_in_program("course-run-4", self.simple_program) @skip_unless_lms class TestGetProgramsByType(CacheIsolationTestCase): - """ Test for the ``get_programs_by_type()`` and the ``get_programs_by_type_slug()`` functions. """ - ENABLED_CACHES = ['default'] + """Test for the ``get_programs_by_type()`` and the ``get_programs_by_type_slug()`` functions.""" + + ENABLED_CACHES = ["default"] @classmethod def setUpClass(cls): - """ Sets up program data. """ + """Sets up program data.""" super().setUpClass() cls.site = SiteFactory() cls.other_site = SiteFactory() cls.masters_program_1 = ProgramFactory.create( - type='Masters', - type_attrs=ProgramTypeAttrsFactory.create(slug="masters") + type="Masters", type_attrs=ProgramTypeAttrsFactory.create(slug="masters") ) cls.masters_program_2 = ProgramFactory.create( - type='Masters', - type_attrs=ProgramTypeAttrsFactory.create(slug="masters") + type="Masters", type_attrs=ProgramTypeAttrsFactory.create(slug="masters") ) cls.masters_program_other_site = ProgramFactory.create( - type='Masters', - type_attrs=ProgramTypeAttrsFactory.create(slug="masters") + type="Masters", type_attrs=ProgramTypeAttrsFactory.create(slug="masters") ) cls.bachelors_program = ProgramFactory.create( - type='Bachelors', - type_attrs=ProgramTypeAttrsFactory.create(slug="bachelors") - ) - cls.no_type_program = ProgramFactory.create( - type=None, - type_attrs=None + type="Bachelors", type_attrs=ProgramTypeAttrsFactory.create(slug="bachelors") ) + cls.no_type_program = ProgramFactory.create(type=None, type_attrs=None) def setUp(self): - """ Loads program data into the cache before each test function. """ + """Loads program data into the cache before each test function.""" super().setUp() self.init_cache() def init_cache(self): - """ This function plays the role of the ``cache_programs`` management command. """ + """This function plays the role of the ``cache_programs`` management command.""" all_programs = [ self.masters_program_1, self.masters_program_2, self.bachelors_program, self.no_type_program, - self.masters_program_other_site + self.masters_program_other_site, ] - cached_programs = { - PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']): program for program in all_programs - } + cached_programs = {PROGRAM_CACHE_KEY_TPL.format(uuid=program["uuid"]): program for program in all_programs} cache.set_many(cached_programs, None) programs_by_type = defaultdict(list) programs_by_type_slug = defaultdict(list) for program in all_programs: - program_type = normalize_program_type(program.get('type')) - program_type_slug = (program.get('type_attrs') or {}).get('slug') + program_type = normalize_program_type(program.get("type")) + program_type_slug = (program.get("type_attrs") or {}).get("slug") site_id = self.site.id if program == self.masters_program_other_site: site_id = self.other_site.id - program_type_cache_key = PROGRAMS_BY_TYPE_CACHE_KEY_TPL.format( - site_id=site_id, - program_type=program_type - ) + program_type_cache_key = PROGRAMS_BY_TYPE_CACHE_KEY_TPL.format(site_id=site_id, program_type=program_type) program_type_slug_cache_key = PROGRAMS_BY_TYPE_SLUG_CACHE_KEY_TPL.format( - site_id=site_id, - program_slug=program_type_slug + site_id=site_id, program_slug=program_type_slug ) - programs_by_type[program_type_cache_key].append(program['uuid']) - programs_by_type_slug[program_type_slug_cache_key].append(program['uuid']) + programs_by_type[program_type_cache_key].append(program["uuid"]) + programs_by_type_slug[program_type_slug_cache_key].append(program["uuid"]) cache.set_many(programs_by_type, None) cache.set_many(programs_by_type_slug, None) def test_get_masters_programs(self): expected_programs = [self.masters_program_1, self.masters_program_2] - self.assertCountEqual(expected_programs, get_programs_by_type(self.site, 'masters')) - self.assertCountEqual(expected_programs, get_programs_by_type_slug(self.site, 'masters')) + self.assertCountEqual(expected_programs, get_programs_by_type(self.site, "masters")) + self.assertCountEqual(expected_programs, get_programs_by_type_slug(self.site, "masters")) def test_get_bachelors_programs(self): expected_programs = [self.bachelors_program] - assert expected_programs == get_programs_by_type(self.site, 'bachelors') - assert expected_programs == get_programs_by_type_slug(self.site, 'bachelors') + assert expected_programs == get_programs_by_type(self.site, "bachelors") + assert expected_programs == get_programs_by_type_slug(self.site, "bachelors") def test_get_no_such_type_programs(self): expected_programs = [] - assert expected_programs == get_programs_by_type(self.site, 'doctorate') - assert expected_programs == get_programs_by_type_slug(self.site, 'doctorate') + assert expected_programs == get_programs_by_type(self.site, "doctorate") + assert expected_programs == get_programs_by_type_slug(self.site, "doctorate") def test_get_masters_programs_other_site(self): expected_programs = [self.masters_program_other_site] - assert expected_programs == get_programs_by_type(self.other_site, 'masters') - assert expected_programs == get_programs_by_type_slug(self.other_site, 'masters') + assert expected_programs == get_programs_by_type(self.other_site, "masters") + assert expected_programs == get_programs_by_type_slug(self.other_site, "masters") def test_get_programs_null_type(self): expected_programs = [self.no_type_program] @@ -924,9 +870,9 @@ def test_get_programs_false_type(self): assert expected_programs == get_programs_by_type_slug(self.site, False) def test_normalize_program_type(self): - assert 'none' == normalize_program_type(None) - assert 'false' == normalize_program_type(False) - assert 'true' == normalize_program_type(True) - assert '' == normalize_program_type('') - assert 'masters' == normalize_program_type('Masters') - assert 'masters' == normalize_program_type('masters') + assert "none" == normalize_program_type(None) + assert "false" == normalize_program_type(False) + assert "true" == normalize_program_type(True) + assert "" == normalize_program_type("") + assert "masters" == normalize_program_type("Masters") + assert "masters" == normalize_program_type("masters") diff --git a/openedx/core/djangoapps/catalog/utils.py b/openedx/core/djangoapps/catalog/utils.py index f9b46ee546f7..cc5c202fd4b8 100644 --- a/openedx/core/djangoapps/catalog/utils.py +++ b/openedx/core/djangoapps/catalog/utils.py @@ -1,6 +1,5 @@ """Helper functions for working with the catalog service.""" - import copy import datetime import logging @@ -11,6 +10,7 @@ from django.core.cache import cache from django.core.exceptions import ObjectDoesNotExist from edx_rest_api_client.auth import SuppliedJwtAuth +from edx_rest_api_client.client import USER_AGENT from opaque_keys.edx.keys import CourseKey from pytz import UTC @@ -25,17 +25,15 @@ PROGRAMS_BY_TYPE_CACHE_KEY_TPL, PROGRAMS_BY_TYPE_SLUG_CACHE_KEY_TPL, SITE_PATHWAY_IDS_CACHE_KEY_TPL, - SITE_PROGRAM_UUIDS_CACHE_KEY_TPL + SITE_PROGRAM_UUIDS_CACHE_KEY_TPL, ) from openedx.core.djangoapps.catalog.models import CatalogIntegration from openedx.core.djangoapps.oauth_dispatch.jwt import create_jwt_for_user from openedx.core.lib.edx_api_utils import get_api_data -from edx_rest_api_client.client import USER_AGENT - logger = logging.getLogger(__name__) -missing_details_msg_tpl = 'Failed to get details for program {uuid} from the cache.' +missing_details_msg_tpl = "Failed to get details for program {uuid} from the cache." def get_catalog_api_base_url(site=None): @@ -43,7 +41,7 @@ def get_catalog_api_base_url(site=None): Returns a base API url used to make Catalog API requests. """ if site: - return site.configuration.get_value('COURSE_CATALOG_API_URL') + return site.configuration.get_value("COURSE_CATALOG_API_URL") return CatalogIntegration.current().get_internal_api_url() @@ -54,7 +52,7 @@ def get_catalog_api_client(user): """ jwt = create_jwt_for_user(user) client = requests.Session() - client.headers.update({'User-Agent': USER_AGENT}) + client.headers.update({"User-Agent": USER_AGENT}) client.auth = SuppliedJwtAuth(jwt) return client @@ -82,8 +80,8 @@ def check_catalog_integration_and_get_user(error_message_field): user = catalog_integration.get_service_user() except ObjectDoesNotExist: logger.error( - 'Catalog service user with username [{username}] does not exist. ' - '{field} will not be retrieved.'.format( + "Catalog service user with username [{username}] does not exist. " + "{field} will not be retrieved.".format( username=catalog_integration.service_username, field=error_message_field, ) @@ -92,7 +90,7 @@ def check_catalog_integration_and_get_user(error_message_field): return user, catalog_integration else: logger.error( - 'Unable to retrieve details about {field} because Catalog Integration is not enabled'.format( + "Unable to retrieve details about {field} because Catalog Integration is not enabled".format( field=error_message_field, ) ) @@ -118,7 +116,7 @@ def get_programs(site=None, uuid=None, uuids=None, course=None, catalog_course_u dict, if a specific program is requested. """ if len([arg for arg in (site, uuid, uuids, course, catalog_course_uuid, organization) if arg is not None]) != 1: - raise TypeError('get_programs takes exactly one argument') + raise TypeError("get_programs takes exactly one argument") if uuid: program = cache.get(PROGRAM_CACHE_KEY_TPL.format(uuid=uuid)) @@ -139,12 +137,12 @@ def get_programs(site=None, uuid=None, uuids=None, course=None, catalog_course_u # without programs. After this is changed, log any cache misses here. return [] elif site: - site_config = getattr(site, 'configuration', None) - catalog_url = site_config.get_value('COURSE_CATALOG_API_URL') if site_config else None + site_config = getattr(site, "configuration", None) + catalog_url = site_config.get_value("COURSE_CATALOG_API_URL") if site_config else None if site_config and catalog_url: uuids = cache.get(SITE_PROGRAM_UUIDS_CACHE_KEY_TPL.format(domain=site.domain), []) if not uuids: - logger.warning(f'Failed to get program UUIDs from the cache for site {site.domain}.') + logger.warning(f"Failed to get program UUIDs from the cache for site {site.domain}.") else: uuids = [] elif organization: @@ -169,9 +167,7 @@ def get_programs_by_type(site, program_type): ) uuids = cache.get(program_type_cache_key, []) if not uuids: - logger.warning(str( - f'Failed to get program UUIDs from cache for site {site.id} and type {program_type}' - )) + logger.warning(str(f"Failed to get program UUIDs from cache for site {site.id} and type {program_type}")) return get_programs_by_uuids(uuids) @@ -192,9 +188,9 @@ def get_programs_by_type_slug(site, program_type_slug): ) uuids = cache.get(program_type_slug_cache_key, []) if not uuids: - logger.warning(str( - f'Failed to get program UUIDs from cache for site {site.id} and type slug {program_type_slug}' - )) + logger.warning( + str(f"Failed to get program UUIDs from cache for site {site.id} and type slug {program_type_slug}") + ) return get_programs_by_uuids(uuids) @@ -216,16 +212,14 @@ def get_programs_by_uuids(uuids): # immediately afterwards will succeed in bringing back all the keys. This # behavior can be mitigated by trying again for the missing keys, which is # what we do here. Splitting the get_many into smaller chunks may also help. - missing_uuids = set(uuid_strings) - {program['uuid'] for program in programs} + missing_uuids = set(uuid_strings) - {program["uuid"] for program in programs} if missing_uuids: - logger.info( - f'Failed to get details for {len(missing_uuids)} programs. Retrying.' - ) + logger.info(f"Failed to get details for {len(missing_uuids)} programs. Retrying.") retried_programs = cache.get_many([PROGRAM_CACHE_KEY_TPL.format(uuid=uuid) for uuid in missing_uuids]) programs += list(retried_programs.values()) - still_missing_uuids = set(uuid_strings) - {program['uuid'] for program in programs} + still_missing_uuids = set(uuid_strings) - {program["uuid"] for program in programs} for uuid in still_missing_uuids: logger.warning(missing_details_msg_tpl.format(uuid=uuid)) @@ -242,21 +236,21 @@ def get_program_types(name=None): list of dict, representing program types. dict, if a specific program type is requested. """ - user, catalog_integration = check_catalog_integration_and_get_user(error_message_field='Program types') + user, catalog_integration = check_catalog_integration_and_get_user(error_message_field="Program types") if user: - cache_key = f'{catalog_integration.CACHE_KEY}.program_types' + cache_key = f"{catalog_integration.CACHE_KEY}.program_types" data = get_api_data( catalog_integration, "program_types", api_client=get_catalog_api_client(user), base_api_url=get_catalog_api_base_url(), - cache_key=cache_key if catalog_integration.is_cache_enabled else None + cache_key=cache_key if catalog_integration.is_cache_enabled else None, ) # Filter by name if a name was provided if name: - data = next(program_type for program_type in data if program_type['name'] == name) + data = next(program_type for program_type in data if program_type["name"] == name) return data else: @@ -278,7 +272,7 @@ def get_pathways(site, pathway_id=None): list of dict, representing pathways. dict, if a specific pathway is requested. """ - missing_details_msg_tpl = 'Failed to get details for credit pathway {id} from the cache.' + missing_details_msg_tpl = "Failed to get details for credit pathway {id} from the cache." if pathway_id: pathway = cache.get(PATHWAY_CACHE_KEY_TPL.format(id=pathway_id)) @@ -288,7 +282,7 @@ def get_pathways(site, pathway_id=None): return pathway pathway_ids = cache.get(SITE_PATHWAY_IDS_CACHE_KEY_TPL.format(domain=site.domain), []) if not pathway_ids: - logger.warning('Failed to get credit pathway ids from the cache.') + logger.warning("Failed to get credit pathway ids from the cache.") pathways = cache.get_many([PATHWAY_CACHE_KEY_TPL.format(id=pathway_id) for pathway_id in pathway_ids]) pathways = list(pathways.values()) @@ -301,18 +295,14 @@ def get_pathways(site, pathway_id=None): # immediately afterwards will succeed in bringing back all the keys. This # behavior can be mitigated by trying again for the missing keys, which is # what we do here. Splitting the get_many into smaller chunks may also help. - missing_ids = set(pathway_ids) - {pathway['id'] for pathway in pathways} + missing_ids = set(pathway_ids) - {pathway["id"] for pathway in pathways} if missing_ids: - logger.info( - f'Failed to get details for {len(missing_ids)} pathways. Retrying.' - ) + logger.info(f"Failed to get details for {len(missing_ids)} pathways. Retrying.") - retried_pathways = cache.get_many( - [PATHWAY_CACHE_KEY_TPL.format(id=pathway_id) for pathway_id in missing_ids] - ) + retried_pathways = cache.get_many([PATHWAY_CACHE_KEY_TPL.format(id=pathway_id) for pathway_id in missing_ids]) pathways += list(retried_pathways.values()) - still_missing_ids = set(pathway_ids) - {pathway['id'] for pathway in pathways} + still_missing_ids = set(pathway_ids) - {pathway["id"] for pathway in pathways} for missing_id in still_missing_ids: logger.warning(missing_details_msg_tpl.format(id=missing_id)) @@ -326,9 +316,9 @@ def get_currency_data(): list of dict, representing program types. dict, if a specific program type is requested. """ - user, catalog_integration = check_catalog_integration_and_get_user(error_message_field='Currency data') + user, catalog_integration = check_catalog_integration_and_get_user(error_message_field="Currency data") if user: - cache_key = f'{catalog_integration.CACHE_KEY}.currency' + cache_key = f"{catalog_integration.CACHE_KEY}.currency" return get_api_data( catalog_integration, @@ -336,12 +326,13 @@ def get_currency_data(): api_client=get_catalog_api_client(user), base_api_url=get_catalog_api_base_url(), traverse_pagination=False, - cache_key=cache_key if catalog_integration.is_cache_enabled else None) + cache_key=cache_key if catalog_integration.is_cache_enabled else None, + ) else: return [] -def format_price(price, symbol='$', code='USD'): +def format_price(price, symbol="$", code="USD"): """ Format the price to have the appropriate currency and digits.. @@ -351,8 +342,8 @@ def format_price(price, symbol='$', code='USD'): :return: A formatted price string, i.e. '$10 USD', '$10.52 USD'. """ if int(price) == price: - return f'{symbol}{int(price)} {code}' - return f'{symbol}{price:0.2f} {code}' + return f"{symbol}{int(price)} {code}" + return f"{symbol}{price:0.2f} {code}" def get_localized_price_text(price, request): @@ -362,14 +353,10 @@ def get_localized_price_text(price, request): If the users location has been added to the request, this will return the given price based on conversion rate from the Catalog service and return a localized string otherwise will return the default price in USD """ - user_currency = { - 'symbol': '$', - 'rate': 1, - 'code': 'USD' - } + user_currency = {"symbol": "$", "rate": 1, "code": "USD"} # session.country_code is added via CountryMiddleware in the LMS - user_location = getattr(request, 'session', {}).get('country_code') + user_location = getattr(request, "session", {}).get("country_code") # Override default user_currency if location is available if user_location and get_currency_data: @@ -378,9 +365,7 @@ def get_localized_price_text(price, request): user_currency = currency_data.get(user_country.alpha_3, user_currency) return format_price( - price=(price * user_currency['rate']), - symbol=user_currency['symbol'], - code=user_currency['code'] + price=(price * user_currency["rate"]), symbol=user_currency["symbol"], code=user_currency["code"] ) @@ -404,18 +389,18 @@ def get_programs_with_type(site, include_hidden=True): programs = get_programs(site) if programs: - program_types = {program_type['name']: program_type for program_type in get_program_types()} + program_types = {program_type["name"]: program_type for program_type in get_program_types()} for program in programs: - if program['type'] not in program_types: + if program["type"] not in program_types: continue - if program['hidden'] and not include_hidden: + if program["hidden"] and not include_hidden: continue # deepcopy the program dict here so we are not adding # the type to the cached object program_with_type = copy.deepcopy(program) - program_with_type['type'] = program_types[program['type']] + program_with_type["type"] = program_types[program["type"]] programs_with_type.append(program_with_type) return programs_with_type @@ -429,19 +414,19 @@ def get_course_runs(): list of dict with each record representing a course run. """ course_runs = [] - user, catalog_integration = check_catalog_integration_and_get_user(error_message_field='Course runs') + user, catalog_integration = check_catalog_integration_and_get_user(error_message_field="Course runs") if user: querystring = { - 'page_size': catalog_integration.page_size, - 'exclude_utm': 1, + "page_size": catalog_integration.page_size, + "exclude_utm": 1, } course_runs = get_api_data( catalog_integration, - 'course_runs', + "course_runs", api_client=get_catalog_api_client(user), base_api_url=get_catalog_api_base_url(), - querystring=querystring + querystring=querystring, ) return course_runs @@ -449,22 +434,22 @@ def get_course_runs(): def get_course_runs_for_course(course_uuid): # lint-amnesty, pylint: disable=missing-function-docstring if course_uuid is None: - raise ValueError('missing course_uuid') - user, catalog_integration = check_catalog_integration_and_get_user(error_message_field='Course runs') + raise ValueError("missing course_uuid") + user, catalog_integration = check_catalog_integration_and_get_user(error_message_field="Course runs") if user: cache_key = f"{catalog_integration.CACHE_KEY}.course.{course_uuid}.course_runs" data = get_api_data( catalog_integration, - 'courses', + "courses", resource_id=course_uuid, api_client=get_catalog_api_client(user), base_api_url=get_catalog_api_base_url(), cache_key=cache_key if catalog_integration.is_cache_enabled else None, long_term_cache=True, - many=False + many=False, ) - return data.get('course_runs', []) + return data.get("course_runs", []) else: return [] @@ -479,22 +464,22 @@ def get_owners_for_course(course_uuid): if course_uuid is None: return [] - user, catalog_integration = check_catalog_integration_and_get_user(error_message_field='Owners') + user, catalog_integration = check_catalog_integration_and_get_user(error_message_field="Owners") if user: cache_key = f"{catalog_integration.CACHE_KEY}.course.{course_uuid}.course_runs" data = get_api_data( catalog_integration, - 'courses', + "courses", resource_id=course_uuid, api_client=get_catalog_api_client(user), base_api_url=get_catalog_api_base_url(), cache_key=cache_key if catalog_integration.is_cache_enabled else None, traverse_pagination=False, long_term_cache=True, - many=False + many=False, ) - return data.get('owners', []) + return data.get("owners", []) return [] @@ -513,7 +498,7 @@ def get_course_uuid_for_course(course_run_key): if course_run_key is None: return None - user, catalog_integration = check_catalog_integration_and_get_user(error_message_field='Course UUID') + user, catalog_integration = check_catalog_integration_and_get_user(error_message_field="Course UUID") if user: api_client = get_catalog_api_client(user) base_api_url = get_catalog_api_base_url() @@ -522,7 +507,7 @@ def get_course_uuid_for_course(course_run_key): course_run_data = get_api_data( catalog_integration, - 'course_runs', + "course_runs", resource_id=str(course_run_key), api_client=api_client, base_api_url=base_api_url, @@ -532,14 +517,14 @@ def get_course_uuid_for_course(course_run_key): traverse_pagination=False, ) - course_key_str = course_run_data.get('course', None) + course_key_str = course_run_data.get("course", None) if course_key_str: run_cache_key = f"{catalog_integration.CACHE_KEY}.course.{course_key_str}" data = get_api_data( catalog_integration, - 'courses', + "courses", resource_id=course_key_str, api_client=api_client, base_api_url=base_api_url, @@ -548,7 +533,7 @@ def get_course_uuid_for_course(course_run_key): many=False, traverse_pagination=False, ) - uuid_str = data.get('uuid', None) + uuid_str = data.get("uuid", None) if uuid_str: return uuid.UUID(uuid_str) return None @@ -599,22 +584,23 @@ def get_fulfillable_course_runs_for_entitlement(entitlement, course_runs): # Only retrieve list of published course runs that can still be enrolled and upgraded search_time = datetime.datetime.now(UTC) for course_run in course_runs: - course_id = CourseKey.from_string(course_run.get('key')) + course_id = CourseKey.from_string(course_run.get("key")) (user_enrollment_mode, is_active) = CourseEnrollment.enrollment_mode_for_user( - user=entitlement.user, - course_id=course_id + user=entitlement.user, course_id=course_id ) is_enrolled_in_mode = is_active and (user_enrollment_mode == entitlement.mode) - if (is_enrolled_in_mode and - entitlement.enrollment_course_run and - course_id == entitlement.enrollment_course_run.course_id): + if ( + is_enrolled_in_mode + and entitlement.enrollment_course_run + and course_id == entitlement.enrollment_course_run.course_id + ): # User is enrolled in the course so we should include it in the list of enrollable sessions always # this will ensure it is available for the UI enrollable_sessions.append(course_run) elif not is_enrolled_in_mode and is_course_run_entitlement_fulfillable(course_id, entitlement, search_time): enrollable_sessions.append(course_run) - enrollable_sessions.sort(key=lambda session: session.get('start')) + enrollable_sessions.sort(key=lambda session: session.get("start")) return enrollable_sessions @@ -633,18 +619,21 @@ def get_course_run_details(course_run_key, fields): return course_run_details user, catalog_integration = check_catalog_integration_and_get_user( - error_message_field=f'Data for course_run {course_run_key}' + error_message_field=f"Data for course_run {course_run_key}" ) if user: - cache_key = f'{catalog_integration.CACHE_KEY}.course_runs' + cache_key = f"{catalog_integration.CACHE_KEY}.course_runs" course_run_details = get_api_data( catalog_integration, - 'course_runs', + "course_runs", api_client=get_catalog_api_client(user), base_api_url=get_catalog_api_base_url(), resource_id=course_run_key, - cache_key=cache_key, many=False, traverse_pagination=False, fields=fields + cache_key=cache_key, + many=False, + traverse_pagination=False, + fields=fields, ) return course_run_details @@ -664,10 +653,7 @@ def is_course_run_in_program(course_run_key, program): # walks the structure to collect the set of course run keys, # and then sees if `course_run_key` is in that set. # If we need to optimize this later, we can. - course_run_key_str = ( - str(course_run_key) if isinstance(course_run_key, CourseKey) - else course_run_key - ) + course_run_key_str = str(course_run_key) if isinstance(course_run_key, CourseKey) else course_run_key course_run_keys = course_run_keys_for_program(program) return course_run_key_str in course_run_keys @@ -711,7 +697,7 @@ def child_programs(program): if not curriculum: return [] result = [] - for child in curriculum.get('programs', []): + for child in curriculum.get("programs", []): result.append(child) result.extend(child_programs(child)) return result @@ -722,7 +708,7 @@ def _primary_active_curriculum(program): Returns the first active curriculum in the given program, or None. """ try: - return next(c for c in program.get('curricula', []) if c.get('is_active')) + return next(c for c in program.get("curricula", []) if c.get("is_active")) except StopIteration: return @@ -734,9 +720,7 @@ def _course_runs_from_container(container): a program itself (since either may contain a ``courses`` list). """ return [ - course_run.get('key') - for course in container.get('courses', []) - for course_run in course.get('course_runs', []) + course_run.get("key") for course in container.get("courses", []) for course_run in course.get("course_runs", []) ] @@ -746,14 +730,11 @@ def _courses_from_container(container): which is either the ``curriculum`` field of a program, or a program itself (since either may contain a ``courses`` list). """ - return [ - course.get('uuid') - for course in container.get('courses', []) - ] + return [course.get("uuid") for course in container.get("courses", [])] def normalize_program_type(program_type): - """ Function that normalizes a program type string for use in a cache key. """ + """Function that normalizes a program type string for use in a cache key.""" return str(program_type).lower() @@ -776,15 +757,15 @@ def get_course_data(course_key_str, fields, querystring=None): Returns: dict with details about specified course. """ - user, catalog_integration = check_catalog_integration_and_get_user(error_message_field='Course UUID') + user, catalog_integration = check_catalog_integration_and_get_user(error_message_field="Course UUID") if user: api_client = get_catalog_api_client(user) base_api_url = get_catalog_api_base_url() if course_key_str: - course_cache_key = f'{catalog_integration.CACHE_KEY}.course.{course_key_str}' + course_cache_key = f"{catalog_integration.CACHE_KEY}.course.{course_key_str}" data = get_api_data( catalog_integration, - 'courses', + "courses", resource_id=course_key_str, api_client=api_client, base_api_url=base_api_url, @@ -792,7 +773,7 @@ def get_course_data(course_key_str, fields, querystring=None): long_term_cache=True, many=False, fields=fields, - querystring=querystring + querystring=querystring, ) if data: return data @@ -809,22 +790,22 @@ def get_course_run_data(course_run_id, fields): Returns: dict with details about specified course run. """ - user, catalog_integration = check_catalog_integration_and_get_user(error_message_field='Course Run ID') + user, catalog_integration = check_catalog_integration_and_get_user(error_message_field="Course Run ID") if user: api_client = get_catalog_api_client(user) base_api_url = get_catalog_api_base_url() if course_run_id: - course_run_cache_key = f'{catalog_integration.CACHE_KEY}.course_run.{course_run_id}' + course_run_cache_key = f"{catalog_integration.CACHE_KEY}.course_run.{course_run_id}" data = get_api_data( catalog_integration, - 'course_runs', + "course_runs", resource_id=course_run_id, api_client=api_client, base_api_url=base_api_url, cache_key=course_run_cache_key if catalog_integration.is_cache_enabled else None, long_term_cache=True, many=False, - fields=fields + fields=fields, ) if data: return data diff --git a/openedx/core/djangoapps/credentials/tests/test_utils.py b/openedx/core/djangoapps/credentials/tests/test_utils.py index 2e24dba1a536..59bf3d86cda7 100644 --- a/openedx/core/djangoapps/credentials/tests/test_utils.py +++ b/openedx/core/djangoapps/credentials/tests/test_utils.py @@ -1,4 +1,5 @@ """Tests covering Credentials utilities.""" + import uuid from unittest import mock @@ -13,19 +14,19 @@ from openedx.core.djangoapps.credentials.utils import ( get_courses_completion_status, get_credentials, - get_credentials_records_url + get_credentials_records_url, ) from openedx.core.djangoapps.oauth_dispatch.tests.factories import ApplicationFactory from openedx.core.djangolib.testing.utils import CacheIsolationTestCase, skip_unless_lms -UTILS_MODULE = 'openedx.core.djangoapps.credentials.utils' +UTILS_MODULE = "openedx.core.djangoapps.credentials.utils" @skip_unless_lms class TestGetCredentials(CredentialsApiConfigMixin, CacheIsolationTestCase): - """ Tests for credentials utility functions. """ + """Tests for credentials utility functions.""" - ENABLED_CACHES = ['default'] + ENABLED_CACHES = ["default"] def setUp(self): super().setUp() @@ -35,7 +36,7 @@ def setUp(self): self.credentials_config = self.create_credentials_config(cache_ttl=1) self.user = UserFactory() - @mock.patch(UTILS_MODULE + '.get_api_data') + @mock.patch(UTILS_MODULE + ".get_api_data") def test_get_many(self, mock_get_edx_api_data): expected = factories.UserCredential.create_batch(3) mock_get_edx_api_data.return_value = expected @@ -47,17 +48,17 @@ def test_get_many(self, mock_get_edx_api_data): __, __, kwargs = call querystring = { - 'username': self.user.username, - 'status': 'awarded', - 'only_visible': 'True', + "username": self.user.username, + "status": "awarded", + "only_visible": "True", } - cache_key = f'{self.credentials_config.CACHE_KEY}.{self.user.username}' - assert kwargs['querystring'] == querystring - assert kwargs['cache_key'] == cache_key + cache_key = f"{self.credentials_config.CACHE_KEY}.{self.user.username}" + assert kwargs["querystring"] == querystring + assert kwargs["cache_key"] == cache_key assert actual == expected - @mock.patch(UTILS_MODULE + '.get_api_data') + @mock.patch(UTILS_MODULE + ".get_api_data") def test_get_one(self, mock_get_edx_api_data): expected = factories.UserCredential() mock_get_edx_api_data.return_value = expected @@ -70,32 +71,32 @@ def test_get_one(self, mock_get_edx_api_data): __, __, kwargs = call querystring = { - 'username': self.user.username, - 'status': 'awarded', - 'only_visible': 'True', - 'program_uuid': program_uuid, + "username": self.user.username, + "status": "awarded", + "only_visible": "True", + "program_uuid": program_uuid, } - cache_key = f'{self.credentials_config.CACHE_KEY}.{self.user.username}.{program_uuid}' - assert kwargs['querystring'] == querystring - assert kwargs['cache_key'] == cache_key + cache_key = f"{self.credentials_config.CACHE_KEY}.{self.user.username}.{program_uuid}" + assert kwargs["querystring"] == querystring + assert kwargs["cache_key"] == cache_key assert actual == expected - @mock.patch(UTILS_MODULE + '.get_api_data') + @mock.patch(UTILS_MODULE + ".get_api_data") def test_type_filter(self, mock_get_edx_api_data): - get_credentials(self.user, credential_type='program') + get_credentials(self.user, credential_type="program") mock_get_edx_api_data.assert_called_once() call = mock_get_edx_api_data.mock_calls[0] __, __, kwargs = call querystring = { - 'username': self.user.username, - 'status': 'awarded', - 'only_visible': 'True', - 'type': 'program', + "username": self.user.username, + "status": "awarded", + "only_visible": "True", + "type": "program", } - assert kwargs['querystring'] == querystring + assert kwargs["querystring"] == querystring def test_get_credentials_records_url(self): """ @@ -107,30 +108,32 @@ def test_get_credentials_records_url(self): result = get_credentials_records_url("abcdefgh-ijkl-mnop-qrst-uvwxyz123456") assert result == "https://credentials.example.com/records/programs/abcdefghijklmnopqrstuvwxyz123456" - @mock.patch('requests.Response.raise_for_status') - @mock.patch('requests.Response.json') - @mock.patch(UTILS_MODULE + '.get_credentials_api_client') + @mock.patch("requests.Response.raise_for_status") + @mock.patch("requests.Response.json") + @mock.patch(UTILS_MODULE + ".get_credentials_api_client") def test_get_courses_completion_status(self, mock_get_api_client, mock_json, mock_raise): """ Test to verify the functionality of get_courses_completion_status """ UserFactory.create(username=settings.CREDENTIALS_SERVICE_USERNAME) course_statuses = factories.UserCredentialsCourseRunStatus.create_batch(3) - response_data = [course_status['course_run']['key'] for course_status in course_statuses] + response_data = [course_status["course_run"]["key"] for course_status in course_statuses] mock_raise.return_value = None - mock_json.return_value = {'lms_user_id': self.user.id, - 'status': course_statuses, - 'username': self.user.username} + mock_json.return_value = { + "lms_user_id": self.user.id, + "status": course_statuses, + "username": self.user.username, + } mock_get_api_client.return_value.post.return_value = Response() - course_run_keys = [course_status['course_run']['key'] for course_status in course_statuses] + course_run_keys = [course_status["course_run"]["key"] for course_status in course_statuses] api_response, is_exception = get_courses_completion_status(self.user.id, course_run_keys) assert api_response == response_data assert is_exception is False - @mock.patch('requests.Response.raise_for_status') + @mock.patch("requests.Response.raise_for_status") def test_get_courses_completion_status_api_error(self, mock_raise): - mock_raise.return_value = HTTPError('An Error occured') + mock_raise.return_value = HTTPError("An Error occured") UserFactory.create(username=settings.CREDENTIALS_SERVICE_USERNAME) - api_response, is_exception = get_courses_completion_status(self.user.id, ['fake1', 'fake2', 'fake3']) + api_response, is_exception = get_courses_completion_status(self.user.id, ["fake1", "fake2", "fake3"]) assert api_response == [] assert is_exception is True diff --git a/openedx/core/djangoapps/credentials/utils.py b/openedx/core/djangoapps/credentials/utils.py index 437f7c8c95f0..33f4fc33e93e 100644 --- a/openedx/core/djangoapps/credentials/utils.py +++ b/openedx/core/djangoapps/credentials/utils.py @@ -1,4 +1,5 @@ """Helper functions for working with Credentials.""" + import logging from typing import Dict, List from urllib.parse import urljoin @@ -104,9 +105,7 @@ def get_credentials( # Bypass caching for staff users, who may be generating credentials and # want to see them displayed immediately. use_cache = credential_configuration.is_cache_enabled and not user.is_staff - cache_key = ( - f"{credential_configuration.CACHE_KEY}.{user.username}" if use_cache else None - ) + cache_key = f"{credential_configuration.CACHE_KEY}.{user.username}" if use_cache else None if cache_key and program_uuid: cache_key = f"{cache_key}.{program_uuid}" @@ -139,14 +138,9 @@ def get_courses_completion_status(username, course_run_ids): log.warning("%s configuration is disabled.", credential_configuration.API_NAME) return [], False - completion_status_url = ( - f"{settings.CREDENTIALS_INTERNAL_SERVICE_URL}/api" - "/credentials/v1/learner_cert_status/" - ) + completion_status_url = f"{settings.CREDENTIALS_INTERNAL_SERVICE_URL}/api" "/credentials/v1/learner_cert_status/" try: - api_client = get_credentials_api_client( - User.objects.get(username=settings.CREDENTIALS_SERVICE_USERNAME) - ) + api_client = get_credentials_api_client(User.objects.get(username=settings.CREDENTIALS_SERVICE_USERNAME)) api_response = api_client.post( completion_status_url, json={ diff --git a/openedx/core/djangoapps/programs/utils.py b/openedx/core/djangoapps/programs/utils.py index c9858a7bd27c..03d767364aa0 100644 --- a/openedx/core/djangoapps/programs/utils.py +++ b/openedx/core/djangoapps/programs/utils.py @@ -1,6 +1,5 @@ """Helper functions for working with Programs.""" - import datetime import logging from collections import defaultdict @@ -36,7 +35,7 @@ from openedx.core.djangoapps.catalog.utils import ( get_fulfillable_course_runs_for_entitlement, get_pathways, - get_programs + get_programs, ) from openedx.core.djangoapps.commerce.utils import get_ecommerce_api_base_url, get_ecommerce_api_client from openedx.core.djangoapps.content.course_overviews.models import CourseOverview @@ -69,16 +68,17 @@ def get_buy_subscription_url(program_uuid, skus): """ Returns the URL to the Subscription Purchase page for the given program UUID and course Skus. """ - formatted_skus = urlencode({'sku': skus}, doseq=True) - url = f'{settings.SUBSCRIPTIONS_BUY_SUBSCRIPTION_URL}{program_uuid}/?{formatted_skus}' + formatted_skus = urlencode({"sku": skus}, doseq=True) + url = f"{settings.SUBSCRIPTIONS_BUY_SUBSCRIPTION_URL}{program_uuid}/?{formatted_skus}" return url def get_program_urls(program_data): """Returns important urls of program.""" from lms.djangoapps.learner_dashboard.utils import FAKE_COURSE_KEY, strip_course_id - program_uuid = program_data.get('uuid') - skus = program_data.get('skus') + + program_uuid = program_data.get("uuid") + skus = program_data.get("skus") ecommerce_service = EcommerceService() # TODO: Don't have business logic of course-certificate==record-available here in LMS. @@ -87,17 +87,15 @@ def get_program_urls(program_data): # a waffle flag. This feature is in active developoment. program_record_url = get_credentials_records_url(program_uuid=program_uuid) urls = { - 'program_listing_url': reverse('program_listing_view'), - 'track_selection_url': strip_course_id( - reverse('course_modes_choose', kwargs={'course_id': FAKE_COURSE_KEY}) - ), - 'commerce_api_url': reverse('commerce_api:v0:baskets:create'), - 'buy_button_url': ecommerce_service.get_checkout_page_url(*skus), - 'program_record_url': program_record_url, - 'buy_subscription_url': get_buy_subscription_url(program_uuid, skus), - 'manage_subscription_url': settings.SUBSCRIPTIONS_MANAGE_SUBSCRIPTION_URL, - 'orders_and_subscriptions_url': settings.ORDER_HISTORY_MICROFRONTEND_URL, - 'subscriptions_learner_help_center_url': settings.SUBSCRIPTIONS_LEARNER_HELP_CENTER_URL + "program_listing_url": reverse("program_listing_view"), + "track_selection_url": strip_course_id(reverse("course_modes_choose", kwargs={"course_id": FAKE_COURSE_KEY})), + "commerce_api_url": reverse("commerce_api:v0:baskets:create"), + "buy_button_url": ecommerce_service.get_checkout_page_url(*skus), + "program_record_url": program_record_url, + "buy_subscription_url": get_buy_subscription_url(program_uuid, skus), + "manage_subscription_url": settings.SUBSCRIPTIONS_MANAGE_SUBSCRIPTION_URL, + "orders_and_subscriptions_url": settings.ORDER_HISTORY_MICROFRONTEND_URL, + "subscriptions_learner_help_center_url": settings.SUBSCRIPTIONS_LEARNER_HELP_CENTER_URL, } return urls @@ -107,12 +105,12 @@ def get_industry_and_credit_pathways(program_data, site): industry_pathways = [] credit_pathways = [] try: - for pathway_id in program_data['pathway_ids']: + for pathway_id in program_data["pathway_ids"]: pathway = get_pathways(site, pathway_id) - if pathway and pathway['email']: - if pathway['pathway_type'] == PathwayType.CREDIT.value: + if pathway and pathway["email"]: + if pathway["pathway_type"] == PathwayType.CREDIT.value: credit_pathways.append(pathway) - elif pathway['pathway_type'] == PathwayType.INDUSTRY.value: + elif pathway["pathway_type"] == PathwayType.INDUSTRY.value: industry_pathways.append(pathway) # if pathway caching did not complete fully (no pathway_ids) except KeyError: @@ -124,9 +122,9 @@ def get_industry_and_credit_pathways(program_data, site): def get_program_marketing_url(programs_config, mobile_only=False): """Build a URL used to link to programs on the marketing site.""" if mobile_only: - marketing_url = 'edxapp://course?programs' + marketing_url = "edxapp://course?programs" else: - marketing_url = urljoin(settings.MKTG_URLS.get('ROOT'), programs_config.marketing_path).rstrip('/') + marketing_url = urljoin(settings.MKTG_URLS.get("ROOT"), programs_config.marketing_path).rstrip("/") return marketing_url @@ -135,8 +133,8 @@ def get_program_subscriptions_marketing_url(): """Build a URL used to link to subscription eligible programs on the marketing site.""" marketing_urls = settings.MKTG_URLS return urljoin( - marketing_urls.get('ROOT'), - marketing_urls.get('PROGRAM_SUBSCRIPTIONS'), + marketing_urls.get("ROOT"), + marketing_urls.get("PROGRAM_SUBSCRIPTIONS"), ) @@ -153,13 +151,13 @@ def attach_program_detail_url(programs, mobile_only=False): """ for program in programs: if mobile_only: - detail_fragment_url = reverse('program_details_fragment_view', kwargs={'program_uuid': program['uuid']}) - path_id = detail_fragment_url.replace('/dashboard/', '') - detail_url = f'edxapp://enrolled_program_info?path_id={path_id}' + detail_fragment_url = reverse("program_details_fragment_view", kwargs={"program_uuid": program["uuid"]}) + path_id = detail_fragment_url.replace("/dashboard/", "") + detail_url = f"edxapp://enrolled_program_info?path_id={path_id}" else: - detail_url = reverse('program_details_view', kwargs={'program_uuid': program['uuid']}) + detail_url = reverse("program_details_view", kwargs={"program_uuid": program["uuid"]}) - program['detail_url'] = detail_url + program["detail_url"] = detail_url return programs @@ -220,14 +218,14 @@ def invert_programs(self): inverted_programs = defaultdict(list) for program in self.programs: - for course in program['courses']: - course_uuid = course['uuid'] + for course in program["courses"]: + course_uuid = course["uuid"] if course_uuid in self.course_uuids: program_list = inverted_programs[course_uuid] if program not in program_list: program_list.append(program) - for course_run in course['course_runs']: - course_run_id = course_run['key'] + for course_run in course["course_runs"]: + course_run_id = course_run["key"] if course_run_id in self.course_run_ids: program_list = inverted_programs[course_run_id] if program not in program_list: @@ -235,7 +233,7 @@ def invert_programs(self): # Sort programs by title for consistent presentation. for program_list in inverted_programs.values(): - program_list.sort(key=lambda p: p['title']) + program_list.sort(key=lambda p: p["title"]) return inverted_programs @@ -280,25 +278,22 @@ def _is_course_in_progress(self, now, course): Returns: bool, indicating whether the course is in progress. """ - enrolled_runs = [run for run in course['course_runs'] if run['key'] in self.course_run_ids] + enrolled_runs = [run for run in course["course_runs"] if run["key"] in self.course_run_ids] # Check if the user is enrolled in a required run and mode/seat. - runs_with_required_mode = [ - run for run in enrolled_runs - if run['type'] == self.enrolled_run_modes[run['key']] - ] + runs_with_required_mode = [run for run in enrolled_runs if run["type"] == self.enrolled_run_modes[run["key"]]] if runs_with_required_mode: - not_failed_runs = [run for run in runs_with_required_mode if run['key'] not in self.failed_course_runs] + not_failed_runs = [run for run in runs_with_required_mode if run["key"] not in self.failed_course_runs] if not_failed_runs: return True # Check if seats required for course completion are still available. upgrade_deadlines = [] for run in enrolled_runs: - for seat in run['seats']: - if seat['type'] == run['type'] and run['type'] != self.enrolled_run_modes[run['key']]: - upgrade_deadlines.append(seat['upgrade_deadline']) + for seat in run["seats"]: + if seat["type"] == run["type"] and run["type"] != self.enrolled_run_modes[run["key"]]: + upgrade_deadlines.append(seat["upgrade_deadline"]) # An upgrade deadline of None means the course is always upgradeable. return any(not deadline or deadline and parse(deadline) > now for deadline in upgrade_deadlines) @@ -326,24 +321,21 @@ def progress(self, programs=None, count_only=True): program_copy = deepcopy(program) completed, in_progress, not_started = [], [], [] - for course in program_copy['courses']: + for course in program_copy["courses"]: active_entitlement = CourseEntitlement.get_entitlement_if_active( - user=self.user, - course_uuid=course['uuid'] + user=self.user, course_uuid=course["uuid"] ) if self._is_course_complete(course): completed.append(course) elif self._is_course_enrolled(course) or active_entitlement: # Show all currently enrolled courses and active entitlements as in progress if active_entitlement: - course['course_runs'] = get_fulfillable_course_runs_for_entitlement( - active_entitlement, - course['course_runs'] + course["course_runs"] = get_fulfillable_course_runs_for_entitlement( + active_entitlement, course["course_runs"] ) - course['user_entitlement'] = active_entitlement.to_dict() - course['enroll_url'] = reverse( - 'entitlements_api:v1:enrollments', - args=[str(active_entitlement.uuid)] + course["user_entitlement"] = active_entitlement.to_dict() + course["enroll_url"] = reverse( + "entitlements_api:v1:enrollments", args=[str(active_entitlement.uuid)] ) in_progress.append(course) else: @@ -351,20 +343,20 @@ def progress(self, programs=None, count_only=True): if course_in_progress: in_progress.append(course) else: - course['expired'] = not course_in_progress + course["expired"] = not course_in_progress not_started.append(course) else: not_started.append(course) - progress.append({ - 'uuid': program_copy['uuid'], - 'completed': len(completed) if count_only else completed, - 'in_progress': len(in_progress) if count_only else in_progress, - 'not_started': len(not_started) if count_only else not_started, - 'all_unenrolled': all( - not self._is_course_enrolled(course) for course in program_copy['courses'] - ), - }) + progress.append( + { + "uuid": program_copy["uuid"], + "completed": len(completed) if count_only else completed, + "in_progress": len(in_progress) if count_only else in_progress, + "not_started": len(not_started) if count_only else not_started, + "all_unenrolled": all(not self._is_course_enrolled(course) for course in program_copy["courses"]), + } + ) return progress @@ -383,7 +375,7 @@ def completed_programs_with_available_dates(self): for program in self.programs: available_date = self._available_date_for_program(program, certificates_by_run) if available_date: - completed[program['uuid']] = available_date + completed[program["uuid"]] = available_date return completed def _available_date_for_program(self, program_data, certificates): @@ -397,11 +389,11 @@ def _available_date_for_program(self, program_data, certificates): Returns a datetime object or None if the program is not complete. """ program_available_date = None - for course in program_data['courses']: + for course in program_data["courses"]: earliest_course_run_date = None - for course_run in course['course_runs']: - key = CourseKey.from_string(course_run['key']) + for course_run in course["course_runs"]: + key = CourseKey.from_string(course_run["key"]) # Get a certificate if one exists certificate = certificates.get(key) @@ -409,20 +401,15 @@ def _available_date_for_program(self, program_data, certificates): continue # Modes must match (see _is_course_complete() comments for why) - course_run_mode = self._course_run_mode_translation(course_run['type']) + course_run_mode = self._course_run_mode_translation(course_run["type"]) certificate_mode = self._certificate_mode_translation(certificate.mode) modes_match = course_run_mode == certificate_mode # Grab the available date and keep it if it's the earliest one for this catalog course. if modes_match and CertificateStatuses.is_passing_status(certificate.status): course_overview = CourseOverview.get_from_id(key) - available_date = certificate_api.available_date_for_certificate( - course_overview, - certificate - ) - earliest_course_run_date = min( - date for date in [available_date, earliest_course_run_date] if date - ) + available_date = certificate_api.available_date_for_certificate(course_overview, certificate) + earliest_course_run_date = min(date for date in [available_date, earliest_course_run_date] if date) # If we're missing a cert for a course, the program isn't completed and we should just bail now if earliest_course_run_date is None: @@ -477,7 +464,7 @@ def reshape(course_run): with course run certificates. """ return { - 'course_run_id': course_run['key'], + "course_run_id": course_run["key"], # A course run's type is assumed to indicate which mode must be # completed in order for the run to count towards program completion. # This supports the same flexible program construction allowed by the @@ -485,10 +472,10 @@ def reshape(course_run): # count towards completion of a course in a program). This may change # in the future to make use of the more rigid set of "applicable seat # types" associated with each program type in the catalog. - 'type': self._course_run_mode_translation(course_run['type']), + "type": self._course_run_mode_translation(course_run["type"]), } - return any(reshape(course_run) in self.completed_course_runs for course_run in course['course_runs']) + return any(reshape(course_run) in self.completed_course_runs for course_run in course["course_runs"]) @cached_property def completed_course_runs(self): @@ -498,7 +485,7 @@ def completed_course_runs(self): Returns: list of dicts, each representing a course run certificate """ - return self.course_runs_with_state['completed'] + return self.course_runs_with_state["completed"] @cached_property def failed_course_runs(self): @@ -508,7 +495,7 @@ def failed_course_runs(self): Returns: list of strings, each a course run ID """ - return [run['course_run_id'] for run in self.course_runs_with_state['failed']] + return [run["course_run_id"] for run in self.course_runs_with_state["failed"]] @cached_property def course_runs_with_state(self): @@ -525,10 +512,10 @@ def course_runs_with_state(self): completed_runs, failed_runs = [], [] for certificate in course_run_certificates: - course_key = certificate['course_key'] + course_key = certificate["course_key"] course_data = { - 'course_run_id': str(course_key), - 'type': self._certificate_mode_translation(certificate['type']), + "course_run_id": str(course_key), + "type": self._certificate_mode_translation(certificate["type"]), } try: @@ -538,15 +525,12 @@ def course_runs_with_state(self): else: may_certify = certificate_api.certificates_viewable_for_course(course_overview) - if ( - CertificateStatuses.is_passing_status(certificate['status']) - and may_certify - ): + if CertificateStatuses.is_passing_status(certificate["status"]) and may_certify: completed_runs.append(course_data) else: failed_runs.append(course_data) - return {'completed': completed_runs, 'failed': failed_runs} + return {"completed": completed_runs, "failed": failed_runs} def _is_course_enrolled(self, course): """Check if a user is enrolled in a course. @@ -560,7 +544,7 @@ def _is_course_enrolled(self, course): Returns: bool, indicating whether the course is in progress. """ - return any(course_run['key'] in self.course_run_ids for course_run in course['course_runs']) + return any(course_run["key"] in self.course_run_ids for course_run in course["course_runs"]) # pylint: disable=missing-docstring @@ -578,7 +562,7 @@ def __init__(self, program_data, user, mobile_only=False): self.data = program_data self.user = user self.mobile_only = mobile_only - self.data.update({'is_mobile_only': self.mobile_only}) + self.data.update({"is_mobile_only": self.mobile_only}) self.course_run_key = None self.course_overview = None @@ -586,7 +570,7 @@ def __init__(self, program_data, user, mobile_only=False): def extend(self): """Execute extension handlers, returning the extended data.""" - self._execute('_extend') + self._execute("_extend") self._collect_one_click_purchase_eligibility_data() return self.data @@ -601,51 +585,55 @@ def _handlers(cls, prefix): def _extend_course_runs(self): """Execute course run data handlers.""" - for course in self.data['courses']: - for course_run in course['course_runs']: + for course in self.data["courses"]: + for course_run in course["course_runs"]: # State to be shared across handlers. - self.course_run_key = CourseKey.from_string(course_run['key']) + self.course_run_key = CourseKey.from_string(course_run["key"]) # Some (old) course runs may exist for a program which do not exist in LMS. In that case, # continue without the course run. try: self.course_overview = CourseOverview.get_from_id(self.course_run_key) except CourseOverview.DoesNotExist: - log.warning('Failed to get course overview for course run key: %s', course_run.get('key')) + log.warning("Failed to get course overview for course run key: %s", course_run.get("key")) else: self.enrollment_start = self.course_overview.enrollment_start or DEFAULT_ENROLLMENT_START_DATE - self._execute('_attach_course_run', course_run) + self._execute("_attach_course_run", course_run) def _attach_course_run_certificate_url(self, run_mode): certificate_data = certificate_api.certificate_downloadable_status(self.user, self.course_run_key) - certificate_uuid = certificate_data.get('uuid') - run_mode['certificate_url'] = certificate_api.get_certificate_url( - user_id=self.user.id, # Providing user_id allows us to fall back to PDF certificates - # if web certificates are not configured for a given course. - course_id=self.course_run_key, - uuid=certificate_uuid, - ) if certificate_uuid else None + certificate_uuid = certificate_data.get("uuid") + run_mode["certificate_url"] = ( + certificate_api.get_certificate_url( + user_id=self.user.id, # Providing user_id allows us to fall back to PDF certificates + # if web certificates are not configured for a given course. + course_id=self.course_run_key, + uuid=certificate_uuid, + ) + if certificate_uuid + else None + ) def _attach_course_run_course_url(self, run_mode): if self.mobile_only: - run_mode['course_url'] = 'edxapp://enrolled_course_info?course_id={}'.format(run_mode.get('key')) + run_mode["course_url"] = "edxapp://enrolled_course_info?course_id={}".format(run_mode.get("key")) else: - run_mode['course_url'] = reverse('course_root', args=[self.course_run_key]) + run_mode["course_url"] = reverse("course_root", args=[self.course_run_key]) def _attach_course_run_enrollment_open_date(self, run_mode): - run_mode['enrollment_open_date'] = strftime_localized(self.enrollment_start, 'SHORT_DATE') + run_mode["enrollment_open_date"] = strftime_localized(self.enrollment_start, "SHORT_DATE") def _attach_course_run_is_course_ended(self, run_mode): end_date = self.course_overview.end or datetime.datetime.max.replace(tzinfo=utc) - run_mode['is_course_ended'] = end_date < datetime.datetime.now(utc) + run_mode["is_course_ended"] = end_date < datetime.datetime.now(utc) def _attach_course_run_is_enrolled(self, run_mode): - run_mode['is_enrolled'] = CourseEnrollment.is_enrolled(self.user, self.course_run_key) + run_mode["is_enrolled"] = CourseEnrollment.is_enrolled(self.user, self.course_run_key) def _attach_course_run_is_enrollment_open(self, run_mode): enrollment_end = self.course_overview.enrollment_end or datetime.datetime.max.replace(tzinfo=utc) - run_mode['is_enrollment_open'] = self.enrollment_start <= datetime.datetime.now(utc) < enrollment_end + run_mode["is_enrollment_open"] = self.enrollment_start <= datetime.datetime.now(utc) < enrollment_end def _attach_course_run_advertised_start(self, run_mode): """ @@ -654,10 +642,10 @@ def _attach_course_run_advertised_start(self, run_mode): to start on December 1, 2016, the author might provide 'Winter 2016' as the advertised start. """ - run_mode['advertised_start'] = self.course_overview.advertised_start + run_mode["advertised_start"] = self.course_overview.advertised_start def _attach_course_run_upgrade_url(self, run_mode): - required_mode_slug = run_mode['type'] + required_mode_slug = run_mode["type"] enrolled_mode_slug, _ = CourseEnrollment.enrollment_mode_for_user(self.user, self.course_run_key) is_mode_mismatch = required_mode_slug != enrolled_mode_slug is_upgrade_required = is_mode_mismatch and CourseEnrollment.is_enrolled(self.user, self.course_run_key) @@ -666,19 +654,19 @@ def _attach_course_run_upgrade_url(self, run_mode): # Requires that the ecommerce service be in use. required_mode = CourseMode.mode_for_course(self.course_run_key, required_mode_slug) ecommerce = EcommerceService() - sku = getattr(required_mode, 'sku', None) + sku = getattr(required_mode, "sku", None) if ecommerce.is_enabled(self.user) and sku: - run_mode['upgrade_url'] = ecommerce.get_checkout_page_url(required_mode.sku) + run_mode["upgrade_url"] = ecommerce.get_checkout_page_url(required_mode.sku) else: - run_mode['upgrade_url'] = None + run_mode["upgrade_url"] = None else: - run_mode['upgrade_url'] = None + run_mode["upgrade_url"] = None def _attach_course_run_may_certify(self, run_mode): - run_mode['may_certify'] = certificate_api.certificates_viewable_for_course(self.course_overview) + run_mode["may_certify"] = certificate_api.certificates_viewable_for_course(self.course_overview) def _attach_course_run_is_mobile_only(self, run_mode): - run_mode['is_mobile_only'] = self.mobile_only + run_mode["is_mobile_only"] = self.mobile_only def _filter_out_courses_with_entitlements(self, courses): """ @@ -694,17 +682,17 @@ def _filter_out_courses_with_entitlements(self, courses): Returns: A subset of the given list of course dicts """ - course_uuids = {course['uuid'] for course in courses} + course_uuids = {course["uuid"] for course in courses} # Filter the entitlements' modes with a case-insensitive match against applicable seat_types entitlements = self.user.courseentitlement_set.filter( - mode__in=self.data['applicable_seat_types'], + mode__in=self.data["applicable_seat_types"], course_uuid__in=course_uuids, ) # Here we check the entitlements' expired_at_datetime property rather than filter by the expired_at attribute # to ensure that the expiration status is as up to date as possible entitlements = [e for e in entitlements if not e.expired_at_datetime] courses_with_entitlements = {str(entitlement.course_uuid) for entitlement in entitlements} - return [course for course in courses if course['uuid'] not in courses_with_entitlements] + return [course for course in courses if course["uuid"] not in courses_with_entitlements] def _filter_out_courses_with_enrollments(self, courses): """ @@ -717,14 +705,11 @@ def _filter_out_courses_with_enrollments(self, courses): Returns: A subset of the given list of course dicts """ - enrollments = self.user.courseenrollment_set.filter( - is_active=True, - mode__in=self.data['applicable_seat_types'] - ) + enrollments = self.user.courseenrollment_set.filter(is_active=True, mode__in=self.data["applicable_seat_types"]) course_runs_with_enrollments = {str(enrollment.course_id) for enrollment in enrollments} courses_without_enrollments = [] for course in courses: - if all(str(run['key']) not in course_runs_with_enrollments for run in course['course_runs']): + if all(str(run["key"]) not in course_runs_with_enrollments for run in course["course_runs"]): courses_without_enrollments.append(course) return courses_without_enrollments @@ -734,40 +719,40 @@ def _collect_one_click_purchase_eligibility_data(self): # lint-amnesty, pylint: Extend the program data with data about learner's eligibility for one click purchase, discount data of the program and SKUs of seats that should be added to basket. """ - if 'professional' in self.data['applicable_seat_types']: - self.data['applicable_seat_types'].append('no-id-professional') - applicable_seat_types = {seat for seat in self.data['applicable_seat_types'] if seat != 'credit'} + if "professional" in self.data["applicable_seat_types"]: + self.data["applicable_seat_types"].append("no-id-professional") + applicable_seat_types = {seat for seat in self.data["applicable_seat_types"] if seat != "credit"} - is_learner_eligible_for_one_click_purchase = self.data['is_program_eligible_for_one_click_purchase'] - bundle_uuid = self.data.get('uuid') + is_learner_eligible_for_one_click_purchase = self.data["is_program_eligible_for_one_click_purchase"] + bundle_uuid = self.data.get("uuid") skus = [] - bundle_variant = 'full' + bundle_variant = "full" if is_learner_eligible_for_one_click_purchase: # lint-amnesty, pylint: disable=too-many-nested-blocks - courses = self.data['courses'] + courses = self.data["courses"] if not self.user.is_anonymous: courses = self._filter_out_courses_with_enrollments(courses) courses = self._filter_out_courses_with_entitlements(courses) - if len(courses) < len(self.data['courses']): - bundle_variant = 'partial' + if len(courses) < len(self.data["courses"]): + bundle_variant = "partial" for course in courses: entitlement_product = False - for entitlement in course.get('entitlements', []): + for entitlement in course.get("entitlements", []): # We add the first entitlement product found with an applicable seat type because, at this time, # we are assuming that, for any given course, there is at most one paid entitlement available. - if entitlement['mode'] in applicable_seat_types: - skus.append(entitlement['sku']) + if entitlement["mode"] in applicable_seat_types: + skus.append(entitlement["sku"]) entitlement_product = True break if not entitlement_product: - course_runs = course.get('course_runs', []) - published_course_runs = [run for run in course_runs if run['status'] == 'published'] + course_runs = course.get("course_runs", []) + published_course_runs = [run for run in course_runs if run["status"] == "published"] if len(published_course_runs) == 1: - for seat in published_course_runs[0]['seats']: - if seat['type'] in applicable_seat_types and seat['sku']: - skus.append(seat['sku']) + for seat in published_course_runs[0]["seats"]: + if seat["type"] in applicable_seat_types and seat["sku"]: + skus.append(seat["sku"]) break else: # If a course in the program has more than 1 published course run @@ -798,36 +783,36 @@ def _collect_one_click_purchase_eligibility_data(self): # lint-amnesty, pylint: params = dict(sku=skus, is_anonymous=True) else: if bundle_uuid: - params = dict( - sku=skus, username=self.user.username, bundle=bundle_uuid - ) + params = dict(sku=skus, username=self.user.username, bundle=bundle_uuid) else: params = dict(sku=skus, username=self.user.username) response = api_client.get(api_url, params=params) response.raise_for_status() discount_data = response.json() - program_discounted_price = discount_data['total_incl_tax'] - program_full_price = discount_data['total_incl_tax_excl_discounts'] - discount_data['is_discounted'] = program_discounted_price < program_full_price - discount_data['discount_value'] = program_full_price - program_discounted_price - - self.data.update({ - 'discount_data': discount_data, - 'full_program_price': discount_data['total_incl_tax'], - 'variant': bundle_variant - }) + program_discounted_price = discount_data["total_incl_tax"] + program_full_price = discount_data["total_incl_tax_excl_discounts"] + discount_data["is_discounted"] = program_discounted_price < program_full_price + discount_data["discount_value"] = program_full_price - program_discounted_price + + self.data.update( + { + "discount_data": discount_data, + "full_program_price": discount_data["total_incl_tax"], + "variant": bundle_variant, + } + ) except RequestException: - log.exception('Failed to get discount price for following product SKUs: %s ', ', '.join(skus)) - self.data.update({ - 'discount_data': {'is_discounted': False} - }) + log.exception("Failed to get discount price for following product SKUs: %s ", ", ".join(skus)) + self.data.update({"discount_data": {"is_discounted": False}}) else: is_learner_eligible_for_one_click_purchase = False - self.data.update({ - 'is_learner_eligible_for_one_click_purchase': is_learner_eligible_for_one_click_purchase, - 'skus': skus, - }) + self.data.update( + { + "is_learner_eligible_for_one_click_purchase": is_learner_eligible_for_one_click_purchase, + "skus": skus, + } + ) def get_certificates(user, extended_program): @@ -846,42 +831,43 @@ def get_certificates(user, extended_program): """ certificates = [] - for course in extended_program['courses']: - for course_run in course['course_runs']: - url = course_run.get('certificate_url') - if url and course_run.get('may_certify'): - certificates.append({ - 'type': 'course', - 'title': course_run['title'], - 'url': url, - }) + for course in extended_program["courses"]: + for course_run in course["course_runs"]: + url = course_run.get("certificate_url") + if url and course_run.get("may_certify"): + certificates.append( + { + "type": "course", + "title": course_run["title"], + "url": url, + } + ) # We only want one certificate per course to be returned. break - program_credentials = get_credentials(user, program_uuid=extended_program['uuid'], credential_type='program') + program_credentials = get_credentials(user, program_uuid=extended_program["uuid"], credential_type="program") # only include a program certificate if a certificate is available for every course - if program_credentials and (len(certificates) == len(extended_program['courses'])): - enabled_force_program_cert_auth = configuration_helpers.get_value( - 'force_program_cert_auth', - True - ) - cert_url = program_credentials[0]['certificate_url'] + if program_credentials and (len(certificates) == len(extended_program["courses"])): + enabled_force_program_cert_auth = configuration_helpers.get_value("force_program_cert_auth", True) + cert_url = program_credentials[0]["certificate_url"] url = get_logged_in_program_certificate_url(cert_url) if enabled_force_program_cert_auth else cert_url - certificates.append({ - 'type': 'program', - 'title': extended_program['title'], - 'url': url, - }) + certificates.append( + { + "type": "program", + "title": extended_program["title"], + "url": url, + } + ) return certificates def get_logged_in_program_certificate_url(certificate_url): parsed_url = urlparse(certificate_url) - query_string = 'next=' + parsed_url.path - url_parts = (parsed_url.scheme, parsed_url.netloc, '/login/', '', query_string, '') + query_string = "next=" + parsed_url.path + url_parts = (parsed_url.scheme, parsed_url.netloc, "/login/", "", query_string, "") return urlunparse(url_parts) @@ -903,49 +889,44 @@ def __init__(self, program_data, user): self.instructors = [] # Values for programs' price calculation. - self.data['avg_price_per_course'] = 0.0 - self.data['number_of_courses'] = 0 - self.data['full_program_price'] = 0.0 + self.data["avg_price_per_course"] = 0.0 + self.data["number_of_courses"] = 0 + self.data["full_program_price"] = 0.0 def _extend_program(self): """Aggregates data from the program data structure.""" - cache_key = 'program.instructors.{uuid}'.format( - uuid=self.data['uuid'] - ) + cache_key = "program.instructors.{uuid}".format(uuid=self.data["uuid"]) program_instructors = cache.get(cache_key) - for course in self.data['courses']: - self._execute('_collect_course', course) + for course in self.data["courses"]: + self._execute("_collect_course", course) if not program_instructors: - for course_run in course['course_runs']: - self._execute('_collect_instructors', course_run) + for course_run in course["course_runs"]: + self._execute("_collect_instructors", course_run) if not program_instructors: # We cache the program instructors list to avoid repeated modulestore queries program_instructors = self.instructors cache.set(cache_key, program_instructors, 3600) - if 'instructor_ordering' not in self.data: + if "instructor_ordering" not in self.data: # If no instructor ordering is set in discovery, it doesn't populate this key - self.data['instructor_ordering'] = [] + self.data["instructor_ordering"] = [] sorted_instructor_names = [ - ' '.join([name for name in (instructor['given_name'], instructor['family_name']) if name]) - for instructor in self.data['instructor_ordering'] + " ".join([name for name in (instructor["given_name"], instructor["family_name"]) if name]) + for instructor in self.data["instructor_ordering"] ] instructors_to_be_sorted = [ - instructor for instructor in program_instructors - if instructor['name'] in sorted_instructor_names + instructor for instructor in program_instructors if instructor["name"] in sorted_instructor_names ] instructors_to_not_be_sorted = [ - instructor for instructor in program_instructors - if instructor['name'] not in sorted_instructor_names + instructor for instructor in program_instructors if instructor["name"] not in sorted_instructor_names ] sorted_instructors = sorted( - instructors_to_be_sorted, - key=lambda item: sorted_instructor_names.index(item['name']) + instructors_to_be_sorted, key=lambda item: sorted_instructor_names.index(item["name"]) ) - self.data['instructors'] = sorted_instructors + instructors_to_not_be_sorted + self.data["instructors"] = sorted_instructors + instructors_to_not_be_sorted def extend(self): """Execute extension handlers, returning the extended data.""" @@ -961,7 +942,7 @@ def _handlers(cls, prefix): return {name for name in chain(cls.__dict__, ProgramDataExtender.__dict__) if name.startswith(prefix)} def _attach_course_run_can_enroll(self, run_mode): - run_mode['can_enroll'] = bool(self.user.has_perm(ENROLL_IN_COURSE, self.course_overview)) + run_mode["can_enroll"] = bool(self.user.has_perm(ENROLL_IN_COURSE, self.course_overview)) def _attach_course_run_certificate_url(self, run_mode): """ @@ -977,16 +958,16 @@ def _attach_course_run_upgrade_url(self, run_mode): if not self.user.is_anonymous: super()._attach_course_run_upgrade_url(run_mode) else: - run_mode['upgrade_url'] = None + run_mode["upgrade_url"] = None def _collect_course_pricing(self, course): - self.data['number_of_courses'] += 1 - course_runs = course['course_runs'] + self.data["number_of_courses"] += 1 + course_runs = course["course_runs"] if course_runs: - seats = course_runs[0]['seats'] + seats = course_runs[0]["seats"] if seats: - self.data['full_program_price'] += float(seats[0]['price']) - self.data['avg_price_per_course'] = self.data['full_program_price'] / self.data['number_of_courses'] + self.data["full_program_price"] += float(seats[0]["price"]) + self.data["avg_price_per_course"] = self.data["full_program_price"] / self.data["number_of_courses"] def _collect_instructors(self, course_run): """ @@ -996,19 +977,21 @@ def _collect_instructors(self, course_run): instructor data into the catalog, retrieve it via the catalog API, and remove this code. """ module_store = modulestore() - course_run_key = CourseKey.from_string(course_run['key']) + course_run_key = CourseKey.from_string(course_run["key"]) course_block = module_store.get_course(course_run_key) if course_block: - course_instructors = getattr(course_block, 'instructor_info', {}) + course_instructors = getattr(course_block, "instructor_info", {}) # Deduplicate program instructors using instructor name - curr_instructors_names = [instructor.get('name', '').strip() for instructor in self.instructors] - for instructor in course_instructors.get('instructors', []): - if instructor.get('name', '').strip() not in curr_instructors_names: + curr_instructors_names = [instructor.get("name", "").strip() for instructor in self.instructors] + for instructor in course_instructors.get("instructors", []): + if instructor.get("name", "").strip() not in curr_instructors_names: self.instructors.append(instructor) -def is_user_enrolled_in_program_type(user, program_type_slug, paid_modes_only=False, enrollments=None, entitlements=None): # lint-amnesty, pylint: disable=line-too-long +def is_user_enrolled_in_program_type( + user, program_type_slug, paid_modes_only=False, enrollments=None, entitlements=None +): # lint-amnesty, pylint: disable=line-too-long """ This method will look at the learners Enrollments and Entitlements to determine if a learner is enrolled in a Program of the given type. @@ -1036,10 +1019,10 @@ def is_user_enrolled_in_program_type(user, program_type_slug, paid_modes_only=Fa return False for program in programs: - for course in program.get('courses', []): - course_uuids.add(course.get('uuid')) - for course_run in course.get('course_runs', []): - course_runs.add(course_run['key']) + for course in program.get("courses", []): + course_uuids.add(course.get("uuid")) + for course_run in course.get("course_runs", []): + course_runs.add(course_run["key"]) # Check Entitlements first, because there will be less Course Entitlements than # Course Run Enrollments. @@ -1050,11 +1033,11 @@ def is_user_enrolled_in_program_type(user, program_type_slug, paid_modes_only=Fa student_enrollments = enrollments if enrollments is not None else get_enrollments(user.username) for enrollment in student_enrollments: - course_run_id = enrollment['course_details']['course_id'] + course_run_id = enrollment["course_details"]["course_id"] if paid_modes_only: course_run_key = CourseKey.from_string(course_run_id) paid_modes = [mode.slug for mode in get_paid_modes_for_course(course_run_key)] - if enrollment['mode'] in paid_modes and course_run_id in course_runs: + if enrollment["mode"] in paid_modes and course_run_id in course_runs: return True elif course_run_id in course_runs: return True @@ -1065,11 +1048,7 @@ def get_subscription_api_client(user): """ Returns an API client which can be used to make Subscriptions API requests. """ - scopes = [ - 'user_id', - 'email', - 'profile' - ] + scopes = ["user_id", "email", "profile"] jwt = create_jwt_for_user(user, scopes=scopes) client = requests.Session() client.auth = SuppliedJwtAuth(jwt) @@ -1086,24 +1065,28 @@ def get_programs_subscription_data(user, program_uuid=None): api_path = f"{settings.SUBSCRIPTIONS_API_PATH}" subscription_data = [] - log.info(f"B2C_SUBSCRIPTIONS: Requesting Program subscription data for user: {user}" + - (f" for program_uuid: {program_uuid}" if program_uuid is not None else "")) + log.info( + f"B2C_SUBSCRIPTIONS: Requesting Program subscription data for user: {user}" + + (f" for program_uuid: {program_uuid}" if program_uuid is not None else "") + ) try: if program_uuid: - response = client.get(api_path, params={'resource_id': program_uuid, 'most_active_and_recent': 'true'}) + response = client.get(api_path, params={"resource_id": program_uuid, "most_active_and_recent": "true"}) response.raise_for_status() - subscription_data = response.json().get('results', []) + subscription_data = response.json().get("results", []) else: next_page = 1 while next_page: response = client.get(api_path, params=dict(page=next_page)) response.raise_for_status() - subscription_data.extend(response.json().get('results', [])) - next_page = response.json().get('next') + subscription_data.extend(response.json().get("results", [])) + next_page = response.json().get("next") except Exception as exc: # pylint: disable=broad-except log.exception( f"B2C_SUBSCRIPTIONS: Failed to retrieve Program Subscription Data for user: {user} with error: {exc}" - + f" for program_uuid: {str(program_uuid)}" if program_uuid is not None else "" + + f" for program_uuid: {str(program_uuid)}" + if program_uuid is not None + else "" ) return subscription_data