diff --git a/.github/workflows/mysql8-migrations.yml b/.github/workflows/mysql8-migrations.yml index cb8598b0a..76da2ace5 100644 --- a/.github/workflows/mysql8-migrations.yml +++ b/.github/workflows/mysql8-migrations.yml @@ -39,7 +39,7 @@ jobs: echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT - name: Cache pip dependencies id: cache-dependencies - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: ${{ steps.pip-cache-dir.outputs.dir }} key: ${{ runner.os }}-pip-${{ hashFiles('requirements/pip_tools.txt') }} diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2666702d9..c0701ae01 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,6 +17,39 @@ Unreleased ---------- * nothing unreleased +[5.6.1] +-------- +* fix: Log all learner transmission records. + +[5.6.0] +-------- +* feat: Adds sorting support to enterprise-customer-members endpoint + +[5.5.2] +-------- +* feat: Add page_size support to enterprise-customer-members endpoint + +[5.5.1] +-------- +* fix: Fixed the query fetching enterprise customer members + +[5.5.0] +------- +* feat: introduce Waffle flag for enabling the Learner Portal BFF API. + +[5.4.2] +-------- +* feat: Added a management command to update the Social Auth UID's for an enterprise. + +[5.4.1] +------- +* fix: The default enrollment ``learner_status`` view now considers intended courses + from which the requested user has unenrolled as no-longer realizable. + +[5.4.0] +------- +* chore: Update python requirements. + [5.3.1] ------- * fix: rely on single constant to define course mode priority order (i.e., ensure all enrollable modes are considered; previously missing honor mode in `enroll_learners_in_courses`). diff --git a/enterprise/__init__.py b/enterprise/__init__.py index f76324143..701028e69 100644 --- a/enterprise/__init__.py +++ b/enterprise/__init__.py @@ -2,4 +2,4 @@ Your project description goes here. """ -__version__ = "5.3.1" +__version__ = "5.6.1" diff --git a/enterprise/api/v1/serializers.py b/enterprise/api/v1/serializers.py index 69488030f..d6ef22c11 100644 --- a/enterprise/api/v1/serializers.py +++ b/enterprise/api/v1/serializers.py @@ -1922,7 +1922,22 @@ def get_role_assignments(self, obj): return None -class EnterpriseMembersSerializer(serializers.Serializer): +class EnterpriseCustomerMembersRequestQuerySerializer(serializers.Serializer): + """ + Serializer for the Enterprise Customer Members endpoint query filter + """ + user_query = serializers.CharField(required=False, max_length=250) + sort_by = serializers.ChoiceField( + choices=[ + ('name', 'name'), + ('joined_org', 'joined_org'), + ], + required=False, + ) + is_reversed = serializers.BooleanField(required=False, default=False) + + +class EnterpriseMembersSerializer(serializers.ModelSerializer): """ Serializer for EnterpriseCustomerUser model with additions. """ @@ -1954,6 +1969,7 @@ def get_enterprise_customer_user(self, obj): """ if user := obj: return { + "user_id": user[0], "email": user[1], "joined_org": user[2].strftime("%b %d, %Y"), "name": user[3], diff --git a/enterprise/api/v1/views/default_enterprise_enrollments.py b/enterprise/api/v1/views/default_enterprise_enrollments.py index f1fcb6ab2..aa1716527 100644 --- a/enterprise/api/v1/views/default_enterprise_enrollments.py +++ b/enterprise/api/v1/views/default_enterprise_enrollments.py @@ -211,6 +211,17 @@ def _partition_default_enrollment_intentions_by_enrollment_status( already_enrolled.append((intention, non_audit_enrollment)) continue + if non_audit_enrollment and non_audit_enrollment.unenrolled: + # Learner has un-enrolled in non-audit mode for this course run, + # so don't consider this as an enrollable intention. + # Note that that we don't consider the case of an unenrolled *audit* enrollment, + # because default enrollments should be "exactly once" per (user, enterprise), if possible. + # If only an (unenrolled) audit enrollment exists, it means the user likely + # never had a default intention realized for them in the given course, + # so we'd still like to consider it as enrollable. + needs_enrollment_not_enrollable.append((intention, non_audit_enrollment)) + continue + if not intention.is_course_run_enrollable: # Course run is not enrollable needs_enrollment_not_enrollable.append((intention, audit_enrollment)) diff --git a/enterprise/api/v1/views/enterprise_customer_members.py b/enterprise/api/v1/views/enterprise_customer_members.py index ca292aa4c..4a41965cc 100644 --- a/enterprise/api/v1/views/enterprise_customer_members.py +++ b/enterprise/api/v1/views/enterprise_customer_members.py @@ -6,6 +6,7 @@ from rest_framework import permissions, response, status from rest_framework.pagination import PageNumberPagination +from rest_framework.response import Response from django.core.exceptions import ValidationError from django.db import connection @@ -22,6 +23,7 @@ class EnterpriseCustomerMembersPaginator(PageNumberPagination): """Custom paginator for the enterprise customer members.""" page_size = 10 + page_size_query_param = 'page_size' def get_paginated_response(self, data): """Return a paginated style `Response` object for the given output data.""" @@ -62,13 +64,31 @@ class EnterpriseCustomerMembersViewSet(EnterpriseReadOnlyModelViewSet): def get_members(self, request, *args, **kwargs): """ Get all members associated with that enterprise customer + + Request Arguments: + - ``enterprise_uuid`` (URL location, required): The uuid of the enterprise from which learners should be listed. + + Optional query params: + - ``user_query`` (string, optional): Filter the returned members by user name and email with a provided + sub-string + - ``sort_by`` (string, optional): Specify how the returned members should be ordered. Supported sorting values + are `joined_org`, `name`, and `enrollments`. + - ``is_reversed`` (bool, optional): Include to reverse the records in descending order. By default, the results + returned are in ascending order. """ + query_params = self.request.query_params + param_serializers = serializers.EnterpriseCustomerMembersRequestQuerySerializer( + data=query_params + ) + if not param_serializers.is_valid(): + return Response(param_serializers.errors, status=400) enterprise_uuid = kwargs.get("enterprise_uuid", None) # Raw sql is picky about uuid format uuid_no_dashes = str(enterprise_uuid).replace("-", "") users = [] - user_query = self.request.query_params.get("user_query", None) - + user_query = param_serializers.validated_data.get('user_query') + is_reversed = param_serializers.validated_data.get('is_reversed', False) + sort_by = param_serializers.validated_data.get('sort_by') # On logistration, the name field of auth_userprofile is populated, but if it's not # filled in, we check the auth_user model for it's first/last name fields # https://2u-internal.atlassian.net/wiki/spaces/ENGAGE/pages/747143186/Use+of+full+name+in+edX#Data-on-Name-Field @@ -78,7 +98,7 @@ def get_members(self, request, *args, **kwargs): au.id, au.email, au.date_joined, - coalesce(NULLIF(aup.name, ''), (au.first_name || ' ' || au.last_name)) as full_name + coalesce(NULLIF(aup.name, ''), au.username) as full_name FROM enterprise_enterprisecustomeruser ecu INNER JOIN auth_user as au on ecu.user_id = au.id LEFT JOIN auth_userprofile as aup on au.id = aup.user_id @@ -108,6 +128,14 @@ def get_members(self, request, *args, **kwargs): status=status.HTTP_404_NOT_FOUND, ) + if sort_by: + lambda_keys = { + # 3 and 2 are indices in the tuple associated to a user row (uuid, email, joined_org, name) + 'name': lambda t: t[3], + 'joined_org': lambda t: t[2], + } + users = sorted(users, key=lambda_keys.get(sort_by), reverse=is_reversed) + # paginate the queryset users_page = self.paginator.paginate_queryset(users, request, view=self) diff --git a/enterprise/management/commands/update_enterprise_social_auth_uids.py b/enterprise/management/commands/update_enterprise_social_auth_uids.py new file mode 100644 index 000000000..8c40bf67f --- /dev/null +++ b/enterprise/management/commands/update_enterprise_social_auth_uids.py @@ -0,0 +1,165 @@ +""" +Django management command to update the social auth records UID +""" + +import csv +import logging + +from django.core.exceptions import ValidationError +from django.core.management.base import BaseCommand +from django.db import transaction + +try: + from social_django.models import UserSocialAuth +except ImportError: + UserSocialAuth = None + +logger = logging.getLogger(__name__) + + +class CSVUpdateError(Exception): + """Custom exception for CSV update process.""" + pass # pylint: disable=unnecessary-pass + + +class Command(BaseCommand): + """ + Update the enterprise related social auth records UID to the new one. + + Example usage: + ./manage.py lms update_enterprise_social_auth_uids csv_file_path + ./manage.py lms update_enterprise_social_auth_uids csv_file_path --old-prefix="slug:" --new-prefix="slug:x|{}@xyz" + ./manage.py lms update_enterprise_social_auth_uids csv_file_path --no-dry-run + + """ + + help = 'Records update from CSV with console logging' + + def add_arguments(self, parser): + parser.add_argument('csv_file', type=str, help='Path to the CSV file') + parser.add_argument( + '--old_prefix', + type=str, + default=None, + help='Optional old prefix for old UID. If not provided, uses CSV value.' + ) + parser.add_argument( + '--new_prefix', + type=str, + default=None, + help='Optional new prefix for new UID. If not provided, uses CSV value.' + ) + parser.add_argument( + '--no-dry-run', + action='store_false', + dest='dry_run', + default=True, + help='Actually save changes instead of simulating' + ) + + def handle(self, *args, **options): + logger.info("Command has started...") + csv_path = options['csv_file'] + dry_run = options['dry_run'] + old_prefix = options['old_prefix'] + new_prefix = options['new_prefix'] + + total_processed = 0 + total_updated = 0 + total_errors = 0 + + try: + with open(csv_path, 'r') as csvfile: + reader = csv.DictReader(csvfile) + + for row_num, row in enumerate(reader, start=1): + total_processed += 1 + + try: + with transaction.atomic(): + if self.update_record(row, dry_run, old_prefix, new_prefix): + total_updated += 1 + + except Exception as row_error: # pylint: disable=broad-except + total_errors += 1 + error_msg = f"Row {row_num} update failed: {row} - Error: {str(row_error)}" + logger.error(error_msg, exc_info=True) + + summary_msg = ( + f"CSV Update Summary:\n" + f"Total Records Processed: {total_processed}\n" + f"Records Successfully Updated: {total_updated}\n" + f"Errors Encountered: {total_errors}\n" + f"Dry Run Mode: {'Enabled' if dry_run else 'Disabled'}" + ) + logger.info(summary_msg) + except IOError as io_error: + logger.critical(f"File I/O error: {str(io_error)}") + + except Exception as e: # pylint: disable=broad-except + logger.critical(f"Critical error in CSV processing: {str(e)}") + + def update_record(self, row, dry_run=True, old_prefix=None, new_prefix=None): + """ + Update a single record, applying optional prefixes to UIDs if provided. + + Args: + row (dict): CSV row data + dry_run (bool): Whether to simulate or actually save changes + old_prefix (str): Prefix to apply to the old UID + new_prefix (str): Prefix to apply to the new UID + + Returns: + bool: Whether the update was successful + """ + try: + old_uid = row.get('old-uid') + new_uid = row.get('new-uid') + + # Validating that both values are present + if not old_uid or not new_uid: + raise CSVUpdateError("Missing required UID fields") + + # Construct dynamic UIDs + old_uid_with_prefix = f'{old_prefix}{old_uid}' if old_prefix else old_uid + new_uid_with_prefix = ( + new_prefix.format(new_uid) if new_prefix and '{}' in new_prefix + else f"{new_prefix}{new_uid}" if new_prefix + else new_uid + ) + + instance_with_old_uid = UserSocialAuth.objects.filter(uid=old_uid_with_prefix).first() + + if not instance_with_old_uid: + raise CSVUpdateError(f"No record found with old UID {old_uid_with_prefix}") + + instance_with_new_uid = UserSocialAuth.objects.filter(uid=new_uid_with_prefix).first() + if instance_with_new_uid: + log_entry = f"Warning: Existing record with new UID {new_uid_with_prefix} is deleting." + logger.info(log_entry) + if not dry_run: + instance_with_new_uid.delete() + + if not dry_run: + instance_with_old_uid.uid = new_uid_with_prefix + instance_with_old_uid.save() + + log_entry = f"Successfully updated record: Old UID {old_uid_with_prefix} → New UID {new_uid_with_prefix}" + logger.info(log_entry) + + return True + + except ValidationError as ve: + error_msg = f"Validation error: {ve}" + logger.error(error_msg) + raise + + except CSVUpdateError as update_error: + error_msg = f"Update processing error: {update_error}" + logger.error(error_msg) + raise + + except Exception as e: + error_msg = f"Unexpected error during record update: {e}" + logger.error(error_msg, exc_info=True) + raise diff --git a/enterprise/models.py b/enterprise/models.py index a765718c9..0eaa1f889 100644 --- a/enterprise/models.py +++ b/enterprise/models.py @@ -4703,7 +4703,7 @@ def _get_filtered_ecu_ids(self, user_query): with users as ( select ecu.id, au.email, - coalesce(NULLIF(aup.name, ''), (au.first_name || ' ' || au.last_name)) as full_name + coalesce(NULLIF(aup.name, ''), au.username) as full_name from enterprise_enterprisecustomeruser ecu inner join auth_user au on ecu.user_id = au.id left join auth_userprofile aup on au.id = aup.user_id diff --git a/enterprise/toggles.py b/enterprise/toggles.py index 003e5618b..23ff16102 100644 --- a/enterprise/toggles.py +++ b/enterprise/toggles.py @@ -67,6 +67,18 @@ ENTERPRISE_LOG_PREFIX, ) +# .. toggle_name: enterprise.enterprise_learner_bff_enabled +# .. toggle_implementation: WaffleFlag +# .. toggle_default: False +# .. toggle_description: Enables the enterprise learner BFF +# .. toggle_use_cases: open_edx +# .. toggle_creation_date: 2024-12-16 +ENTERPRISE_LEARNER_BFF_ENABLED = WaffleFlag( + f'{ENTERPRISE_NAMESPACE}.learner_bff_enabled', + __name__, + ENTERPRISE_LOG_PREFIX, +) + def top_down_assignment_real_time_lcm(): """ @@ -103,6 +115,13 @@ def enterprise_customer_support_tool(): return ENTERPRISE_CUSTOMER_SUPPORT_TOOL.is_enabled() +def enterprise_learner_bff_enabled(): + """ + Returns whether the enterprise learner BFF is enabled. + """ + return ENTERPRISE_LEARNER_BFF_ENABLED.is_enabled() + + def enterprise_features(): """ Returns a dict of enterprise Waffle-based feature flags. @@ -113,4 +132,5 @@ def enterprise_features(): 'enterprise_groups_v1': enterprise_groups_v1(), 'enterprise_customer_support_tool': enterprise_customer_support_tool(), 'enterprise_groups_v2': enterprise_groups_v2(), + 'enterprise_learner_bff_enabled': enterprise_learner_bff_enabled(), } diff --git a/integrated_channels/moodle/client.py b/integrated_channels/moodle/client.py index cd883e12a..25b6cf544 100644 --- a/integrated_channels/moodle/client.py +++ b/integrated_channels/moodle/client.py @@ -418,19 +418,19 @@ def _wrapped_create_course_completion(self, user_id, payload): headers = response.headers else: headers = None - if not status_code or not text or not headers: - LOGGER.info( - 'Learner Data Transmission' - f'for course={completion_data["courseID"]} with data ' - f'source: {module_name}, ' - f'activityid: {course_module_id}, ' - f'grades[0][studentid]: {moodle_user_id}, ' - f'grades[0][grade]: {completion_data["grade"] * self.enterprise_configuration.grade_scale} ' - f' with response: {response} ' - f'Status Code: {status_code}, ' - f'Text: {text}, ' - f'Headers: {headers}, ' - ) + + LOGGER.info( + 'Learner Data Transmission' + f'for course={completion_data["courseID"]} with data ' + f'source: {module_name}, ' + f'activityid: {course_module_id}, ' + f'grades[0][studentid]: {moodle_user_id}, ' + f'grades[0][grade]: {completion_data["grade"] * self.enterprise_configuration.grade_scale} ' + f' with response: {response} ' + f'Status Code: {status_code}, ' + f'Text: {text}, ' + f'Headers: {headers}, ' + ) return response diff --git a/requirements/celery53.txt b/requirements/celery53.txt index f429cad2c..85fc551b4 100644 --- a/requirements/celery53.txt +++ b/requirements/celery53.txt @@ -1,7 +1,7 @@ amqp==5.3.1 billiard==4.2.1 celery==5.4.0 -click==8.1.7 +click==8.1.8 click-didyoumean==0.3.1 click-repl==0.3.0 kombu==5.4.2 diff --git a/requirements/ci.txt b/requirements/ci.txt index eaa5c1ca4..8fa480206 100644 --- a/requirements/ci.txt +++ b/requirements/ci.txt @@ -18,11 +18,11 @@ pluggy==1.5.0 # via tox py==1.11.0 # via tox -six==1.16.0 +six==1.17.0 # via tox tox==3.28.0 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/constraints.txt # -r requirements/ci.in -virtualenv==20.27.1 +virtualenv==20.28.1 # via tox diff --git a/requirements/common_constraints.txt b/requirements/common_constraints.txt index 84139d660..b5f13fa87 100644 --- a/requirements/common_constraints.txt +++ b/requirements/common_constraints.txt @@ -25,3 +25,7 @@ elasticsearch<7.14.0 # Cause: https://github.com/openedx/edx-lint/issues/458 # This can be unpinned once https://github.com/openedx/edx-lint/issues/459 has been resolved. pip<24.3 + +# Cause: https://github.com/openedx/edx-lint/issues/475 +# This can be unpinned once https://github.com/openedx/edx-lint/issues/476 has been resolved. +urllib3<2.3.0 diff --git a/requirements/dev.txt b/requirements/dev.txt index af722c36a..27890001b 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -8,19 +8,19 @@ accessible-pygments==0.0.5 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/doc.txt # pydata-sphinx-theme -aiohappyeyeballs==2.4.3 +aiohappyeyeballs==2.4.4 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/doc.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test.txt # aiohttp -aiohttp==3.10.10 +aiohttp==3.11.11 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/doc.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test.txt # openai -aiosignal==1.3.1 +aiosignal==1.3.2 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/doc.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt @@ -55,11 +55,11 @@ asn1crypto==1.5.1 # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test.txt # snowflake-connector-python -astroid==3.3.5 +astroid==3.3.8 # via # pylint # pylint-celery -attrs==24.2.0 +attrs==24.3.0 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/doc.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt @@ -72,7 +72,7 @@ babel==2.16.0 # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/doc.txt # pydata-sphinx-theme # sphinx -bcrypt==4.2.0 +bcrypt==4.2.1 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/doc.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt @@ -88,7 +88,7 @@ billiard==4.2.1 # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test.txt # celery -bleach==6.1.0 +bleach==6.2.0 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/doc.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt @@ -101,7 +101,7 @@ celery==5.4.0 # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/doc.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test.txt -certifi==2024.8.30 +certifi==2024.12.14 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/doc.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt @@ -127,7 +127,7 @@ charset-normalizer==2.0.12 # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test.txt # requests # snowflake-connector-python -click==8.1.7 +click==8.1.8 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/doc.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt @@ -161,18 +161,18 @@ click-repl==0.3.0 # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test.txt # celery -code-annotations==1.8.0 +code-annotations==2.1.0 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/doc.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test.txt # edx-lint # edx-toggles -coverage[toml]==7.6.7 +coverage[toml]==7.6.10 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test.txt # pytest-cov -cryptography==43.0.3 +cryptography==44.0.0 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/doc.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt @@ -192,13 +192,13 @@ defusedxml==0.7.1 # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test.txt # djangorestframework-xml -diff-cover==9.2.0 +diff-cover==9.2.1 # via -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test.txt dill==0.3.9 # via pylint distlib==0.3.9 # via virtualenv -django==4.2.16 +django==4.2.17 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/common_constraints.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/doc.txt @@ -223,7 +223,7 @@ django==4.2.16 # edx-toggles # jsonfield # openedx-events -django-cache-memoize==0.2.0 +django-cache-memoize==0.2.1 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/doc.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt @@ -288,7 +288,7 @@ django-simple-history==3.1.1 # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/doc.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test.txt -django-waffle==4.1.0 +django-waffle==4.2.0 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/doc.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt @@ -349,13 +349,13 @@ edx-braze-client==0.2.5 # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/doc.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test.txt -edx-ccx-keys==1.3.0 +edx-ccx-keys==2.0.2 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/doc.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test.txt # openedx-events -edx-django-utils==7.0.0 +edx-django-utils==7.1.0 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/doc.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt @@ -408,12 +408,12 @@ factory-boy==3.3.1 # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/constraints.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/doc.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test.txt -faker==33.0.0 +faker==33.3.1 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/doc.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test.txt # factory-boy -fastavro==1.9.7 +fastavro==1.10.0 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/doc.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt @@ -465,7 +465,7 @@ isort==5.13.2 # via # -r requirements/dev.in # pylint -jinja2==3.1.4 +jinja2==3.1.5 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/doc.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt @@ -520,13 +520,13 @@ multidict==6.1.0 # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test.txt # aiohttp # yarl -newrelic==10.2.0 +newrelic==10.4.0 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/doc.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test.txt # edx-django-utils -nh3==0.2.18 +nh3==0.2.20 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/doc.txt # readme-renderer @@ -546,7 +546,7 @@ openedx-events==9.15.0 # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/doc.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test.txt -packaging==24.1 +packaging==24.2 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/doc.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt @@ -585,14 +585,14 @@ pgpy==0.6.0 # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/doc.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test.txt -pillow==11.0.0 +pillow==11.1.0 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/doc.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test.txt pip-tools==7.4.1 # via -r requirements/dev.in -pkginfo==1.11.2 +pkginfo==1.12.0 # via twine platformdirs==4.3.6 # via @@ -617,13 +617,14 @@ prompt-toolkit==3.0.48 # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test.txt # click-repl -propcache==0.2.0 +propcache==0.2.1 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/doc.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test.txt + # aiohttp # yarl -psutil==6.1.0 +psutil==6.1.1 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/doc.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt @@ -649,13 +650,13 @@ pycparser==2.22 # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test.txt # cffi -pydata-sphinx-theme==0.16.0 +pydata-sphinx-theme==0.16.1 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/doc.txt # sphinx-book-theme pydocstyle==6.3.0 # via -r requirements/dev.in -pygments==2.18.0 +pygments==2.19.1 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/doc.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test.txt @@ -665,7 +666,7 @@ pygments==2.18.0 # pydata-sphinx-theme # readme-renderer # sphinx -pyjwt[crypto]==2.9.0 +pyjwt[crypto]==2.10.1 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/doc.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt @@ -674,7 +675,7 @@ pyjwt[crypto]==2.9.0 # edx-drf-extensions # edx-rest-api-client # snowflake-connector-python -pylint==3.3.1 +pylint==3.3.3 # via # edx-lint # pylint-celery @@ -701,7 +702,7 @@ pynacl==1.5.0 # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test.txt # edx-django-utils # paramiko -pyopenssl==24.2.1 +pyopenssl==24.3.0 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/doc.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt @@ -798,12 +799,11 @@ semantic-version==2.10.0 # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test.txt # edx-drf-extensions -six==1.16.0 +six==1.17.0 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/doc.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test.txt - # bleach # edx-ccx-keys # edx-lint # edx-rbac @@ -822,7 +822,7 @@ snowballstemmer==2.2.0 # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/doc.txt # pydocstyle # sphinx -snowflake-connector-python==3.12.3 +snowflake-connector-python==3.12.4 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/doc.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt @@ -868,13 +868,13 @@ sphinxcontrib-serializinghtml==2.0.0 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/doc.txt # sphinx -sqlparse==0.5.1 +sqlparse==0.5.3 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/doc.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test.txt # django -stevedore==5.3.0 +stevedore==5.4.0 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/doc.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt @@ -911,7 +911,7 @@ tox==3.28.0 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/constraints.txt # -r requirements/dev.in -tqdm==4.66.6 +tqdm==4.67.1 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/doc.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt @@ -951,6 +951,7 @@ uritemplate==4.1.1 # drf-yasg urllib3==2.2.3 # via + # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/common_constraints.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/doc.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test.txt @@ -963,7 +964,7 @@ vine==5.1.0 # amqp # celery # kombu -virtualenv==20.27.1 +virtualenv==20.28.1 # via tox wcwidth==0.2.13 # via @@ -977,11 +978,11 @@ webencodings==0.5.1 # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test.txt # bleach -wheel==0.45.0 +wheel==0.45.1 # via # -r requirements/dev.in # pip-tools -yarl==1.17.0 +yarl==1.18.3 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/doc.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt diff --git a/requirements/django.txt b/requirements/django.txt index 64aaf996f..ebf97308f 100644 --- a/requirements/django.txt +++ b/requirements/django.txt @@ -1 +1 @@ -django==4.2.16 +django==4.2.17 diff --git a/requirements/doc.txt b/requirements/doc.txt index b28df822e..0f2c47f0b 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -6,15 +6,15 @@ # accessible-pygments==0.0.5 # via pydata-sphinx-theme -aiohappyeyeballs==2.4.3 +aiohappyeyeballs==2.4.4 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # aiohttp -aiohttp==3.10.10 +aiohttp==3.11.11 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # openai -aiosignal==1.3.1 +aiosignal==1.3.2 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # aiohttp @@ -37,7 +37,7 @@ asn1crypto==1.5.1 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # snowflake-connector-python -attrs==24.2.0 +attrs==24.3.0 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # aiohttp @@ -47,7 +47,7 @@ babel==2.16.0 # via # pydata-sphinx-theme # sphinx -bcrypt==4.2.0 +bcrypt==4.2.1 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # paramiko @@ -57,13 +57,13 @@ billiard==4.2.1 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # celery -bleach==6.1.0 +bleach==6.2.0 # via -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt celery==5.4.0 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/constraints.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt -certifi==2024.8.30 +certifi==2024.12.14 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # requests @@ -79,7 +79,7 @@ charset-normalizer==2.0.12 # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # requests # snowflake-connector-python -click==8.1.7 +click==8.1.8 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # celery @@ -100,11 +100,11 @@ click-repl==0.3.0 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # celery -code-annotations==1.8.0 +code-annotations==2.1.0 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # edx-toggles -cryptography==43.0.3 +cryptography==44.0.0 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # django-fernet-fields-v2 @@ -118,7 +118,7 @@ defusedxml==0.7.1 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # djangorestframework-xml -django==4.2.16 +django==4.2.17 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/common_constraints.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt @@ -140,7 +140,7 @@ django==4.2.16 # edx-toggles # jsonfield # openedx-events -django-cache-memoize==0.2.0 +django-cache-memoize==0.2.1 # via -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt django-config-models==2.7.0 # via -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt @@ -172,7 +172,7 @@ django-simple-history==3.1.1 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/constraints.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt -django-waffle==4.1.0 +django-waffle==4.2.0 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # edx-django-utils @@ -214,11 +214,11 @@ edx-api-doc-tools==2.0.0 # via -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt edx-braze-client==0.2.5 # via -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt -edx-ccx-keys==1.3.0 +edx-ccx-keys==2.0.2 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # openedx-events -edx-django-utils==7.0.0 +edx-django-utils==7.1.0 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # django-config-models @@ -248,9 +248,9 @@ factory-boy==3.3.1 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/constraints.txt # -r requirements/doc.in -faker==33.0.0 +faker==33.3.1 # via factory-boy -fastavro==1.9.7 +fastavro==1.10.0 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # openedx-events @@ -277,7 +277,7 @@ inflection==0.5.1 # drf-yasg iniconfig==2.0.0 # via pytest -jinja2==3.1.4 +jinja2==3.1.5 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # code-annotations @@ -303,11 +303,11 @@ multidict==6.1.0 # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # aiohttp # yarl -newrelic==10.2.0 +newrelic==10.4.0 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # edx-django-utils -nh3==0.2.18 +nh3==0.2.20 # via readme-renderer oauthlib==3.2.2 # via @@ -317,7 +317,7 @@ openai==0.28.1 # via -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt openedx-events==9.15.0 # via -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt -packaging==24.1 +packaging==24.2 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # drf-yasg @@ -338,7 +338,7 @@ pbr==6.1.0 # stevedore pgpy==0.6.0 # via -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt -pillow==11.0.0 +pillow==11.1.0 # via -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt platformdirs==4.3.6 # via @@ -350,11 +350,12 @@ prompt-toolkit==3.0.48 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # click-repl -propcache==0.2.0 +propcache==0.2.1 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt + # aiohttp # yarl -psutil==6.1.0 +psutil==6.1.1 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # edx-django-utils @@ -368,16 +369,16 @@ pycparser==2.22 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # cffi -pydata-sphinx-theme==0.16.0 +pydata-sphinx-theme==0.16.1 # via sphinx-book-theme -pygments==2.18.0 +pygments==2.19.1 # via # accessible-pygments # doc8 # pydata-sphinx-theme # readme-renderer # sphinx -pyjwt[crypto]==2.9.0 +pyjwt[crypto]==2.10.1 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # drf-jwt @@ -393,7 +394,7 @@ pynacl==1.5.0 # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # edx-django-utils # paramiko -pyopenssl==24.2.1 +pyopenssl==24.3.0 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # snowflake-connector-python @@ -447,10 +448,9 @@ semantic-version==2.10.0 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # edx-drf-extensions -six==1.16.0 +six==1.17.0 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt - # bleach # edx-ccx-keys # edx-rbac # python-dateutil @@ -458,7 +458,7 @@ slumber==0.7.1 # via -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt snowballstemmer==2.2.0 # via sphinx -snowflake-connector-python==3.12.3 +snowflake-connector-python==3.12.4 # via -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt sortedcontainers==2.4.0 # via @@ -485,11 +485,11 @@ sphinxcontrib-qthelp==2.0.0 # via sphinx sphinxcontrib-serializinghtml==2.0.0 # via sphinx -sqlparse==0.5.1 +sqlparse==0.5.3 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # django -stevedore==5.3.0 +stevedore==5.4.0 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # code-annotations @@ -508,7 +508,7 @@ tomlkit==0.13.2 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # snowflake-connector-python -tqdm==4.66.6 +tqdm==4.67.1 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # openai @@ -534,6 +534,7 @@ uritemplate==4.1.1 # drf-yasg urllib3==2.2.3 # via + # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/common_constraints.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # requests vine==5.1.0 @@ -550,7 +551,7 @@ webencodings==0.5.1 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # bleach -yarl==1.17.0 +yarl==1.18.3 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # aiohttp diff --git a/requirements/edx-platform-constraints.txt b/requirements/edx-platform-constraints.txt index a316d03a8..dca06a7d9 100644 --- a/requirements/edx-platform-constraints.txt +++ b/requirements/edx-platform-constraints.txt @@ -8,13 +8,13 @@ # via -r requirements/edx/github.in acid-xblock==0.4.1 # via -r requirements/edx/kernel.in -aiohappyeyeballs==2.4.3 +aiohappyeyeballs==2.4.4 # via aiohttp -aiohttp==3.10.10 +aiohttp==3.11.11 # via # geoip2 # openai -aiosignal==1.3.1 +aiosignal==1.3.2 # via aiohttp algoliasearch==3.0.0 # via @@ -36,7 +36,7 @@ asgiref==3.8.1 # django-countries asn1crypto==1.5.1 # via snowflake-connector-python -attrs==24.2.0 +attrs==24.3.0 # via # -r requirements/edx/kernel.in # aiohttp @@ -53,12 +53,14 @@ babel==2.16.0 # enmerkar-underscore backoff==1.10.0 # via analytics-python -bcrypt==4.2.0 +bcrypt==4.2.1 # via paramiko beautifulsoup4==4.12.3 - # via pynliner + # via + # openedx-forum + # pynliner # via celery -bleach[css]==6.1.0 +bleach[css]==6.2.0 # via # edx-enterprise # lti-consumer-xblock @@ -68,20 +70,20 @@ bleach[css]==6.1.0 # xblock-poll boto==2.49.0 # via -r requirements/edx/kernel.in -boto3==1.35.50 +boto3==1.35.93 # via # -r requirements/edx/kernel.in # django-ses # fs-s3fs # ora2 -botocore==1.35.50 +botocore==1.35.93 # via # -r requirements/edx/kernel.in # boto3 # s3transfer bridgekeeper==0.9 # via -r requirements/edx/kernel.in -cachecontrol==0.14.0 +cachecontrol==0.14.1 # via firebase-admin cachetools==5.5.0 # via google-auth @@ -96,9 +98,8 @@ camel-converter[pydantic]==4.0.1 # edx-enterprise # event-tracking # openedx-learning -certifi==2024.8.30 +certifi==2024.12.14 # via - # -r requirements/edx/paver.txt # elasticsearch # py2neo # requests @@ -113,7 +114,6 @@ chardet==5.2.0 charset-normalizer==2.0.12 # via # -c requirements/edx/../constraints.txt - # -r requirements/edx/paver.txt # requests # snowflake-connector-python chem==1.3.0 @@ -132,7 +132,7 @@ chem==1.3.0 click-plugins==1.1.1 # via celery # via celery -code-annotations==1.8.0 +code-annotations==2.1.0 # via # edx-enterprise # edx-toggles @@ -140,7 +140,7 @@ codejail-includes==1.0.0 # via -r requirements/edx/kernel.in crowdsourcehinter-xblock==0.8 # via -r requirements/edx/bundled.in -cryptography==43.0.3 +cryptography==44.0.0 # via # -r requirements/edx/kernel.in # django-fernet-fields-v2 @@ -162,7 +162,7 @@ defusedxml==0.7.1 # ora2 # python3-openid # social-auth-core -django==4.2.16 +django==4.2.17 # via # -c requirements/edx/../common_constraints.txt # -c requirements/edx/../constraints.txt @@ -228,6 +228,7 @@ django==4.2.16 # openedx-django-wiki # openedx-events # openedx-filters + # openedx-forum # openedx-learning # ora2 # social-auth-app-django @@ -236,7 +237,7 @@ django==4.2.16 # xss-utils django-appconf==1.0.6 # via django-statici18n -django-cache-memoize==0.2.0 +django-cache-memoize==0.2.1 django-celery-results==2.5.1 # via -r requirements/edx/kernel.in django-classy-tags==4.1.0 @@ -247,7 +248,7 @@ django-config-models==2.7.0 # edx-enterprise # edx-name-affirmation # lti-consumer-xblock -django-cors-headers==4.5.0 +django-cors-headers==4.6.0 # via -r requirements/edx/kernel.in django-countries==7.6.1 # via @@ -273,7 +274,7 @@ django-ipware==7.0.1 # -r requirements/edx/kernel.in # edx-enterprise # edx-proctoring -django-js-asset==2.2.0 +django-js-asset==3.0.1 # via django-mptt django-method-override==1.0.4 # via -r requirements/edx/kernel.in @@ -300,7 +301,7 @@ django-mptt==0.16.0 # -r requirements/edx/kernel.in # openedx-django-wiki django-multi-email-field==0.7.0 -django-mysql==4.14.0 +django-mysql==4.15.0 # via -r requirements/edx/kernel.in django-oauth-toolkit==1.7.1 # via @@ -308,7 +309,7 @@ django-oauth-toolkit==1.7.1 # -r requirements/edx/kernel.in # edx-enterprise django-object-actions==4.3.0 -django-pipeline==3.1.0 +django-pipeline==4.0.0 # via -r requirements/edx/kernel.in django-push-notifications==3.1.0 # via edx-ace @@ -318,7 +319,7 @@ django-sekizai==4.1.0 # via # -r requirements/edx/kernel.in # openedx-django-wiki -django-ses==4.2.0 +django-ses==4.3.1 # via -r requirements/edx/bundled.in # via # -c requirements/edx/../constraints.txt @@ -328,12 +329,13 @@ django-ses==4.2.0 # edx-organizations # edx-proctoring # ora2 -django-statici18n==2.5.0 +django-statici18n==2.6.0 # via # -r requirements/edx/kernel.in # lti-consumer-xblock # xblock-drag-and-drop-v2 # xblock-poll + # xblocks-contrib django-storages==1.14.3 # via # -c requirements/edx/../constraints.txt @@ -341,7 +343,7 @@ django-storages==1.14.3 # edxval django-user-tasks==3.2.0 # via -r requirements/edx/kernel.in -django-waffle==4.1.0 +django-waffle==4.2.0 # via # -r requirements/edx/kernel.in # edx-django-utils @@ -371,19 +373,18 @@ djangorestframework==3.14.0 # edx-organizations # edx-proctoring # edx-submissions + # openedx-forum # openedx-learning # ora2 # super-csv djangorestframework-xml==2.0.0 dnspython==2.7.0 - # via - # -r requirements/edx/paver.txt - # pymongo + # via pymongo done-xblock==2.4.0 # via -r requirements/edx/bundled.in drf-jwt==1.19.2 # via edx-drf-extensions -drf-spectacular==0.27.2 +drf-spectacular==0.28.0 # via -r requirements/edx/kernel.in drf-yasg==1.21.8 # via @@ -405,7 +406,7 @@ edx-bulk-grades==1.1.0 # via # -r requirements/edx/kernel.in # staff-graded-xblock -edx-ccx-keys==1.3.0 +edx-ccx-keys==2.0.2 # via # -r requirements/edx/kernel.in # lti-consumer-xblock @@ -415,9 +416,9 @@ edx-celeryutils==1.3.0 # -r requirements/edx/kernel.in # edx-name-affirmation # super-csv -edx-codejail==3.5.1 +edx-codejail==3.5.2 # via -r requirements/edx/kernel.in -edx-completion==4.7.3 +edx-completion==4.7.8 # via -r requirements/edx/kernel.in edx-django-release-util==1.4.0 # via @@ -426,7 +427,7 @@ edx-django-release-util==1.4.0 # edxval edx-django-sites-extensions==4.2.0 # via -r requirements/edx/kernel.in -edx-django-utils==7.0.0 +edx-django-utils==7.1.0 # via # -r requirements/edx/kernel.in # django-config-models @@ -455,7 +456,7 @@ edx-drf-extensions==10.5.0 # edx-when # edxval # openedx-learning -edx-enterprise==4.32.2 +edx-enterprise==5.5.2 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/kernel.in @@ -468,6 +469,7 @@ edx-i18n-tools==1.5.0 # -c requirements/edx/../constraints.txt # -r requirements/edx/bundled.in # ora2 + # xblocks-contrib edx-milestones==0.6.0 # via -r requirements/edx/kernel.in edx-name-affirmation==3.0.1 @@ -475,7 +477,6 @@ edx-name-affirmation==3.0.1 edx-opaque-keys[django]==2.11.0 # via # -r requirements/edx/kernel.in - # -r requirements/edx/paver.txt # edx-bulk-grades # edx-ccx-keys # edx-completion @@ -490,7 +491,7 @@ edx-opaque-keys[django]==2.11.0 # ora2 edx-organizations==6.13.0 # via -r requirements/edx/kernel.in -edx-proctoring==4.18.3 +edx-proctoring==5.0.1 # via # -r requirements/edx/kernel.in # edx-proctoring-proctortrack @@ -501,10 +502,12 @@ edx-rest-api-client==6.0.0 # edx-enterprise # edx-proctoring edx-search==4.1.1 - # via -r requirements/edx/kernel.in + # via + # -r requirements/edx/kernel.in + # openedx-forum edx-sga==0.25.0 # via -r requirements/edx/bundled.in -edx-submissions==3.8.2 +edx-submissions==3.8.4 # via # -r requirements/edx/kernel.in # ora2 @@ -527,12 +530,14 @@ edx-when==2.5.0 # via # -r requirements/edx/kernel.in # edx-proctoring -edxval==2.6.0 +edxval==2.8.0 # via -r requirements/edx/kernel.in elasticsearch==7.9.1 # via # -c requirements/edx/../common_constraints.txt + # -c requirements/edx/../constraints.txt # edx-search + # openedx-forum enmerkar==0.7.1 # via enmerkar-underscore enmerkar-underscore==2.3.1 @@ -544,11 +549,11 @@ event-tracking==3.0.0 # edx-completion # edx-proctoring # edx-search -fastavro==1.9.7 +fastavro==1.10.0 # via openedx-events filelock==3.16.1 # via snowflake-connector-python -firebase-admin==6.5.0 +firebase-admin==6.6.0 # via edx-ace frozenlist==1.5.0 # via @@ -566,20 +571,20 @@ fs-s3fs==0.1.8 # openedx-django-pyfs future==1.0.0 # via pyjwkest -geoip2==4.8.0 +geoip2==4.8.1 # via -r requirements/edx/kernel.in glob2==0.7 # via -r requirements/edx/kernel.in -google-api-core[grpc]==2.22.0 +google-api-core[grpc]==2.24.0 # via # firebase-admin # google-api-python-client # google-cloud-core # google-cloud-firestore # google-cloud-storage -google-api-python-client==2.149.0 +google-api-python-client==2.157.0 # via firebase-admin -google-auth==2.35.0 +google-auth==2.37.0 # via # google-api-core # google-api-python-client @@ -595,7 +600,7 @@ google-cloud-core==2.4.1 # google-cloud-storage google-cloud-firestore==2.19.0 # via firebase-admin -google-cloud-storage==2.18.2 +google-cloud-storage==2.19.0 # via firebase-admin google-crc32c==1.6.0 # via @@ -603,15 +608,15 @@ google-crc32c==1.6.0 # google-resumable-media google-resumable-media==2.7.2 # via google-cloud-storage -googleapis-common-protos==1.65.0 +googleapis-common-protos==1.66.0 # via # google-api-core # grpcio-status -grpcio==1.67.0 +grpcio==1.69.0 # via # google-api-core # grpcio-status -grpcio-status==1.67.0 +grpcio-status==1.69.0 # via google-api-core gunicorn==23.0.0 # via -r requirements/edx/kernel.in @@ -625,11 +630,10 @@ httplib2==0.22.0 # via # google-api-python-client # google-auth-httplib2 -icalendar==6.0.1 +icalendar==6.1.0 # via -r requirements/edx/kernel.in idna==3.10 # via - # -r requirements/edx/paver.txt # optimizely-sdk # requests # snowflake-connector-python @@ -646,7 +650,7 @@ ipaddress==1.0.23 # via -r requirements/edx/kernel.in isodate==0.7.2 # via python3-saml -jinja2==3.1.4 +jinja2==3.1.5 # via code-annotations jmespath==1.0.1 # via @@ -679,19 +683,16 @@ laboratory==1.0.2 # via -r requirements/edx/kernel.in lazy==1.6 # via - # -r requirements/edx/paver.txt # acid-xblock # lti-consumer-xblock # ora2 # xblock -libsass==0.10.0 - # via - # -c requirements/edx/../constraints.txt - # -r requirements/edx/paver.txt loremipsum==1.0.5 # via ora2 -lti-consumer-xblock==9.11.3 - # via -r requirements/edx/kernel.in +lti-consumer-xblock==9.12.1 + # via + # -c requirements/edx/../constraints.txt + # -r requirements/edx/kernel.in lxml[html-clean,html_clean]==5.3.0 # via # -r requirements/edx/kernel.in @@ -705,11 +706,11 @@ lxml[html-clean,html_clean]==5.3.0 # python3-saml # xblock # xmlsec -lxml-html-clean==0.3.1 +lxml-html-clean==0.4.1 # via lxml mailsnake==1.6.4 # via -r requirements/edx/bundled.in -mako==1.3.6 +mako==1.3.8 # via # -r requirements/edx/kernel.in # acid-xblock @@ -725,7 +726,6 @@ markdown==3.3.7 # xblock-poll markupsafe==3.0.2 # via - # -r requirements/edx/paver.txt # chem # jinja2 # mako @@ -733,12 +733,10 @@ markupsafe==3.0.2 # xblock maxminddb==2.6.2 # via geoip2 -meilisearch==0.31.6 +meilisearch==0.33.0 # via # -r requirements/edx/kernel.in # edx-search -mock==5.1.0 - # via -r requirements/edx/paver.txt mongoengine==0.29.1 # via -r requirements/edx/kernel.in monotonic==1.6 @@ -755,11 +753,13 @@ multidict==6.1.0 # via # aiohttp # yarl -mysqlclient==2.2.5 - # via -r requirements/edx/kernel.in -newrelic==10.2.0 +mysqlclient==2.2.6 + # via + # -r requirements/edx/kernel.in + # openedx-forum +newrelic==10.4.0 # via edx-django-utils -nh3==0.2.18 +nh3==0.2.20 # via -r requirements/edx/kernel.in nltk==3.9.1 # via chem @@ -786,13 +786,16 @@ openai==0.28.1 # -c requirements/edx/../constraints.txt # edx-enterprise openedx-atlas==0.6.2 - # via -r requirements/edx/kernel.in -openedx-calc==3.1.2 + # via + # -r requirements/edx/kernel.in + # openedx-forum +openedx-calc==4.0.1 # via -r requirements/edx/kernel.in openedx-django-pyfs==3.7.0 # via # lti-consumer-xblock # xblock + # xblocks-contrib openedx-django-require==2.1.0 # via -r requirements/edx/kernel.in openedx-django-wiki==2.1.0 @@ -806,12 +809,14 @@ openedx-events==9.15.0 # edx-name-affirmation # event-tracking # ora2 -openedx-filters==1.11.0 +openedx-filters==1.12.0 # via # -r requirements/edx/kernel.in # lti-consumer-xblock # ora2 -openedx-learning==0.17.0 +openedx-forum==0.1.5 + # via -r requirements/edx/kernel.in +openedx-learning==0.18.1 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/kernel.in @@ -821,22 +826,21 @@ optimizely-sdk==4.1.1 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/bundled.in -ora2==6.14.0 +ora2==6.14.3 # via -r requirements/edx/bundled.in -packaging==24.1 +packaging==24.2 # via # drf-yasg # gunicorn # py2neo # snowflake-connector-python -pansi==2020.7.3 +pansi==2024.11.0 # via py2neo paramiko==3.5.0 path==16.11.0 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/kernel.in - # -r requirements/edx/paver.txt # edx-i18n-tools # path-py path-py==12.5.0 @@ -844,42 +848,41 @@ path-py==12.5.0 # edx-enterprise # ora2 # staff-graded-xblock -paver==1.3.4 - # via -r requirements/edx/paver.txt pbr==6.1.0 - # via - # -r requirements/edx/paver.txt - # stevedore + # via stevedore pgpy==0.6.0 piexif==1.1.3 # via -r requirements/edx/kernel.in -pillow==11.0.0 +pillow==11.1.0 # via # -r requirements/edx/kernel.in # edx-enterprise # edx-organizations # edxval + # pansi platformdirs==4.3.6 # via snowflake-connector-python polib==1.2.0 # via edx-i18n-tools # via click-repl -propcache==0.2.0 - # via yarl +propcache==0.2.1 + # via + # aiohttp + # yarl proto-plus==1.25.0 # via # google-api-core # google-cloud-firestore -protobuf==5.28.3 +protobuf==5.29.2 # via # google-api-core # google-cloud-firestore # googleapis-common-protos # grpcio-status # proto-plus -psutil==6.1.0 +psutil==6.1.1 # via - # -r requirements/edx/paver.txt + # -r requirements/edx/kernel.in # edx-django-utils py2neo @ https://github.com/overhangio/py2neo/releases/download/2021.2.3/py2neo-2021.2.3.tar.gz # via @@ -902,20 +905,18 @@ pycryptodomex==3.21.0 # edx-proctoring # lti-consumer-xblock # pyjwkest -pydantic==2.9.2 +pydantic==2.10.4 # via camel-converter -pydantic-core==2.23.4 +pydantic-core==2.27.2 # via pydantic -pygments==2.18.0 - # via - # -r requirements/edx/bundled.in - # py2neo +pygments==2.19.1 + # via py2neo pyjwkest==1.4.2 # via # -r requirements/edx/kernel.in # edx-token-utils # lti-consumer-xblock -pyjwt[crypto]==2.9.0 +pyjwt[crypto]==2.10.1 # via # -r requirements/edx/kernel.in # drf-jwt @@ -932,15 +933,15 @@ pylatexenc==2.10 pylti1p3==2.0.0 # via -r requirements/edx/kernel.in pymemcache==4.0.0 - # via -r requirements/edx/paver.txt + # via -r requirements/edx/kernel.in pymongo==4.4.0 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/kernel.in - # -r requirements/edx/paver.txt # edx-opaque-keys # event-tracking # mongoengine + # openedx-forum # openedx-mongodbproxy pynacl==1.5.0 # via @@ -948,11 +949,11 @@ pynacl==1.5.0 # paramiko pynliner==0.8.0 # via -r requirements/edx/kernel.in -pyopenssl==24.2.1 +pyopenssl==24.3.0 # via # optimizely-sdk # snowflake-connector-python -pyparsing==3.2.0 +pyparsing==3.2.1 # via # chem # httplib2 @@ -978,8 +979,6 @@ python-dateutil==2.9.0.post0 # xblock python-ipware==3.0.0 # via django-ipware -python-memcached==1.62 - # via -r requirements/edx/paver.txt python-slugify==8.0.4 # via code-annotations python-swiftclient==4.6.0 @@ -1023,7 +1022,7 @@ random2==1.0.2 # via -r requirements/edx/kernel.in recommender-xblock==3.0.0 # via -r requirements/edx/bundled.in -redis==5.2.0 +redis==5.2.1 # via # -r requirements/edx/kernel.in # walrus @@ -1031,11 +1030,10 @@ referencing==0.35.1 # via # jsonschema # jsonschema-specifications -regex==2024.9.11 +regex==2024.11.6 # via nltk requests==2.32.3 # via - # -r requirements/edx/paver.txt # algoliasearch # analytics-python # cachecontrol @@ -1050,6 +1048,7 @@ requests==2.32.3 # mailsnake # meilisearch # openai + # openedx-forum # optimizely-sdk # pyjwkest # pylti1p3 @@ -1064,7 +1063,7 @@ requests-oauthlib==2.0.0 # via # -r requirements/edx/kernel.in # social-auth-core -rpds-py==0.20.0 +rpds-py==0.22.3 # via # jsonschema # referencing @@ -1076,11 +1075,11 @@ rules==3.5 # edx-enterprise # edx-proctoring # openedx-learning -s3transfer==0.10.3 +s3transfer==0.10.4 # via boto3 sailthru-client==2.2.3 # via edx-ace -scipy==1.14.1 +scipy==1.15.0 # via # chem # openedx-calc @@ -1095,12 +1094,10 @@ simplejson==3.19.3 # super-csv # xblock # xblock-utils -six==1.16.0 +six==1.17.0 # via # -r requirements/edx/kernel.in - # -r requirements/edx/paver.txt # analytics-python - # bleach # codejail-includes # crowdsourcehinter-xblock # edx-ace @@ -1115,10 +1112,7 @@ six==1.16.0 # fs-s3fs # html5lib # interchange - # libsass # optimizely-sdk - # pansi - # paver # py2neo # pyjwkest # python-dateutil @@ -1127,7 +1121,7 @@ slumber==0.7.1 # -r requirements/edx/kernel.in # edx-bulk-grades # edx-enterprise -snowflake-connector-python==3.12.3 +snowflake-connector-python==3.12.4 social-auth-app-django==5.4.1 # via # -c requirements/edx/../constraints.txt @@ -1148,14 +1142,13 @@ sortedcontainers==2.4.0 # snowflake-connector-python soupsieve==2.6 # via beautifulsoup4 -sqlparse==0.5.1 +sqlparse==0.5.3 # via django staff-graded-xblock==2.3.0 # via -r requirements/edx/bundled.in -stevedore==5.3.0 +stevedore==5.4.0 # via # -r requirements/edx/kernel.in - # -r requirements/edx/paver.txt # code-annotations # edx-ace # edx-django-utils @@ -1168,17 +1161,16 @@ sympy==1.13.3 testfixtures==8.3.0 text-unidecode==1.3 # via python-slugify -tinycss2==1.2.1 +tinycss2==1.4.0 # via bleach tomlkit==0.13.2 # via snowflake-connector-python -tqdm==4.66.6 +tqdm==4.67.1 # via # nltk # openai typing-extensions==4.12.2 # via - # -r requirements/edx/paver.txt # django-countries # edx-opaque-keys # jwcrypto @@ -1202,7 +1194,7 @@ uritemplate==4.1.1 # google-api-python-client urllib3==2.2.3 # via - # -r requirements/edx/paver.txt + # -c requirements/edx/../common_constraints.txt # botocore # elasticsearch # py2neo @@ -1217,8 +1209,6 @@ voluptuous==0.15.2 # via ora2 walrus==0.9.4 # via edx-event-bus-redis -watchdog==5.0.3 - # via -r requirements/edx/paver.txt wcwidth==0.2.13 # via prompt-toolkit web-fragments==2.2.0 @@ -1238,8 +1228,10 @@ webob==1.8.9 # via # -r requirements/edx/kernel.in # xblock -wrapt==1.16.0 - # via -r requirements/edx/paver.txt +wheel==0.45.1 + # via django-pipeline +wrapt==1.17.0 + # via -r requirements/edx/kernel.in xblock[django]==5.1.0 # via # -r requirements/edx/kernel.in @@ -1255,6 +1247,7 @@ xblock[django]==5.1.0 # xblock-drag-and-drop-v2 # xblock-google-drive # xblock-utils + # xblocks-contrib xblock-drag-and-drop-v2==4.0.3 # via -r requirements/edx/bundled.in xblock-google-drive==0.7.0 @@ -1265,13 +1258,15 @@ xblock-utils==4.0.0 # via # edx-sga # xblock-poll +xblocks-contrib==0.1.0 + # via -r requirements/edx/bundled.in xmlsec==1.3.14 # via python3-saml xss-utils==0.6.0 # via -r requirements/edx/kernel.in -yarl==1.17.0 +yarl==1.18.3 # via aiohttp -zipp==3.20.2 +zipp==3.21.0 # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: diff --git a/requirements/js_test.txt b/requirements/js_test.txt index 713a041d4..791410659 100644 --- a/requirements/js_test.txt +++ b/requirements/js_test.txt @@ -4,7 +4,7 @@ # # make upgrade # -attrs==24.2.0 +attrs==24.3.0 # via # outcome # trio @@ -12,7 +12,7 @@ autocommand==2.2.2 # via jaraco-text backports-tarfile==1.2.0 # via jaraco-context -certifi==2024.8.30 +certifi==2024.12.14 # via selenium cheroot==10.0.1 # via cherrypy @@ -67,19 +67,19 @@ python-dateutil==2.9.0.post0 # via tempora pyyaml==6.0.2 # via jasmine -selenium==4.26.1 +selenium==4.27.1 # via jasmine -six==1.16.0 +six==1.17.0 # via python-dateutil sniffio==1.3.1 # via trio sortedcontainers==2.4.0 # via trio -tempora==5.7.0 +tempora==5.8.0 # via # -r requirements/js_test.in # portend -trio==0.27.0 +trio==0.28.0 # via # selenium # trio-websocket @@ -88,7 +88,9 @@ trio-websocket==0.11.1 typing-extensions==4.12.2 # via selenium urllib3[socks]==2.2.3 - # via selenium + # via + # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/common_constraints.txt + # selenium websocket-client==1.8.0 # via selenium wsproto==1.2.0 diff --git a/requirements/test-master.txt b/requirements/test-master.txt index e4e911aa4..ede624e45 100644 --- a/requirements/test-master.txt +++ b/requirements/test-master.txt @@ -4,15 +4,15 @@ # # make upgrade # -aiohappyeyeballs==2.4.3 +aiohappyeyeballs==2.4.4 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/edx-platform-constraints.txt # aiohttp -aiohttp==3.10.10 +aiohttp==3.11.11 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/edx-platform-constraints.txt # openai -aiosignal==1.3.1 +aiosignal==1.3.2 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/edx-platform-constraints.txt # aiohttp @@ -31,18 +31,18 @@ asn1crypto==1.5.1 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/edx-platform-constraints.txt # snowflake-connector-python -attrs==24.2.0 +attrs==24.3.0 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/edx-platform-constraints.txt # aiohttp # openedx-events -bcrypt==4.2.0 +bcrypt==4.2.1 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/edx-platform-constraints.txt # paramiko billiard==4.2.1 # via celery -bleach==6.1.0 +bleach==6.2.0 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/edx-platform-constraints.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/base.in @@ -50,7 +50,7 @@ celery==5.4.0 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/constraints.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/base.in -certifi==2024.8.30 +certifi==2024.12.14 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/edx-platform-constraints.txt # requests @@ -66,7 +66,7 @@ charset-normalizer==2.0.12 # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/edx-platform-constraints.txt # requests # snowflake-connector-python -click==8.1.7 +click==8.1.8 # via # celery # click-didyoumean @@ -82,12 +82,12 @@ click-plugins==1.1.1 # celery click-repl==0.3.0 # via celery -code-annotations==1.8.0 +code-annotations==2.1.0 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/edx-platform-constraints.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/base.in # edx-toggles -cryptography==43.0.3 +cryptography==44.0.0 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/edx-platform-constraints.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/base.in @@ -102,7 +102,7 @@ defusedxml==0.7.1 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/edx-platform-constraints.txt # djangorestframework-xml -django==4.2.16 +django==4.2.17 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/common_constraints.txt # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/edx-platform-constraints.txt @@ -125,7 +125,7 @@ django==4.2.16 # edx-toggles # jsonfield # openedx-events -django-cache-memoize==0.2.0 +django-cache-memoize==0.2.1 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/edx-platform-constraints.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/base.in @@ -177,7 +177,7 @@ django-simple-history==3.1.1 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/constraints.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/base.in -django-waffle==4.1.0 +django-waffle==4.2.0 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/edx-platform-constraints.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/base.in @@ -217,11 +217,11 @@ edx-braze-client==0.2.5 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/edx-platform-constraints.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/base.in -edx-ccx-keys==1.3.0 +edx-ccx-keys==2.0.2 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/edx-platform-constraints.txt # openedx-events -edx-django-utils==7.0.0 +edx-django-utils==7.1.0 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/edx-platform-constraints.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/base.in @@ -258,7 +258,7 @@ edx-toggles==5.2.0 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/edx-platform-constraints.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/base.in -fastavro==1.9.7 +fastavro==1.10.0 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/edx-platform-constraints.txt # openedx-events @@ -281,7 +281,7 @@ inflection==0.5.1 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/edx-platform-constraints.txt # drf-yasg -jinja2==3.1.4 +jinja2==3.1.5 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/edx-platform-constraints.txt # code-annotations @@ -308,7 +308,7 @@ multidict==6.1.0 # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/edx-platform-constraints.txt # aiohttp # yarl -newrelic==10.2.0 +newrelic==10.4.0 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/edx-platform-constraints.txt # edx-django-utils @@ -324,7 +324,7 @@ openedx-events==9.15.0 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/edx-platform-constraints.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/base.in -packaging==24.1 +packaging==24.2 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/edx-platform-constraints.txt # drf-yasg @@ -349,7 +349,7 @@ pgpy==0.6.0 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/edx-platform-constraints.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/base.in -pillow==11.0.0 +pillow==11.1.0 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/edx-platform-constraints.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/base.in @@ -359,11 +359,12 @@ platformdirs==4.3.6 # snowflake-connector-python prompt-toolkit==3.0.48 # via click-repl -propcache==0.2.0 +propcache==0.2.1 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/edx-platform-constraints.txt + # aiohttp # yarl -psutil==6.1.0 +psutil==6.1.1 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/edx-platform-constraints.txt # edx-django-utils @@ -375,7 +376,7 @@ pycparser==2.22 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/edx-platform-constraints.txt # cffi -pyjwt[crypto]==2.9.0 +pyjwt[crypto]==2.10.1 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/edx-platform-constraints.txt # drf-jwt @@ -391,7 +392,7 @@ pynacl==1.5.0 # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/edx-platform-constraints.txt # edx-django-utils # paramiko -pyopenssl==24.2.1 +pyopenssl==24.3.0 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/edx-platform-constraints.txt # snowflake-connector-python @@ -440,10 +441,9 @@ semantic-version==2.10.0 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/edx-platform-constraints.txt # edx-drf-extensions -six==1.16.0 +six==1.17.0 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/edx-platform-constraints.txt - # bleach # edx-ccx-keys # edx-rbac # python-dateutil @@ -451,7 +451,7 @@ slumber==0.7.1 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/edx-platform-constraints.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/base.in -snowflake-connector-python==3.12.3 +snowflake-connector-python==3.12.4 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/edx-platform-constraints.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/base.in @@ -459,11 +459,11 @@ sortedcontainers==2.4.0 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/edx-platform-constraints.txt # snowflake-connector-python -sqlparse==0.5.1 +sqlparse==0.5.3 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/edx-platform-constraints.txt # django -stevedore==5.3.0 +stevedore==5.4.0 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/edx-platform-constraints.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/base.in @@ -482,7 +482,7 @@ tomlkit==0.13.2 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/edx-platform-constraints.txt # snowflake-connector-python -tqdm==4.66.6 +tqdm==4.67.1 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/edx-platform-constraints.txt # openai @@ -508,6 +508,7 @@ uritemplate==4.1.1 # drf-yasg urllib3==2.2.3 # via + # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/common_constraints.txt # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/edx-platform-constraints.txt # requests vine==5.1.0 @@ -523,7 +524,7 @@ webencodings==0.5.1 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/edx-platform-constraints.txt # bleach -yarl==1.17.0 +yarl==1.18.3 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/edx-platform-constraints.txt # aiohttp diff --git a/requirements/test.txt b/requirements/test.txt index 494d73ae7..914aa933b 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -4,15 +4,15 @@ # # make upgrade # -aiohappyeyeballs==2.4.3 +aiohappyeyeballs==2.4.4 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # aiohttp -aiohttp==3.10.10 +aiohttp==3.11.11 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # openai -aiosignal==1.3.1 +aiosignal==1.3.2 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # aiohttp @@ -32,25 +32,25 @@ asn1crypto==1.5.1 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # snowflake-connector-python -attrs==24.2.0 +attrs==24.3.0 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # aiohttp # openedx-events # pytest -bcrypt==4.2.0 +bcrypt==4.2.1 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # paramiko # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # celery -bleach==6.1.0 +bleach==6.2.0 # via -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/constraints.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt -certifi==2024.8.30 +certifi==2024.12.14 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # requests @@ -86,13 +86,13 @@ click-plugins==1.1.1 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # celery -code-annotations==1.8.0 +code-annotations==2.1.0 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # edx-toggles -coverage[toml]==7.6.7 +coverage[toml]==7.6.10 # via pytest-cov -cryptography==43.0.3 +cryptography==44.0.0 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # django-fernet-fields-v2 @@ -108,7 +108,7 @@ defusedxml==0.7.1 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # djangorestframework-xml -diff-cover==9.2.0 +diff-cover==9.2.1 # via -r requirements/test.in # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/common_constraints.txt @@ -131,7 +131,7 @@ diff-cover==9.2.0 # edx-toggles # jsonfield # openedx-events -django-cache-memoize==0.2.0 +django-cache-memoize==0.2.1 # via -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt django-config-models==2.7.0 # via -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt @@ -164,7 +164,7 @@ django-simple-history==3.1.1 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/constraints.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt -django-waffle==4.1.0 +django-waffle==4.2.0 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # edx-django-utils @@ -196,11 +196,11 @@ edx-api-doc-tools==2.0.0 # via -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt edx-braze-client==0.2.5 # via -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt -edx-ccx-keys==1.3.0 +edx-ccx-keys==2.0.2 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # openedx-events -edx-django-utils==7.0.0 +edx-django-utils==7.1.0 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # django-config-models @@ -230,9 +230,9 @@ factory-boy==3.3.1 # via # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/constraints.txt # -r requirements/test.in -faker==33.0.0 +faker==33.3.1 # via factory-boy -fastavro==1.9.7 +fastavro==1.10.0 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # openedx-events @@ -261,7 +261,7 @@ inflection==0.5.1 # drf-yasg iniconfig==2.0.0 # via pytest -jinja2==3.1.4 +jinja2==3.1.5 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # code-annotations @@ -290,7 +290,7 @@ multidict==6.1.0 # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # aiohttp # yarl -newrelic==10.2.0 +newrelic==10.4.0 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # edx-django-utils @@ -302,7 +302,7 @@ openai==0.28.1 # via -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt openedx-events==9.15.0 # via -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt -packaging==24.1 +packaging==24.2 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # drf-yasg @@ -322,7 +322,7 @@ pbr==6.1.0 # stevedore pgpy==0.6.0 # via -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt -pillow==11.0.0 +pillow==11.1.0 # via -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt platformdirs==4.3.6 # via @@ -335,11 +335,12 @@ pluggy==1.5.0 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # click-repl -propcache==0.2.0 +propcache==0.2.1 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt + # aiohttp # yarl -psutil==6.1.0 +psutil==6.1.1 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # edx-django-utils @@ -353,9 +354,9 @@ pycparser==2.22 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # cffi -pygments==2.18.0 +pygments==2.19.1 # via diff-cover -pyjwt[crypto]==2.9.0 +pyjwt[crypto]==2.10.1 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # drf-jwt @@ -371,7 +372,7 @@ pynacl==1.5.0 # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # edx-django-utils # paramiko -pyopenssl==24.2.1 +pyopenssl==24.3.0 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # snowflake-connector-python @@ -431,10 +432,9 @@ semantic-version==2.10.0 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # edx-drf-extensions -six==1.16.0 +six==1.17.0 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt - # bleach # edx-ccx-keys # edx-rbac # freezegun @@ -443,17 +443,17 @@ six==1.16.0 # responses slumber==0.7.1 # via -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt -snowflake-connector-python==3.12.3 +snowflake-connector-python==3.12.4 # via -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt sortedcontainers==2.4.0 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # snowflake-connector-python -sqlparse==0.5.1 +sqlparse==0.5.3 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # django -stevedore==5.3.0 +stevedore==5.4.0 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # code-annotations @@ -473,7 +473,7 @@ tomlkit==0.13.2 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # snowflake-connector-python -tqdm==4.66.6 +tqdm==4.67.1 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # openai @@ -498,6 +498,7 @@ uritemplate==4.1.1 # drf-yasg urllib3==2.2.3 # via + # -c /home/runner/work/edx-enterprise/edx-enterprise/requirements/common_constraints.txt # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # requests # via @@ -513,7 +514,7 @@ webencodings==0.5.1 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # bleach -yarl==1.17.0 +yarl==1.18.3 # via # -r /home/runner/work/edx-enterprise/edx-enterprise/requirements/test-master.txt # aiohttp diff --git a/tests/test_enterprise/api/test_default_enrollment_views.py b/tests/test_enterprise/api/test_default_enrollment_views.py new file mode 100644 index 000000000..91d631805 --- /dev/null +++ b/tests/test_enterprise/api/test_default_enrollment_views.py @@ -0,0 +1,726 @@ +""" +Tests for the edx-enterprise ``api.v1.views.default_enterprise_enrollments module``. +""" +import uuid +from unittest import mock + +import ddt +from faker import Faker +from oauth2_provider.models import get_application_model +from pytest import mark +from rest_framework import status +from rest_framework.reverse import reverse + +from django.conf import settings + +from enterprise.constants import ENTERPRISE_LEARNER_ROLE +from enterprise.models import EnterpriseCourseEnrollment +from test_utils import FAKE_UUIDS, TEST_PASSWORD, factories, fake_catalog_api + +from .constants import AUDIT_COURSE_MODE, VERIFIED_COURSE_MODE +from .test_views import BaseTestEnterpriseAPIViews, create_mock_default_enterprise_enrollment_intention + +Application = get_application_model() +fake = Faker() + +DEFAULT_ENTERPRISE_ENROLLMENT_INTENTION_LIST_ENDPOINT = reverse('default-enterprise-enrollment-intentions-list') +DEFAULT_ENTERPRISE_ENROLLMENT_INTENTION_LEARNER_STATUS_ENDPOINT = reverse( + 'default-enterprise-enrollment-intentions-learner-status' +) + + +def get_default_enterprise_enrollment_intention_detail_endpoint(enrollment_intention_uuid=None): + return reverse( + 'default-enterprise-enrollment-intentions-detail', + kwargs={'pk': enrollment_intention_uuid if enrollment_intention_uuid else FAKE_UUIDS[0]} + ) + + +@ddt.ddt +@mark.django_db +class TestDefaultEnterpriseEnrollmentIntentionViewSet(BaseTestEnterpriseAPIViews): + """ + Test DefaultEnterpriseEnrollmentIntentionViewSet + """ + + def setUp(self): + super().setUp() + self.enterprise_customer = factories.EnterpriseCustomerFactory() + + username = 'test_user_default_enterprise_enrollment_intentions' + self.user = self.create_user(username=username, is_staff=False) + self.client.login(username=self.user.username, password=TEST_PASSWORD) + + def get_default_enrollment_intention_with_learner_enrollment_state(self, enrollment_intention, **kwargs): + """ + Returns the expected serialized default enrollment intention with learner enrollment state. + + Args: + enrollment_intention: The enrollment intention to serialize. + **kwargs: Additional parameters to customize the response. + - applicable_enterprise_catalog_uuids: List of applicable enterprise catalog UUIDs. + - is_course_run_enrollable: Boolean indicating if the course run is enrollable. + - best_mode_for_course_run: The best mode for the course run (e.g., "verified", "audit"). + - has_existing_enrollment: Boolean indicating if there is an existing enrollment. + - is_existing_enrollment_active: Boolean indicating if the existing enrollment is + active, or None if no existing enrollment. + - is_existing_enrollment_audit: Boolean indicating if the existing enrollment is + audit, or None if no existing enrollment. + """ + return { + 'uuid': str(enrollment_intention.uuid), + 'content_key': enrollment_intention.content_key, + 'enterprise_customer': str(self.enterprise_customer.uuid), + 'course_key': enrollment_intention.course_key, + 'course_run_key': enrollment_intention.course_run_key, + 'is_course_run_enrollable': kwargs.get('is_course_run_enrollable', True), + 'best_mode_for_course_run': kwargs.get('best_mode_for_course_run', VERIFIED_COURSE_MODE), + 'applicable_enterprise_catalog_uuids': kwargs.get( + 'applicable_enterprise_catalog_uuids', + [fake_catalog_api.FAKE_CATALOG_RESULT.get('uuid')], + ), + 'course_run_normalized_metadata': { + 'start_date': fake_catalog_api.FAKE_COURSE_RUN.get('start'), + 'end_date': fake_catalog_api.FAKE_COURSE_RUN.get('end'), + 'enroll_by_date': fake_catalog_api.FAKE_COURSE_RUN.get('seats')[1].get('upgrade_deadline'), + 'enroll_start_date': fake_catalog_api.FAKE_COURSE_RUN.get('enrollment_start'), + 'content_price': fake_catalog_api.FAKE_COURSE_RUN.get('first_enrollable_paid_seat_price'), + }, + 'created': enrollment_intention.created.strftime("%Y-%m-%dT%H:%M:%SZ"), + 'modified': enrollment_intention.modified.strftime("%Y-%m-%dT%H:%M:%SZ"), + 'has_existing_enrollment': kwargs.get('has_existing_enrollment', False), + 'is_existing_enrollment_active': kwargs.get('is_existing_enrollment_active', None), + 'is_existing_enrollment_audit': kwargs.get('is_existing_enrollment_audit', None), + } + + def test_default_enterprise_enrollment_intentions_missing_enterprise_uuid(self): + """ + Test expected response when successfully listing existing default enterprise enrollment intentions. + """ + self.set_jwt_cookie(ENTERPRISE_LEARNER_ROLE, str(self.enterprise_customer.uuid)) + response = self.client.get(f"{settings.TEST_SERVER}{DEFAULT_ENTERPRISE_ENROLLMENT_INTENTION_LIST_ENDPOINT}") + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.json() == {'detail': 'enterprise_customer_uuid is a required query parameter.'} + + def test_default_enterprise_enrollment_intentions_invalid_enterprise_uuid(self): + """ + Test expected response when successfully listing existing default enterprise enrollment intentions. + """ + self.set_jwt_cookie(ENTERPRISE_LEARNER_ROLE, str(self.enterprise_customer.uuid)) + query_params = 'enterprise_customer_uuid=invalid-uuid' + response = self.client.get( + f"{settings.TEST_SERVER}{DEFAULT_ENTERPRISE_ENROLLMENT_INTENTION_LIST_ENDPOINT}?{query_params}" + ) + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.json() == {'detail': 'enterprise_customer_uuid query parameter is not a valid UUID.'} + + @mock.patch('enterprise.content_metadata.api.EnterpriseCatalogApiClient') + def test_default_enterprise_enrollment_intentions_list(self, mock_catalog_api_client): + """ + Test expected response when successfully listing existing default enterprise enrollment intentions. + """ + self.set_jwt_cookie(ENTERPRISE_LEARNER_ROLE, str(self.enterprise_customer.uuid)) + enrollment_intention = create_mock_default_enterprise_enrollment_intention( + enterprise_customer=self.enterprise_customer, + mock_catalog_api_client=mock_catalog_api_client, + ) + query_params = f'enterprise_customer_uuid={str(self.enterprise_customer.uuid)}' + response = self.client.get( + f"{settings.TEST_SERVER}{DEFAULT_ENTERPRISE_ENROLLMENT_INTENTION_LIST_ENDPOINT}?{query_params}" + ) + assert response.status_code == status.HTTP_200_OK + response_data = response.json() + assert response_data['count'] == 1 + result = response_data['results'][0] + assert result['content_key'] == enrollment_intention.content_key + assert result['applicable_enterprise_catalog_uuids'] == [fake_catalog_api.FAKE_CATALOG_RESULT.get('uuid')] + + @mock.patch('enterprise.content_metadata.api.EnterpriseCatalogApiClient') + def test_default_enterprise_enrollment_intentions_detail(self, mock_catalog_api_client): + """ + Test expected response when unauthorized user attempts to list default + enterprise enrollment intentions. + """ + self.set_jwt_cookie(ENTERPRISE_LEARNER_ROLE, str(self.enterprise_customer.uuid)) + enrollment_intention = create_mock_default_enterprise_enrollment_intention( + enterprise_customer=self.enterprise_customer, + mock_catalog_api_client=mock_catalog_api_client, + ) + query_params = f'enterprise_customer_uuid={str(self.enterprise_customer.uuid)}' + base_url = get_default_enterprise_enrollment_intention_detail_endpoint(str(enrollment_intention.uuid)) + response = self.client.get(f"{settings.TEST_SERVER}{base_url}?{query_params}") + assert response.status_code == status.HTTP_200_OK + response_data = response.json() + assert response_data['content_key'] == enrollment_intention.content_key + assert response_data['applicable_enterprise_catalog_uuids'] == \ + [fake_catalog_api.FAKE_CATALOG_RESULT.get('uuid')] + + @mock.patch('enterprise.content_metadata.api.EnterpriseCatalogApiClient') + def test_default_enterprise_enrollment_intentions_list_unauthorized(self, mock_catalog_api_client): + """ + Test expected response when unauthorized user attempts to list default + enterprise enrollment intentions. + """ + self.set_jwt_cookie(ENTERPRISE_LEARNER_ROLE, str(self.enterprise_customer.uuid)) + create_mock_default_enterprise_enrollment_intention( + enterprise_customer=self.enterprise_customer, + mock_catalog_api_client=mock_catalog_api_client, + ) + query_params = f'enterprise_customer_uuid={str(uuid.uuid4())}' + response = self.client.get( + f"{settings.TEST_SERVER}{DEFAULT_ENTERPRISE_ENROLLMENT_INTENTION_LIST_ENDPOINT}?{query_params}" + ) + assert response.status_code == status.HTTP_200_OK + response_data = response.json() + assert response_data['count'] == 0 + assert response_data['results'] == [] + + @mock.patch('enterprise.content_metadata.api.EnterpriseCatalogApiClient') + def test_default_enterprise_enrollment_intentions_detail_403_forbidden(self, mock_catalog_api_client): + """ + Test expected response when unauthorized user attempts to list default + enterprise enrollment intentions. + """ + self.set_jwt_cookie(ENTERPRISE_LEARNER_ROLE, str(self.enterprise_customer.uuid)) + enrollment_intention = create_mock_default_enterprise_enrollment_intention( + enterprise_customer=self.enterprise_customer, + mock_catalog_api_client=mock_catalog_api_client, + ) + query_params = f'enterprise_customer_uuid={str(uuid.uuid4())}' + base_url = get_default_enterprise_enrollment_intention_detail_endpoint(str(enrollment_intention.uuid)) + response = self.client.get(f"{settings.TEST_SERVER}{base_url}?{query_params}") + assert response.status_code == status.HTTP_403_FORBIDDEN + + @mock.patch('enterprise.content_metadata.api.EnterpriseCatalogApiClient') + def test_default_enterprise_enrollment_intentions_not_in_catalog(self, mock_catalog_api_client): + """ + Test expected response when default enterprise enrollment intention is not in catalog. + """ + self.set_jwt_cookie(ENTERPRISE_LEARNER_ROLE, str(self.enterprise_customer.uuid)) + enrollment_intention = create_mock_default_enterprise_enrollment_intention( + enterprise_customer=self.enterprise_customer, + mock_catalog_api_client=mock_catalog_api_client, + contains_content_items=False, + catalog_list=[], + ) + query_params = f'enterprise_customer_uuid={str(self.enterprise_customer.uuid)}' + base_url = get_default_enterprise_enrollment_intention_detail_endpoint(str(enrollment_intention.uuid)) + response = self.client.get(f"{settings.TEST_SERVER}{base_url}?{query_params}") + assert response.status_code == status.HTTP_200_OK + response_data = response.json() + assert response_data['content_key'] == enrollment_intention.content_key + assert response_data['applicable_enterprise_catalog_uuids'] == [] + + def test_default_enterprise_enrollment_intentions_learner_status_not_linked(self): + """ + Test default enterprise enrollment intentions for specific learner not linked to enterprise customer. + """ + self.set_jwt_cookie(ENTERPRISE_LEARNER_ROLE, str(self.enterprise_customer.uuid)) + query_params = f'enterprise_customer_uuid={str(self.enterprise_customer.uuid)}' + response = self.client.get( + f"{settings.TEST_SERVER}{DEFAULT_ENTERPRISE_ENROLLMENT_INTENTION_LEARNER_STATUS_ENDPOINT}?{query_params}" + ) + assert response.status_code == status.HTTP_400_BAD_REQUEST + response_data = response.json() + assert response_data['detail'] == ( + f'User with lms_user_id {self.user.id} is not associated with ' + f'the enterprise customer {str(self.enterprise_customer.uuid)}.' + ) + + @mock.patch('enterprise.content_metadata.api.EnterpriseCatalogApiClient') + @mock.patch('enterprise.models.utils.get_best_mode_from_course_key') + def test_default_enterprise_enrollment_intentions_learner_status_enrollable( + self, + mock_get_best_mode_from_course_key, + mock_catalog_api_client, + ): + """ + Test default enterprise enrollment intentions for specific learner linked to enterprise customer, where + the course run associated with the default enrollment intention is enrollable. + """ + self.set_jwt_cookie(ENTERPRISE_LEARNER_ROLE, str(self.enterprise_customer.uuid)) + enrollment_intention = create_mock_default_enterprise_enrollment_intention( + enterprise_customer=self.enterprise_customer, + mock_catalog_api_client=mock_catalog_api_client, + ) + mock_get_best_mode_from_course_key.return_value = VERIFIED_COURSE_MODE + factories.EnterpriseCustomerUserFactory( + user_id=self.user.id, + enterprise_customer=self.enterprise_customer, + ) + query_params = f'enterprise_customer_uuid={str(self.enterprise_customer.uuid)}' + response = self.client.get( + f"{settings.TEST_SERVER}{DEFAULT_ENTERPRISE_ENROLLMENT_INTENTION_LEARNER_STATUS_ENDPOINT}?{query_params}" + ) + assert response.status_code == status.HTTP_200_OK + response_data = response.json() + assert response_data['lms_user_id'] == self.user.id + assert response_data['user_email'] == self.user.email + assert response_data['enrollment_statuses'] == { + 'needs_enrollment': { + 'enrollable': [ + self.get_default_enrollment_intention_with_learner_enrollment_state(enrollment_intention) + ], + 'not_enrollable': [], + }, + 'already_enrolled': [], + } + assert response_data['metadata'] == { + 'total_default_enterprise_enrollment_intentions': 1, + 'total_needs_enrollment': { + 'enrollable': 1, + 'not_enrollable': 0, + }, + 'total_already_enrolled': 0, + } + + @ddt.data( + {'run_is_enrollable': False, 'unenrollment_exists': False}, + {'run_is_enrollable': True, 'unenrollment_exists': True}, + ) + @mock.patch('enterprise.content_metadata.api.EnterpriseCatalogApiClient') + @mock.patch('enterprise.models.utils.get_best_mode_from_course_key') + @mock.patch.object(EnterpriseCourseEnrollment, 'course_enrollment', new_callable=mock.PropertyMock) + @ddt.unpack + def test_default_enrollment_intentions_learner_status_content_not_enrollable( + self, + mock_course_enrollment, + mock_get_best_mode_from_course_key, + mock_catalog_api_client, + run_is_enrollable, + unenrollment_exists, + ): + """ + Test default enterprise enrollment intentions (not enrollable) for + specific learner linked to enterprise customer. + """ + self.set_jwt_cookie(ENTERPRISE_LEARNER_ROLE, str(self.enterprise_customer.uuid)) + + mock_course_run = fake_catalog_api.FAKE_COURSE_RUN.copy() + mock_course_run.update({'is_enrollable': run_is_enrollable}) + mock_course = fake_catalog_api.FAKE_COURSE.copy() + mock_course.update({'course_runs': [mock_course_run]}) + enrollment_intention = create_mock_default_enterprise_enrollment_intention( + enterprise_customer=self.enterprise_customer, + mock_catalog_api_client=mock_catalog_api_client, + content_metadata=mock_course, + ) + + mock_get_best_mode_from_course_key.return_value = VERIFIED_COURSE_MODE + ecu = factories.EnterpriseCustomerUserFactory( + user_id=self.user.id, + enterprise_customer=self.enterprise_customer, + ) + if unenrollment_exists: + factories.EnterpriseCourseEnrollmentFactory( + enterprise_customer_user=ecu, + course_id=fake_catalog_api.FAKE_COURSE_RUN.get('key'), + unenrolled=True, + ) + mock_course_enrollment.return_value = mock.Mock( + is_active=False, + mode=VERIFIED_COURSE_MODE, + ) + + query_params = f'enterprise_customer_uuid={str(self.enterprise_customer.uuid)}' + response = self.client.get( + f"{settings.TEST_SERVER}{DEFAULT_ENTERPRISE_ENROLLMENT_INTENTION_LEARNER_STATUS_ENDPOINT}?{query_params}" + ) + + assert response.status_code == status.HTTP_200_OK + response_data = response.json() + assert response_data['lms_user_id'] == self.user.id + assert response_data['user_email'] == self.user.email + assert response_data['enrollment_statuses'] == { + 'needs_enrollment': { + 'enrollable': [], + 'not_enrollable': [ + self.get_default_enrollment_intention_with_learner_enrollment_state( + enrollment_intention, + is_course_run_enrollable=run_is_enrollable, + has_existing_enrollment=unenrollment_exists, + is_existing_enrollment_active=False if unenrollment_exists else None, + is_existing_enrollment_audit=False if unenrollment_exists else None, + ) + ], + }, + 'already_enrolled': [], + } + assert response_data['metadata'] == { + 'total_default_enterprise_enrollment_intentions': 1, + 'total_needs_enrollment': { + 'enrollable': 0, + 'not_enrollable': 1, + }, + 'total_already_enrolled': 0, + } + + @mock.patch('enterprise.content_metadata.api.EnterpriseCatalogApiClient') + @mock.patch('enterprise.models.utils.get_best_mode_from_course_key') + def test_default_enrollment_intentions_learner_status_content_not_in_catalog( + self, + mock_get_best_mode_from_course_key, + mock_catalog_api_client, + ): + """ + Test default enterprise enrollment intentions (not enrollable, no applicable + catalog) for specific learner linked to enterprise customer. + """ + self.set_jwt_cookie(ENTERPRISE_LEARNER_ROLE, str(self.enterprise_customer.uuid)) + enrollment_intention = create_mock_default_enterprise_enrollment_intention( + enterprise_customer=self.enterprise_customer, + mock_catalog_api_client=mock_catalog_api_client, + contains_content_items=False, + catalog_list=[], + ) + mock_get_best_mode_from_course_key.return_value = VERIFIED_COURSE_MODE + factories.EnterpriseCustomerUserFactory( + user_id=self.user.id, + enterprise_customer=self.enterprise_customer, + ) + query_params = f'enterprise_customer_uuid={str(self.enterprise_customer.uuid)}' + response = self.client.get( + f"{settings.TEST_SERVER}{DEFAULT_ENTERPRISE_ENROLLMENT_INTENTION_LEARNER_STATUS_ENDPOINT}?{query_params}" + ) + assert response.status_code == status.HTTP_200_OK + response_data = response.json() + assert response_data['lms_user_id'] == self.user.id + assert response_data['user_email'] == self.user.email + assert response_data['enrollment_statuses'] == { + 'needs_enrollment': { + 'enrollable': [], + 'not_enrollable': [ + self.get_default_enrollment_intention_with_learner_enrollment_state( + enrollment_intention, + applicable_enterprise_catalog_uuids=[], + is_course_run_enrollable=True, + has_existing_enrollment=False, + is_existing_enrollment_active=None, + is_existing_enrollment_audit=None, + ) + ], + }, + 'already_enrolled': [], + } + assert response_data['metadata'] == { + 'total_default_enterprise_enrollment_intentions': 1, + 'total_needs_enrollment': { + 'enrollable': 0, + 'not_enrollable': 1, + }, + 'total_already_enrolled': 0, + } + + @mock.patch('enterprise.content_metadata.api.EnterpriseCatalogApiClient') + @mock.patch('enterprise.models.utils.get_best_mode_from_course_key') + @mock.patch.object(EnterpriseCourseEnrollment, 'course_enrollment', new_callable=mock.PropertyMock) + def test_default_enrollment_intentions_learner_status_already_enrolled_active( + self, + mock_course_enrollment, + mock_get_best_mode_from_course_key, + mock_catalog_api_client, + ): + """ + Test default enterprise enrollment intentions (already enrolled, active + enrollment) for specific learner linked to enterprise customer. + """ + self.set_jwt_cookie(ENTERPRISE_LEARNER_ROLE, str(self.enterprise_customer.uuid)) + enrollment_intention = create_mock_default_enterprise_enrollment_intention( + enterprise_customer=self.enterprise_customer, + mock_catalog_api_client=mock_catalog_api_client, + ) + mock_get_best_mode_from_course_key.return_value = VERIFIED_COURSE_MODE + enterprise_customer_user = factories.EnterpriseCustomerUserFactory( + user_id=self.user.id, + enterprise_customer=self.enterprise_customer, + ) + factories.EnterpriseCourseEnrollmentFactory( + enterprise_customer_user=enterprise_customer_user, + course_id=fake_catalog_api.FAKE_COURSE_RUN.get('key'), + ) + course_enrollment_kwargs = { + 'is_active': True, + 'mode': VERIFIED_COURSE_MODE, + } + mock_course_enrollment.return_value = mock.Mock(**course_enrollment_kwargs) + query_params = f'enterprise_customer_uuid={str(self.enterprise_customer.uuid)}' + response = self.client.get( + f"{settings.TEST_SERVER}{DEFAULT_ENTERPRISE_ENROLLMENT_INTENTION_LEARNER_STATUS_ENDPOINT}?{query_params}" + ) + assert response.status_code == status.HTTP_200_OK + response_data = response.json() + assert response_data['lms_user_id'] == self.user.id + assert response_data['user_email'] == self.user.email + assert response_data['enrollment_statuses'] == { + 'needs_enrollment': { + 'enrollable': [], + 'not_enrollable': [], + }, + 'already_enrolled': [ + self.get_default_enrollment_intention_with_learner_enrollment_state( + enrollment_intention, + has_existing_enrollment=True, + is_existing_enrollment_active=True, + is_existing_enrollment_audit=False, + ) + ], + } + assert response_data['metadata'] == { + 'total_default_enterprise_enrollment_intentions': 1, + 'total_needs_enrollment': { + 'enrollable': 0, + 'not_enrollable': 0, + }, + 'total_already_enrolled': 1, + } + + @mock.patch('enterprise.content_metadata.api.EnterpriseCatalogApiClient') + @mock.patch('enterprise.models.utils.get_best_mode_from_course_key') + @mock.patch.object(EnterpriseCourseEnrollment, 'course_enrollment', new_callable=mock.PropertyMock) + def test_default_enrollment_intentions_learner_status_already_enrolled_inactive( + self, + mock_course_enrollment, + mock_get_best_mode_from_course_key, + mock_catalog_api_client, + ): + """ + Test default enterprise enrollment intentions (already enrolled, inactive + enrollment) for specific learner linked to enterprise customer. + """ + self.set_jwt_cookie(ENTERPRISE_LEARNER_ROLE, str(self.enterprise_customer.uuid)) + enrollment_intention = create_mock_default_enterprise_enrollment_intention( + enterprise_customer=self.enterprise_customer, + mock_catalog_api_client=mock_catalog_api_client, + ) + mock_get_best_mode_from_course_key.return_value = VERIFIED_COURSE_MODE + enterprise_customer_user = factories.EnterpriseCustomerUserFactory( + user_id=self.user.id, + enterprise_customer=self.enterprise_customer, + ) + factories.EnterpriseCourseEnrollmentFactory( + enterprise_customer_user=enterprise_customer_user, + course_id=fake_catalog_api.FAKE_COURSE_RUN.get('key'), + ) + course_enrollment_kwargs = { + 'is_active': False, + 'mode': VERIFIED_COURSE_MODE, + } + mock_course_enrollment.return_value = mock.Mock(**course_enrollment_kwargs) + query_params = f'enterprise_customer_uuid={str(self.enterprise_customer.uuid)}' + response = self.client.get( + f"{settings.TEST_SERVER}{DEFAULT_ENTERPRISE_ENROLLMENT_INTENTION_LEARNER_STATUS_ENDPOINT}?{query_params}" + ) + assert response.status_code == status.HTTP_200_OK + response_data = response.json() + assert response_data['lms_user_id'] == self.user.id + assert response_data['user_email'] == self.user.email + assert response_data['enrollment_statuses'] == { + 'needs_enrollment': { + 'enrollable': [ + self.get_default_enrollment_intention_with_learner_enrollment_state( + enrollment_intention, + has_existing_enrollment=True, + is_existing_enrollment_active=False, + is_existing_enrollment_audit=False, + ) + ], + 'not_enrollable': [], + }, + 'already_enrolled': [], + } + assert response_data['metadata'] == { + 'total_default_enterprise_enrollment_intentions': 1, + 'total_needs_enrollment': { + 'enrollable': 1, + 'not_enrollable': 0, + }, + 'total_already_enrolled': 0, + } + + @ddt.data( + {'has_audit_mode_only': True}, + {'has_audit_mode_only': False}, + ) + @mock.patch('enterprise.content_metadata.api.EnterpriseCatalogApiClient') + @mock.patch('enterprise.models.utils.get_best_mode_from_course_key') + @mock.patch.object(EnterpriseCourseEnrollment, 'course_enrollment', new_callable=mock.PropertyMock) + @ddt.unpack + def test_default_enrollment_intentions_learner_status_already_enrolled_active_audit( + self, + mock_course_enrollment, + mock_get_best_mode_from_course_key, + mock_catalog_api_client, + has_audit_mode_only, + ): + """ + Test default enterprise enrollment intentions (already enrolled, active + audit enrollment) for specific learner linked to enterprise customer. + """ + self.set_jwt_cookie(ENTERPRISE_LEARNER_ROLE, str(self.enterprise_customer.uuid)) + enrollment_intention = create_mock_default_enterprise_enrollment_intention( + enterprise_customer=self.enterprise_customer, + mock_catalog_api_client=mock_catalog_api_client, + ) + + best_mode_for_course_run = AUDIT_COURSE_MODE if has_audit_mode_only else VERIFIED_COURSE_MODE + mock_get_best_mode_from_course_key.return_value = best_mode_for_course_run + + enterprise_customer_user = factories.EnterpriseCustomerUserFactory( + user_id=self.user.id, + enterprise_customer=self.enterprise_customer, + ) + factories.EnterpriseCourseEnrollmentFactory( + enterprise_customer_user=enterprise_customer_user, + course_id=fake_catalog_api.FAKE_COURSE_RUN.get('key'), + ) + course_enrollment_kwargs = { + 'is_active': True, + 'mode': AUDIT_COURSE_MODE, + } + mock_course_enrollment.return_value = mock.Mock(**course_enrollment_kwargs) + query_params = f'enterprise_customer_uuid={str(self.enterprise_customer.uuid)}' + response = self.client.get( + f"{settings.TEST_SERVER}{DEFAULT_ENTERPRISE_ENROLLMENT_INTENTION_LEARNER_STATUS_ENDPOINT}?{query_params}" + ) + assert response.status_code == status.HTTP_200_OK + response_data = response.json() + assert response_data['lms_user_id'] == self.user.id + assert response_data['user_email'] == self.user.email + + expected_enrollable = [] + expected_already_enrolled = [] + + expected_serialized_intention = self.get_default_enrollment_intention_with_learner_enrollment_state( + enrollment_intention, + has_existing_enrollment=True, + is_existing_enrollment_active=True, + is_existing_enrollment_audit=True, + best_mode_for_course_run=best_mode_for_course_run, + ) + + if has_audit_mode_only: + expected_already_enrolled.append(expected_serialized_intention) + else: + expected_enrollable.append(expected_serialized_intention) + + assert response_data['enrollment_statuses'] == { + 'needs_enrollment': { + 'enrollable': expected_enrollable, + 'not_enrollable': [], + }, + 'already_enrolled': expected_already_enrolled, + } + assert response_data['metadata'] == { + 'total_default_enterprise_enrollment_intentions': 1, + 'total_needs_enrollment': { + 'enrollable': len(expected_enrollable), + 'not_enrollable': 0, + }, + 'total_already_enrolled': len(expected_already_enrolled), + } + + @mock.patch('enterprise.content_metadata.api.EnterpriseCatalogApiClient') + @mock.patch('enterprise.models.utils.get_best_mode_from_course_key') + def test_default_enrollment_intentions_learner_status_staff_lms_user_id_override( + self, + mock_get_best_mode_from_course_key, + mock_catalog_api_client, + ): + """ + Test default enterprise enrollment intentions for staff user, requesting a specific user + linked to enterprise customer via lms_user_id query parameter. + """ + self.set_jwt_cookie(ENTERPRISE_LEARNER_ROLE, str(self.enterprise_customer.uuid)) + + # Create and login as a staff user + staff_user = self.create_user(username='staff_username', password=TEST_PASSWORD, is_staff=True) + self.client.login(username=staff_user.username, password=TEST_PASSWORD) + + enrollment_intention = create_mock_default_enterprise_enrollment_intention( + enterprise_customer=self.enterprise_customer, + mock_catalog_api_client=mock_catalog_api_client, + ) + mock_get_best_mode_from_course_key.return_value = VERIFIED_COURSE_MODE + factories.EnterpriseCustomerUserFactory( + user_id=self.user.id, + enterprise_customer=self.enterprise_customer, + ) + query_params = ( + f'enterprise_customer_uuid={str(self.enterprise_customer.uuid)}' + # Validates staff user can get back data for another user (i.e., request user is `staff_user`) + f'&lms_user_id={self.user.id}' + ) + response = self.client.get( + f"{settings.TEST_SERVER}{DEFAULT_ENTERPRISE_ENROLLMENT_INTENTION_LEARNER_STATUS_ENDPOINT}?{query_params}" + ) + assert response.status_code == status.HTTP_200_OK + response_data = response.json() + assert response_data['lms_user_id'] == self.user.id + assert response_data['user_email'] == self.user.email + assert response_data['enrollment_statuses'] == { + 'needs_enrollment': { + 'enrollable': [ + self.get_default_enrollment_intention_with_learner_enrollment_state(enrollment_intention) + ], + 'not_enrollable': [], + }, + 'already_enrolled': [], + } + assert response_data['metadata'] == { + 'total_default_enterprise_enrollment_intentions': 1, + 'total_needs_enrollment': { + 'enrollable': 1, + 'not_enrollable': 0, + }, + 'total_already_enrolled': 0, + } + + @mock.patch('enterprise.content_metadata.api.EnterpriseCatalogApiClient') + @mock.patch('enterprise.models.utils.get_best_mode_from_course_key') + def test_default_enrollment_intentions_learner_status_nonstaff_lms_user_id_override( + self, + mock_get_best_mode_from_course_key, + mock_catalog_api_client + ): + """ + Test default enterprise enrollment intentions for non-staff user linked to enterprise customer. + """ + self.set_jwt_cookie(ENTERPRISE_LEARNER_ROLE, str(self.enterprise_customer.uuid)) + enrollment_intention = create_mock_default_enterprise_enrollment_intention( + enterprise_customer=self.enterprise_customer, + mock_catalog_api_client=mock_catalog_api_client, + ) + mock_get_best_mode_from_course_key.return_value = VERIFIED_COURSE_MODE + factories.EnterpriseCustomerUserFactory( + user_id=self.user.id, + enterprise_customer=self.enterprise_customer, + ) + query_params = ( + f'enterprise_customer_uuid={str(self.enterprise_customer.uuid)}' + f'&lms_user_id={self.user.id + 1}' # Validates non-staff user can't get back data for another user + ) + response = self.client.get( + f"{settings.TEST_SERVER}{DEFAULT_ENTERPRISE_ENROLLMENT_INTENTION_LEARNER_STATUS_ENDPOINT}?{query_params}" + ) + + assert response.status_code == status.HTTP_200_OK + response_data = response.json() + assert response_data['lms_user_id'] == self.user.id + assert response_data['user_email'] == self.user.email + assert response_data['enrollment_statuses'] == { + 'needs_enrollment': { + 'enrollable': [ + self.get_default_enrollment_intention_with_learner_enrollment_state(enrollment_intention) + ], + 'not_enrollable': [], + }, + 'already_enrolled': [], + } + assert response_data['metadata'] == { + 'total_default_enterprise_enrollment_intentions': 1, + 'total_needs_enrollment': { + 'enrollable': 1, + 'not_enrollable': 0, + }, + 'total_already_enrolled': 0, + } diff --git a/tests/test_enterprise/api/test_filters.py b/tests/test_enterprise/api/test_filters.py index f4b68efe3..6ab8484b0 100644 --- a/tests/test_enterprise/api/test_filters.py +++ b/tests/test_enterprise/api/test_filters.py @@ -305,6 +305,7 @@ def test_filter(self, is_staff, is_linked_to_enterprise, has_access): 'enterprise_groups_v1': False, 'enterprise_customer_support_tool': False, 'enterprise_groups_v2': False, + 'enterprise_learner_bff_enabled': False, } } assert response == mock_empty_200_success_response diff --git a/tests/test_enterprise/api/test_serializers.py b/tests/test_enterprise/api/test_serializers.py index 8cba7803c..4d177fd0d 100644 --- a/tests/test_enterprise/api/test_serializers.py +++ b/tests/test_enterprise/api/test_serializers.py @@ -589,6 +589,7 @@ def setUp(self): def test_serialize_users(self): expected_user = { 'enterprise_customer_user': { + 'user_id': self.user_1.id, 'email': self.user_1.email, 'joined_org': self.user_1.date_joined.strftime("%b %d, %Y"), 'name': (self.user_1.first_name + ' ' + self.user_1.last_name), @@ -609,6 +610,7 @@ def test_serialize_users(self): expected_user_2 = { 'enterprise_customer_user': { + 'user_id': self.user_2.id, 'email': self.user_2.email, 'joined_org': self.user_2.date_joined.strftime("%b %d, %Y"), 'name': self.user_2.first_name + ' ' + self.user_2.last_name, diff --git a/tests/test_enterprise/api/test_views.py b/tests/test_enterprise/api/test_views.py index 1d4016719..87e501024 100644 --- a/tests/test_enterprise/api/test_views.py +++ b/tests/test_enterprise/api/test_views.py @@ -73,6 +73,7 @@ ENTERPRISE_CUSTOMER_SUPPORT_TOOL, ENTERPRISE_GROUPS_V1, ENTERPRISE_GROUPS_V2, + ENTERPRISE_LEARNER_BFF_ENABLED, FEATURE_PREQUERY_SEARCH_SUGGESTIONS, TOP_DOWN_ASSIGNMENT_REAL_TIME_LCM, ) @@ -170,35 +171,6 @@ EXPIRED_LICENSED_ENTERPRISE_COURSE_ENROLLMENTS_ENDPOINT = reverse( 'licensed-enterprise-course-enrollment-bulk-licensed-enrollments-expiration' ) -DEFAULT_ENTERPRISE_ENROLLMENT_INTENTION_LIST_ENDPOINT = reverse('default-enterprise-enrollment-intentions-list') -DEFAULT_ENTERPRISE_ENROLLMENT_INTENTION_LEARNER_STATUS_ENDPOINT = reverse( - 'default-enterprise-enrollment-intentions-learner-status' -) - - -def get_default_enterprise_enrollment_intention_detail_endpoint(enrollment_intention_uuid=None): - return reverse( - 'default-enterprise-enrollment-intentions-detail', - kwargs={'pk': enrollment_intention_uuid if enrollment_intention_uuid else FAKE_UUIDS[0]} - ) - - -def side_effect(url, query_parameters): - """ - returns a url with updated query parameters. - """ - if any(key in ['utm_medium', 'catalog'] for key in query_parameters): - return url - - scheme, netloc, path, query_string, fragment = urlsplit(url) - url_params = parse_qs(query_string) - - # Update url query parameters - url_params.update(query_parameters) - - return urlunsplit( - (scheme, netloc, path, urlencode(url_params, doseq=True), fragment), - ) def create_mock_default_enterprise_enrollment_intention( @@ -237,6 +209,24 @@ def create_mock_default_enterprise_enrollment_intention( return enrollment_intention +def side_effect(url, query_parameters): + """ + returns a url with updated query parameters. + """ + if any(key in ['utm_medium', 'catalog'] for key in query_parameters): + return url + + scheme, netloc, path, query_string, fragment = urlsplit(url) + url_params = parse_qs(query_string) + + # Update url query parameters + url_params.update(query_parameters) + + return urlunsplit( + (scheme, netloc, path, urlencode(url_params, doseq=True), fragment), + ) + + class BaseTestEnterpriseAPIViews(APITest): """ Shared setup and methods for enterprise api views. @@ -2029,63 +2019,64 @@ def test_enterprise_customer_support_tool( @ddt.data( # Request missing required permissions query param. (True, False, [], {}, False, {'detail': 'User is not allowed to access the view.'}, - False, False, False, False, False), + False, False, False, False, False, False), # Staff user that does not have the specified group permission. (True, False, [], {'permissions': ['enterprise_enrollment_api_access']}, False, - {'detail': 'User is not allowed to access the view.'}, False, False, False, False, False), + {'detail': 'User is not allowed to access the view.'}, False, False, False, False, False, False), # Staff user that does have the specified group permission. (True, False, ['enterprise_enrollment_api_access'], {'permissions': ['enterprise_enrollment_api_access']}, - True, None, False, False, False, False, False), + True, None, False, False, False, False, False, False), # Non staff user that is not linked to the enterprise, nor do they have the group permission. (False, False, [], {'permissions': ['enterprise_enrollment_api_access']}, False, - {'detail': 'User is not allowed to access the view.'}, False, False, False, False, False), + {'detail': 'User is not allowed to access the view.'}, False, False, False, False, False, False), # Non staff user that is not linked to the enterprise, but does have the group permission. (False, False, ['enterprise_enrollment_api_access'], {'permissions': ['enterprise_enrollment_api_access']}, - False, None, False, False, False, False, False), + False, None, False, False, False, False, False, False), # Non staff user that is linked to the enterprise, but does not have the group permission. (False, True, [], {'permissions': ['enterprise_enrollment_api_access']}, False, - {'detail': 'User is not allowed to access the view.'}, False, False, False, False, False), + {'detail': 'User is not allowed to access the view.'}, False, False, False, False, False, False), # Non staff user that is linked to the enterprise and does have the group permission (False, True, ['enterprise_enrollment_api_access'], {'permissions': ['enterprise_enrollment_api_access']}, - True, None, False, False, False, False, False), + True, None, False, False, False, False, False, False), # Non staff user that is linked to the enterprise and has group permission and the request has passed # multiple groups to check. (False, True, ['enterprise_enrollment_api_access'], {'permissions': ['enterprise_enrollment_api_access', 'enterprise_data_api_access']}, True, None, False, - False, False, False, False), + False, False, False, False, False), # Staff user with group permission filtering on non existent enterprise id. (True, False, ['enterprise_enrollment_api_access'], {'permissions': ['enterprise_enrollment_api_access'], 'enterprise_id': FAKE_UUIDS[1]}, False, - None, False, False, False, False, False), + None, False, False, False, False, False, False), # Staff user with group permission filtering on enterprise id successfully. (True, False, ['enterprise_enrollment_api_access'], {'permissions': ['enterprise_enrollment_api_access'], 'enterprise_id': FAKE_UUIDS[0]}, True, - None, False, False, False, False, False), + None, False, False, False, False, False, False), # Staff user with group permission filtering on search param with no results. (True, False, ['enterprise_enrollment_api_access'], {'permissions': ['enterprise_enrollment_api_access'], 'search': 'blah'}, False, - None, False, False, False, False, False), + None, False, False, False, False, False, False), # Staff user with group permission filtering on search param with results. (True, False, ['enterprise_enrollment_api_access'], {'permissions': ['enterprise_enrollment_api_access'], 'search': 'test'}, True, - None, False, False, False, False, False), + None, False, False, False, False, False, False), # Staff user with group permission filtering on slug with results. (True, False, ['enterprise_enrollment_api_access'], {'permissions': ['enterprise_enrollment_api_access'], 'slug': TEST_SLUG}, True, - None, False, False, False, False, False), + None, False, False, False, False, False, False), # Staff user with group permissions filtering on slug with no results. (True, False, ['enterprise_enrollment_api_access'], {'permissions': ['enterprise_enrollment_api_access'], 'slug': 'blah'}, False, - None, False, False, False, False, False), + None, False, False, False, False, False, False), # Staff user with group permission filtering on slug with results, with # top down assignment & real-time LCM feature enabled, # prequery search results enabled and # enterprise groups v1 feature enabled # enterprise groups v2 feature enabled # enterprise customer support tool enabled + # enterprise learner bff enabled (True, False, ['enterprise_enrollment_api_access'], {'permissions': ['enterprise_enrollment_api_access'], 'slug': TEST_SLUG}, True, - None, True, True, True, True, True), + None, True, True, True, True, True, True), ) @ddt.unpack @mock.patch('enterprise.utils.get_logo_url') @@ -2101,7 +2092,8 @@ def test_enterprise_customer_with_access_to( feature_prequery_search_suggestions_enabled, enterprise_groups_v1_enabled, enterprise_groups_v2_enabled, - enterprise_customer_support_tool, + enterprise_customer_support_tool_enabled, + enterprise_learner_bff_enabled, mock_get_logo_url, ): """ @@ -2167,7 +2159,6 @@ def test_enterprise_customer_with_access_to( ENTERPRISE_GROUPS_V1, active=enterprise_groups_v1_enabled ): - response = client.get( f"{settings.TEST_SERVER}{ENTERPRISE_CUSTOMER_WITH_ACCESS_TO_ENDPOINT}?{urlencode(query_params, True)}" ) @@ -2180,9 +2171,15 @@ def test_enterprise_customer_with_access_to( ) with override_waffle_flag( ENTERPRISE_CUSTOMER_SUPPORT_TOOL, - active=enterprise_customer_support_tool + active=enterprise_customer_support_tool_enabled + ): + response = client.get( + f"{settings.TEST_SERVER}{ENTERPRISE_CUSTOMER_WITH_ACCESS_TO_ENDPOINT}?{urlencode(query_params, True)}" + ) + with override_waffle_flag( + ENTERPRISE_LEARNER_BFF_ENABLED, + active=enterprise_learner_bff_enabled ): - response = client.get( f"{settings.TEST_SERVER}{ENTERPRISE_CUSTOMER_WITH_ACCESS_TO_ENDPOINT}?{urlencode(query_params, True)}" ) @@ -2254,8 +2251,9 @@ def test_enterprise_customer_with_access_to( 'top_down_assignment_real_time_lcm': is_top_down_assignment_real_time_lcm_enabled, 'feature_prequery_search_suggestions': feature_prequery_search_suggestions_enabled, 'enterprise_groups_v1': enterprise_groups_v1_enabled, - 'enterprise_customer_support_tool': enterprise_customer_support_tool, + 'enterprise_customer_support_tool': enterprise_customer_support_tool_enabled, 'enterprise_groups_v2': enterprise_groups_v2_enabled, + 'enterprise_learner_bff_enabled': enterprise_learner_bff_enabled, } } assert response in (expected_error, mock_empty_200_success_response) @@ -9867,667 +9865,3 @@ def test_list_users_filtered(self): assert expected_json == response.json().get('results') assert response.json().get('count') == 1 - - -@ddt.ddt -@mark.django_db -class TestDefaultEnterpriseEnrollmentIntentionViewSet(BaseTestEnterpriseAPIViews): - """ - Test DefaultEnterpriseEnrollmentIntentionViewSet - """ - - def setUp(self): - super().setUp() - self.enterprise_customer = factories.EnterpriseCustomerFactory() - - username = 'test_user_default_enterprise_enrollment_intentions' - self.user = self.create_user(username=username, is_staff=False) - self.client.login(username=self.user.username, password=TEST_PASSWORD) - - def get_default_enrollment_intention_with_learner_enrollment_state(self, enrollment_intention, **kwargs): - """ - Returns the expected serialized default enrollment intention with learner enrollment state. - - Args: - enrollment_intention: The enrollment intention to serialize. - **kwargs: Additional parameters to customize the response. - - applicable_enterprise_catalog_uuids: List of applicable enterprise catalog UUIDs. - - is_course_run_enrollable: Boolean indicating if the course run is enrollable. - - best_mode_for_course_run: The best mode for the course run (e.g., "verified", "audit"). - - has_existing_enrollment: Boolean indicating if there is an existing enrollment. - - is_existing_enrollment_active: Boolean indicating if the existing enrollment is - active, or None if no existing enrollment. - - is_existing_enrollment_audit: Boolean indicating if the existing enrollment is - audit, or None if no existing enrollment. - """ - return { - 'uuid': str(enrollment_intention.uuid), - 'content_key': enrollment_intention.content_key, - 'enterprise_customer': str(self.enterprise_customer.uuid), - 'course_key': enrollment_intention.course_key, - 'course_run_key': enrollment_intention.course_run_key, - 'is_course_run_enrollable': kwargs.get('is_course_run_enrollable', True), - 'best_mode_for_course_run': kwargs.get('best_mode_for_course_run', VERIFIED_COURSE_MODE), - 'applicable_enterprise_catalog_uuids': kwargs.get( - 'applicable_enterprise_catalog_uuids', - [fake_catalog_api.FAKE_CATALOG_RESULT.get('uuid')], - ), - 'course_run_normalized_metadata': { - 'start_date': fake_catalog_api.FAKE_COURSE_RUN.get('start'), - 'end_date': fake_catalog_api.FAKE_COURSE_RUN.get('end'), - 'enroll_by_date': fake_catalog_api.FAKE_COURSE_RUN.get('seats')[1].get('upgrade_deadline'), - 'enroll_start_date': fake_catalog_api.FAKE_COURSE_RUN.get('enrollment_start'), - 'content_price': fake_catalog_api.FAKE_COURSE_RUN.get('first_enrollable_paid_seat_price'), - }, - 'created': enrollment_intention.created.strftime("%Y-%m-%dT%H:%M:%SZ"), - 'modified': enrollment_intention.modified.strftime("%Y-%m-%dT%H:%M:%SZ"), - 'has_existing_enrollment': kwargs.get('has_existing_enrollment', False), - 'is_existing_enrollment_active': kwargs.get('is_existing_enrollment_active', None), - 'is_existing_enrollment_audit': kwargs.get('is_existing_enrollment_audit', None), - } - - def test_default_enterprise_enrollment_intentions_missing_enterprise_uuid(self): - """ - Test expected response when successfully listing existing default enterprise enrollment intentions. - """ - self.set_jwt_cookie(ENTERPRISE_LEARNER_ROLE, str(self.enterprise_customer.uuid)) - response = self.client.get(f"{settings.TEST_SERVER}{DEFAULT_ENTERPRISE_ENROLLMENT_INTENTION_LIST_ENDPOINT}") - assert response.status_code == status.HTTP_400_BAD_REQUEST - assert response.json() == {'detail': 'enterprise_customer_uuid is a required query parameter.'} - - def test_default_enterprise_enrollment_intentions_invalid_enterprise_uuid(self): - """ - Test expected response when successfully listing existing default enterprise enrollment intentions. - """ - self.set_jwt_cookie(ENTERPRISE_LEARNER_ROLE, str(self.enterprise_customer.uuid)) - query_params = 'enterprise_customer_uuid=invalid-uuid' - response = self.client.get( - f"{settings.TEST_SERVER}{DEFAULT_ENTERPRISE_ENROLLMENT_INTENTION_LIST_ENDPOINT}?{query_params}" - ) - assert response.status_code == status.HTTP_400_BAD_REQUEST - assert response.json() == {'detail': 'enterprise_customer_uuid query parameter is not a valid UUID.'} - - @mock.patch('enterprise.content_metadata.api.EnterpriseCatalogApiClient') - def test_default_enterprise_enrollment_intentions_list(self, mock_catalog_api_client): - """ - Test expected response when successfully listing existing default enterprise enrollment intentions. - """ - self.set_jwt_cookie(ENTERPRISE_LEARNER_ROLE, str(self.enterprise_customer.uuid)) - enrollment_intention = create_mock_default_enterprise_enrollment_intention( - enterprise_customer=self.enterprise_customer, - mock_catalog_api_client=mock_catalog_api_client, - ) - query_params = f'enterprise_customer_uuid={str(self.enterprise_customer.uuid)}' - response = self.client.get( - f"{settings.TEST_SERVER}{DEFAULT_ENTERPRISE_ENROLLMENT_INTENTION_LIST_ENDPOINT}?{query_params}" - ) - assert response.status_code == status.HTTP_200_OK - response_data = response.json() - assert response_data['count'] == 1 - result = response_data['results'][0] - assert result['content_key'] == enrollment_intention.content_key - assert result['applicable_enterprise_catalog_uuids'] == [fake_catalog_api.FAKE_CATALOG_RESULT.get('uuid')] - - @mock.patch('enterprise.content_metadata.api.EnterpriseCatalogApiClient') - def test_default_enterprise_enrollment_intentions_detail(self, mock_catalog_api_client): - """ - Test expected response when unauthorized user attempts to list default - enterprise enrollment intentions. - """ - self.set_jwt_cookie(ENTERPRISE_LEARNER_ROLE, str(self.enterprise_customer.uuid)) - enrollment_intention = create_mock_default_enterprise_enrollment_intention( - enterprise_customer=self.enterprise_customer, - mock_catalog_api_client=mock_catalog_api_client, - ) - query_params = f'enterprise_customer_uuid={str(self.enterprise_customer.uuid)}' - base_url = get_default_enterprise_enrollment_intention_detail_endpoint(str(enrollment_intention.uuid)) - response = self.client.get(f"{settings.TEST_SERVER}{base_url}?{query_params}") - assert response.status_code == status.HTTP_200_OK - response_data = response.json() - assert response_data['content_key'] == enrollment_intention.content_key - assert response_data['applicable_enterprise_catalog_uuids'] == \ - [fake_catalog_api.FAKE_CATALOG_RESULT.get('uuid')] - - @mock.patch('enterprise.content_metadata.api.EnterpriseCatalogApiClient') - def test_default_enterprise_enrollment_intentions_list_unauthorized(self, mock_catalog_api_client): - """ - Test expected response when unauthorized user attempts to list default - enterprise enrollment intentions. - """ - self.set_jwt_cookie(ENTERPRISE_LEARNER_ROLE, str(self.enterprise_customer.uuid)) - create_mock_default_enterprise_enrollment_intention( - enterprise_customer=self.enterprise_customer, - mock_catalog_api_client=mock_catalog_api_client, - ) - query_params = f'enterprise_customer_uuid={str(uuid.uuid4())}' - response = self.client.get( - f"{settings.TEST_SERVER}{DEFAULT_ENTERPRISE_ENROLLMENT_INTENTION_LIST_ENDPOINT}?{query_params}" - ) - assert response.status_code == status.HTTP_200_OK - response_data = response.json() - assert response_data['count'] == 0 - assert response_data['results'] == [] - - @mock.patch('enterprise.content_metadata.api.EnterpriseCatalogApiClient') - def test_default_enterprise_enrollment_intentions_detail_403_forbidden(self, mock_catalog_api_client): - """ - Test expected response when unauthorized user attempts to list default - enterprise enrollment intentions. - """ - self.set_jwt_cookie(ENTERPRISE_LEARNER_ROLE, str(self.enterprise_customer.uuid)) - enrollment_intention = create_mock_default_enterprise_enrollment_intention( - enterprise_customer=self.enterprise_customer, - mock_catalog_api_client=mock_catalog_api_client, - ) - query_params = f'enterprise_customer_uuid={str(uuid.uuid4())}' - base_url = get_default_enterprise_enrollment_intention_detail_endpoint(str(enrollment_intention.uuid)) - response = self.client.get(f"{settings.TEST_SERVER}{base_url}?{query_params}") - assert response.status_code == status.HTTP_403_FORBIDDEN - - @mock.patch('enterprise.content_metadata.api.EnterpriseCatalogApiClient') - def test_default_enterprise_enrollment_intentions_not_in_catalog(self, mock_catalog_api_client): - """ - Test expected response when default enterprise enrollment intention is not in catalog. - """ - self.set_jwt_cookie(ENTERPRISE_LEARNER_ROLE, str(self.enterprise_customer.uuid)) - enrollment_intention = create_mock_default_enterprise_enrollment_intention( - enterprise_customer=self.enterprise_customer, - mock_catalog_api_client=mock_catalog_api_client, - contains_content_items=False, - catalog_list=[], - ) - query_params = f'enterprise_customer_uuid={str(self.enterprise_customer.uuid)}' - base_url = get_default_enterprise_enrollment_intention_detail_endpoint(str(enrollment_intention.uuid)) - response = self.client.get(f"{settings.TEST_SERVER}{base_url}?{query_params}") - assert response.status_code == status.HTTP_200_OK - response_data = response.json() - assert response_data['content_key'] == enrollment_intention.content_key - assert response_data['applicable_enterprise_catalog_uuids'] == [] - - def test_default_enterprise_enrollment_intentions_learner_status_not_linked(self): - """ - Test default enterprise enrollment intentions for specific learner not linked to enterprise customer. - """ - self.set_jwt_cookie(ENTERPRISE_LEARNER_ROLE, str(self.enterprise_customer.uuid)) - query_params = f'enterprise_customer_uuid={str(self.enterprise_customer.uuid)}' - response = self.client.get( - f"{settings.TEST_SERVER}{DEFAULT_ENTERPRISE_ENROLLMENT_INTENTION_LEARNER_STATUS_ENDPOINT}?{query_params}" - ) - assert response.status_code == status.HTTP_400_BAD_REQUEST - response_data = response.json() - assert response_data['detail'] == ( - f'User with lms_user_id {self.user.id} is not associated with ' - f'the enterprise customer {str(self.enterprise_customer.uuid)}.' - ) - - @mock.patch('enterprise.content_metadata.api.EnterpriseCatalogApiClient') - @mock.patch('enterprise.models.utils.get_best_mode_from_course_key') - def test_default_enterprise_enrollment_intentions_learner_status_enrollable( - self, - mock_get_best_mode_from_course_key, - mock_catalog_api_client, - ): - """ - Test default enterprise enrollment intentions for specific learner linked to enterprise customer, where - the course run associated with the default enrollment intention is enrollable. - """ - self.set_jwt_cookie(ENTERPRISE_LEARNER_ROLE, str(self.enterprise_customer.uuid)) - enrollment_intention = create_mock_default_enterprise_enrollment_intention( - enterprise_customer=self.enterprise_customer, - mock_catalog_api_client=mock_catalog_api_client, - ) - mock_get_best_mode_from_course_key.return_value = VERIFIED_COURSE_MODE - factories.EnterpriseCustomerUserFactory( - user_id=self.user.id, - enterprise_customer=self.enterprise_customer, - ) - query_params = f'enterprise_customer_uuid={str(self.enterprise_customer.uuid)}' - response = self.client.get( - f"{settings.TEST_SERVER}{DEFAULT_ENTERPRISE_ENROLLMENT_INTENTION_LEARNER_STATUS_ENDPOINT}?{query_params}" - ) - assert response.status_code == status.HTTP_200_OK - response_data = response.json() - assert response_data['lms_user_id'] == self.user.id - assert response_data['user_email'] == self.user.email - assert response_data['enrollment_statuses'] == { - 'needs_enrollment': { - 'enrollable': [ - self.get_default_enrollment_intention_with_learner_enrollment_state(enrollment_intention) - ], - 'not_enrollable': [], - }, - 'already_enrolled': [], - } - assert response_data['metadata'] == { - 'total_default_enterprise_enrollment_intentions': 1, - 'total_needs_enrollment': { - 'enrollable': 1, - 'not_enrollable': 0, - }, - 'total_already_enrolled': 0, - } - - @mock.patch('enterprise.content_metadata.api.EnterpriseCatalogApiClient') - @mock.patch('enterprise.models.utils.get_best_mode_from_course_key') - def test_default_enrollment_intentions_learner_status_content_not_enrollable( - self, - mock_get_best_mode_from_course_key, - mock_catalog_api_client, - ): - """ - Test default enterprise enrollment intentions (not enrollable) for - specific learner linked to enterprise customer. - """ - self.set_jwt_cookie(ENTERPRISE_LEARNER_ROLE, str(self.enterprise_customer.uuid)) - mock_course_run = fake_catalog_api.FAKE_COURSE_RUN.copy() - mock_course_run.update({'is_enrollable': False}) - mock_course = fake_catalog_api.FAKE_COURSE.copy() - mock_course.update({'course_runs': [mock_course_run]}) - enrollment_intention = create_mock_default_enterprise_enrollment_intention( - enterprise_customer=self.enterprise_customer, - mock_catalog_api_client=mock_catalog_api_client, - content_metadata=mock_course, - ) - mock_get_best_mode_from_course_key.return_value = VERIFIED_COURSE_MODE - factories.EnterpriseCustomerUserFactory( - user_id=self.user.id, - enterprise_customer=self.enterprise_customer, - ) - query_params = f'enterprise_customer_uuid={str(self.enterprise_customer.uuid)}' - response = self.client.get( - f"{settings.TEST_SERVER}{DEFAULT_ENTERPRISE_ENROLLMENT_INTENTION_LEARNER_STATUS_ENDPOINT}?{query_params}" - ) - assert response.status_code == status.HTTP_200_OK - response_data = response.json() - assert response_data['lms_user_id'] == self.user.id - assert response_data['user_email'] == self.user.email - assert response_data['enrollment_statuses'] == { - 'needs_enrollment': { - 'enrollable': [], - 'not_enrollable': [ - self.get_default_enrollment_intention_with_learner_enrollment_state( - enrollment_intention, - is_course_run_enrollable=False, - ) - ], - }, - 'already_enrolled': [], - } - assert response_data['metadata'] == { - 'total_default_enterprise_enrollment_intentions': 1, - 'total_needs_enrollment': { - 'enrollable': 0, - 'not_enrollable': 1, - }, - 'total_already_enrolled': 0, - } - - @mock.patch('enterprise.content_metadata.api.EnterpriseCatalogApiClient') - @mock.patch('enterprise.models.utils.get_best_mode_from_course_key') - def test_default_enrollment_intentions_learner_status_content_not_in_catalog( - self, - mock_get_best_mode_from_course_key, - mock_catalog_api_client, - ): - """ - Test default enterprise enrollment intentions (not enrollable, no applicable - catalog) for specific learner linked to enterprise customer. - """ - self.set_jwt_cookie(ENTERPRISE_LEARNER_ROLE, str(self.enterprise_customer.uuid)) - enrollment_intention = create_mock_default_enterprise_enrollment_intention( - enterprise_customer=self.enterprise_customer, - mock_catalog_api_client=mock_catalog_api_client, - contains_content_items=False, - catalog_list=[], - ) - mock_get_best_mode_from_course_key.return_value = VERIFIED_COURSE_MODE - factories.EnterpriseCustomerUserFactory( - user_id=self.user.id, - enterprise_customer=self.enterprise_customer, - ) - query_params = f'enterprise_customer_uuid={str(self.enterprise_customer.uuid)}' - response = self.client.get( - f"{settings.TEST_SERVER}{DEFAULT_ENTERPRISE_ENROLLMENT_INTENTION_LEARNER_STATUS_ENDPOINT}?{query_params}" - ) - assert response.status_code == status.HTTP_200_OK - response_data = response.json() - assert response_data['lms_user_id'] == self.user.id - assert response_data['user_email'] == self.user.email - assert response_data['enrollment_statuses'] == { - 'needs_enrollment': { - 'enrollable': [], - 'not_enrollable': [ - self.get_default_enrollment_intention_with_learner_enrollment_state( - enrollment_intention, - applicable_enterprise_catalog_uuids=[], - is_course_run_enrollable=True, - has_existing_enrollment=False, - is_existing_enrollment_active=None, - is_existing_enrollment_audit=None, - ) - ], - }, - 'already_enrolled': [], - } - assert response_data['metadata'] == { - 'total_default_enterprise_enrollment_intentions': 1, - 'total_needs_enrollment': { - 'enrollable': 0, - 'not_enrollable': 1, - }, - 'total_already_enrolled': 0, - } - - @mock.patch('enterprise.content_metadata.api.EnterpriseCatalogApiClient') - @mock.patch('enterprise.models.utils.get_best_mode_from_course_key') - @mock.patch.object(EnterpriseCourseEnrollment, 'course_enrollment', new_callable=mock.PropertyMock) - def test_default_enrollment_intentions_learner_status_already_enrolled_active( - self, - mock_course_enrollment, - mock_get_best_mode_from_course_key, - mock_catalog_api_client, - ): - """ - Test default enterprise enrollment intentions (already enrolled, active - enrollment) for specific learner linked to enterprise customer. - """ - self.set_jwt_cookie(ENTERPRISE_LEARNER_ROLE, str(self.enterprise_customer.uuid)) - enrollment_intention = create_mock_default_enterprise_enrollment_intention( - enterprise_customer=self.enterprise_customer, - mock_catalog_api_client=mock_catalog_api_client, - ) - mock_get_best_mode_from_course_key.return_value = VERIFIED_COURSE_MODE - enterprise_customer_user = factories.EnterpriseCustomerUserFactory( - user_id=self.user.id, - enterprise_customer=self.enterprise_customer, - ) - factories.EnterpriseCourseEnrollmentFactory( - enterprise_customer_user=enterprise_customer_user, - course_id=fake_catalog_api.FAKE_COURSE_RUN.get('key'), - ) - course_enrollment_kwargs = { - 'is_active': True, - 'mode': VERIFIED_COURSE_MODE, - } - mock_course_enrollment.return_value = mock.Mock(**course_enrollment_kwargs) - query_params = f'enterprise_customer_uuid={str(self.enterprise_customer.uuid)}' - response = self.client.get( - f"{settings.TEST_SERVER}{DEFAULT_ENTERPRISE_ENROLLMENT_INTENTION_LEARNER_STATUS_ENDPOINT}?{query_params}" - ) - assert response.status_code == status.HTTP_200_OK - response_data = response.json() - assert response_data['lms_user_id'] == self.user.id - assert response_data['user_email'] == self.user.email - assert response_data['enrollment_statuses'] == { - 'needs_enrollment': { - 'enrollable': [], - 'not_enrollable': [], - }, - 'already_enrolled': [ - self.get_default_enrollment_intention_with_learner_enrollment_state( - enrollment_intention, - has_existing_enrollment=True, - is_existing_enrollment_active=True, - is_existing_enrollment_audit=False, - ) - ], - } - assert response_data['metadata'] == { - 'total_default_enterprise_enrollment_intentions': 1, - 'total_needs_enrollment': { - 'enrollable': 0, - 'not_enrollable': 0, - }, - 'total_already_enrolled': 1, - } - - @mock.patch('enterprise.content_metadata.api.EnterpriseCatalogApiClient') - @mock.patch('enterprise.models.utils.get_best_mode_from_course_key') - @mock.patch.object(EnterpriseCourseEnrollment, 'course_enrollment', new_callable=mock.PropertyMock) - def test_default_enrollment_intentions_learner_status_already_enrolled_inactive( - self, - mock_course_enrollment, - mock_get_best_mode_from_course_key, - mock_catalog_api_client, - ): - """ - Test default enterprise enrollment intentions (already enrolled, inactive - enrollment) for specific learner linked to enterprise customer. - """ - self.set_jwt_cookie(ENTERPRISE_LEARNER_ROLE, str(self.enterprise_customer.uuid)) - enrollment_intention = create_mock_default_enterprise_enrollment_intention( - enterprise_customer=self.enterprise_customer, - mock_catalog_api_client=mock_catalog_api_client, - ) - mock_get_best_mode_from_course_key.return_value = VERIFIED_COURSE_MODE - enterprise_customer_user = factories.EnterpriseCustomerUserFactory( - user_id=self.user.id, - enterprise_customer=self.enterprise_customer, - ) - factories.EnterpriseCourseEnrollmentFactory( - enterprise_customer_user=enterprise_customer_user, - course_id=fake_catalog_api.FAKE_COURSE_RUN.get('key'), - ) - course_enrollment_kwargs = { - 'is_active': False, - 'mode': VERIFIED_COURSE_MODE, - } - mock_course_enrollment.return_value = mock.Mock(**course_enrollment_kwargs) - query_params = f'enterprise_customer_uuid={str(self.enterprise_customer.uuid)}' - response = self.client.get( - f"{settings.TEST_SERVER}{DEFAULT_ENTERPRISE_ENROLLMENT_INTENTION_LEARNER_STATUS_ENDPOINT}?{query_params}" - ) - assert response.status_code == status.HTTP_200_OK - response_data = response.json() - assert response_data['lms_user_id'] == self.user.id - assert response_data['user_email'] == self.user.email - assert response_data['enrollment_statuses'] == { - 'needs_enrollment': { - 'enrollable': [ - self.get_default_enrollment_intention_with_learner_enrollment_state( - enrollment_intention, - has_existing_enrollment=True, - is_existing_enrollment_active=False, - is_existing_enrollment_audit=False, - ) - ], - 'not_enrollable': [], - }, - 'already_enrolled': [], - } - assert response_data['metadata'] == { - 'total_default_enterprise_enrollment_intentions': 1, - 'total_needs_enrollment': { - 'enrollable': 1, - 'not_enrollable': 0, - }, - 'total_already_enrolled': 0, - } - - @ddt.data( - {'has_audit_mode_only': True}, - {'has_audit_mode_only': False}, - ) - @mock.patch('enterprise.content_metadata.api.EnterpriseCatalogApiClient') - @mock.patch('enterprise.models.utils.get_best_mode_from_course_key') - @mock.patch.object(EnterpriseCourseEnrollment, 'course_enrollment', new_callable=mock.PropertyMock) - @ddt.unpack - def test_default_enrollment_intentions_learner_status_already_enrolled_active_audit( - self, - mock_course_enrollment, - mock_get_best_mode_from_course_key, - mock_catalog_api_client, - has_audit_mode_only, - ): - """ - Test default enterprise enrollment intentions (already enrolled, active - audit enrollment) for specific learner linked to enterprise customer. - """ - self.set_jwt_cookie(ENTERPRISE_LEARNER_ROLE, str(self.enterprise_customer.uuid)) - enrollment_intention = create_mock_default_enterprise_enrollment_intention( - enterprise_customer=self.enterprise_customer, - mock_catalog_api_client=mock_catalog_api_client, - ) - - best_mode_for_course_run = AUDIT_COURSE_MODE if has_audit_mode_only else VERIFIED_COURSE_MODE - mock_get_best_mode_from_course_key.return_value = best_mode_for_course_run - - enterprise_customer_user = factories.EnterpriseCustomerUserFactory( - user_id=self.user.id, - enterprise_customer=self.enterprise_customer, - ) - factories.EnterpriseCourseEnrollmentFactory( - enterprise_customer_user=enterprise_customer_user, - course_id=fake_catalog_api.FAKE_COURSE_RUN.get('key'), - ) - course_enrollment_kwargs = { - 'is_active': True, - 'mode': AUDIT_COURSE_MODE, - } - mock_course_enrollment.return_value = mock.Mock(**course_enrollment_kwargs) - query_params = f'enterprise_customer_uuid={str(self.enterprise_customer.uuid)}' - response = self.client.get( - f"{settings.TEST_SERVER}{DEFAULT_ENTERPRISE_ENROLLMENT_INTENTION_LEARNER_STATUS_ENDPOINT}?{query_params}" - ) - assert response.status_code == status.HTTP_200_OK - response_data = response.json() - assert response_data['lms_user_id'] == self.user.id - assert response_data['user_email'] == self.user.email - - expected_enrollable = [] - expected_already_enrolled = [] - - expected_serialized_intention = self.get_default_enrollment_intention_with_learner_enrollment_state( - enrollment_intention, - has_existing_enrollment=True, - is_existing_enrollment_active=True, - is_existing_enrollment_audit=True, - best_mode_for_course_run=best_mode_for_course_run, - ) - - if has_audit_mode_only: - expected_already_enrolled.append(expected_serialized_intention) - else: - expected_enrollable.append(expected_serialized_intention) - - assert response_data['enrollment_statuses'] == { - 'needs_enrollment': { - 'enrollable': expected_enrollable, - 'not_enrollable': [], - }, - 'already_enrolled': expected_already_enrolled, - } - assert response_data['metadata'] == { - 'total_default_enterprise_enrollment_intentions': 1, - 'total_needs_enrollment': { - 'enrollable': len(expected_enrollable), - 'not_enrollable': 0, - }, - 'total_already_enrolled': len(expected_already_enrolled), - } - - @mock.patch('enterprise.content_metadata.api.EnterpriseCatalogApiClient') - @mock.patch('enterprise.models.utils.get_best_mode_from_course_key') - def test_default_enrollment_intentions_learner_status_staff_lms_user_id_override( - self, - mock_get_best_mode_from_course_key, - mock_catalog_api_client, - ): - """ - Test default enterprise enrollment intentions for staff user, requesting a specific user - linked to enterprise customer via lms_user_id query parameter. - """ - self.set_jwt_cookie(ENTERPRISE_LEARNER_ROLE, str(self.enterprise_customer.uuid)) - - # Create and login as a staff user - staff_user = self.create_user(username='staff_username', password=TEST_PASSWORD, is_staff=True) - self.client.login(username=staff_user.username, password=TEST_PASSWORD) - - enrollment_intention = create_mock_default_enterprise_enrollment_intention( - enterprise_customer=self.enterprise_customer, - mock_catalog_api_client=mock_catalog_api_client, - ) - mock_get_best_mode_from_course_key.return_value = VERIFIED_COURSE_MODE - factories.EnterpriseCustomerUserFactory( - user_id=self.user.id, - enterprise_customer=self.enterprise_customer, - ) - query_params = ( - f'enterprise_customer_uuid={str(self.enterprise_customer.uuid)}' - # Validates staff user can get back data for another user (i.e., request user is `staff_user`) - f'&lms_user_id={self.user.id}' - ) - response = self.client.get( - f"{settings.TEST_SERVER}{DEFAULT_ENTERPRISE_ENROLLMENT_INTENTION_LEARNER_STATUS_ENDPOINT}?{query_params}" - ) - assert response.status_code == status.HTTP_200_OK - response_data = response.json() - assert response_data['lms_user_id'] == self.user.id - assert response_data['user_email'] == self.user.email - assert response_data['enrollment_statuses'] == { - 'needs_enrollment': { - 'enrollable': [ - self.get_default_enrollment_intention_with_learner_enrollment_state(enrollment_intention) - ], - 'not_enrollable': [], - }, - 'already_enrolled': [], - } - assert response_data['metadata'] == { - 'total_default_enterprise_enrollment_intentions': 1, - 'total_needs_enrollment': { - 'enrollable': 1, - 'not_enrollable': 0, - }, - 'total_already_enrolled': 0, - } - - @mock.patch('enterprise.content_metadata.api.EnterpriseCatalogApiClient') - @mock.patch('enterprise.models.utils.get_best_mode_from_course_key') - def test_default_enrollment_intentions_learner_status_nonstaff_lms_user_id_override( - self, - mock_get_best_mode_from_course_key, - mock_catalog_api_client - ): - """ - Test default enterprise enrollment intentions for non-staff user linked to enterprise customer. - """ - self.set_jwt_cookie(ENTERPRISE_LEARNER_ROLE, str(self.enterprise_customer.uuid)) - enrollment_intention = create_mock_default_enterprise_enrollment_intention( - enterprise_customer=self.enterprise_customer, - mock_catalog_api_client=mock_catalog_api_client, - ) - mock_get_best_mode_from_course_key.return_value = VERIFIED_COURSE_MODE - factories.EnterpriseCustomerUserFactory( - user_id=self.user.id, - enterprise_customer=self.enterprise_customer, - ) - query_params = ( - f'enterprise_customer_uuid={str(self.enterprise_customer.uuid)}' - f'&lms_user_id={self.user.id + 1}' # Validates non-staff user can't get back data for another user - ) - response = self.client.get( - f"{settings.TEST_SERVER}{DEFAULT_ENTERPRISE_ENROLLMENT_INTENTION_LEARNER_STATUS_ENDPOINT}?{query_params}" - ) - - assert response.status_code == status.HTTP_200_OK - response_data = response.json() - assert response_data['lms_user_id'] == self.user.id - assert response_data['user_email'] == self.user.email - assert response_data['enrollment_statuses'] == { - 'needs_enrollment': { - 'enrollable': [ - self.get_default_enrollment_intention_with_learner_enrollment_state(enrollment_intention) - ], - 'not_enrollable': [], - }, - 'already_enrolled': [], - } - assert response_data['metadata'] == { - 'total_default_enterprise_enrollment_intentions': 1, - 'total_needs_enrollment': { - 'enrollable': 1, - 'not_enrollable': 0, - }, - 'total_already_enrolled': 0, - }