diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7a5838c58a..18e791ccbf 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -15,6 +15,18 @@ Change Log Unreleased ---------- +[4.3.1] +-------- +chore: use lms_update_or_create_enrollment without feature flag + +[4.3.0] +-------- +feat: Added the ``enable_career_engagement_network_on_learner_portal`` field for EnterpriseCustomer + +[4.2.0] +-------- +feat: create generic ``PaginationWithFeatureFlags`` to add a ``features`` property to DRF's default pagination response containing Waffle-based feature flags. +feat: integrate ``PaginationWithFeatureFlags`` with ``EnterpriseCustomerViewSet``. [4.1.15] -------- diff --git a/docs/decisions/0012-enterprise-feature-flags-waffle.rst b/docs/decisions/0012-enterprise-feature-flags-waffle.rst new file mode 100644 index 0000000000..9e2d7cd226 --- /dev/null +++ b/docs/decisions/0012-enterprise-feature-flags-waffle.rst @@ -0,0 +1,43 @@ +Waffle-based feature flags for Enterprise +========================================= + +Status +------ + +Accepted (September 2023) + +Context +------- + +Enterprise typically uses environment configuration to control feature flags for soft/dark releases. For micro-frontends, this control involves environment variables that are set at build time and used to compile JavaScript source or configuration settings derived from the runtime MFE configuration API in edx-platform. For backend APIs, this control typically either involves configuration settings or, on occasion, a Waffle flag. + +By solely relying on environment configuration, we are unable to dynamically control feature flags in production based on the user's context. + +For example, we may want to enable a feature for all staff users but keep it disabled for customers/users while it's in development. Similarly, we may want to enable a feature for a subset of specific users (e.g., members of a specific engineering squad) in production to QA before enabling it for all users. + +However, neither of these are really possible with environment configuration. + + +Decisions +--------- + +We will adopt the Waffle-based approach to feature flags for Enterprise micro-frontends in favor of environment variables or the MFE runtime configuration API. This approach will allow us to have more dynamic and granual control over feature flags in production based on the user's context (e.g., all staff users have a feature enabled, a subset of users have a feature enabled, etc.). + + +Consequences +------------ + +* We are introducing a third mechanism by which we control feature flags in the Enterprise micro-frontends. We may want to consider migrating other feature flags to this Waffle-based approach in the future. Similarly, such an exercise may be a good opportunity to revisit what feature flags exist today and what can be removed now that the associate features are stable in production. +* The majority of the feature flag setup for Enterprise systems lives in configuration settings. By moving more towards Waffle, the feature flag setup will live in databases and modified via Django Admin instead of via code changes. + + +Further Improvements +-------------------- + +* To further expand on the capabilities of Waffle-based feature flags, we may want to invest in the ability to enable such feature flags at the **enterprise customer** layer, where we may be able to enable soft-launch features for all users linked to an enterprise customer without needing to introduce new boolean fields on the ``EnterpriseCustomer`` model. + +Alternatives Considered +----------------------- + +* Continue using the environment variable to enabling feature flags like Enterprise has been doing the past few years. In order to test a disabled feature in production, this approach requires developers to allow the environment variable to be intentionally overridden by a `?feature=` query parameter in the URL. Without the query parameter in the URL, there is no alternative ways to temporarily enable the feature without impacting actual users and customers. Waffle-based feature flags give much more flexibility to developers to test features in production without impacting actual users and customers. +* Exposes a net-new API endpoint specific to returning feature flags for Enterprise needs. This approach was not adopted as it would require new API integrations within both the enterprise administrator and learner portal micro-frontends, requiring users to wait for additional network requests to resolve. Instead, we are preferring to include Waffle-based feature flags on existing API endpoints made by both micro-frontends. diff --git a/enterprise/__init__.py b/enterprise/__init__.py index 68827b1fe3..27abfebdc4 100644 --- a/enterprise/__init__.py +++ b/enterprise/__init__.py @@ -2,4 +2,4 @@ Your project description goes here. """ -__version__ = "4.1.15" +__version__ = "4.3.1" diff --git a/enterprise/admin/__init__.py b/enterprise/admin/__init__.py index f2dd11d9c6..2d3cc8baa7 100644 --- a/enterprise/admin/__init__.py +++ b/enterprise/admin/__init__.py @@ -226,7 +226,9 @@ class EnterpriseCustomerAdmin(DjangoObjectActions, SimpleHistoryAdmin): 'enable_integrated_customer_learner_portal_search', 'enable_analytics_screen', 'enable_audit_enrollment', 'enable_audit_data_reporting', 'enable_learner_portal_offers', - 'enable_executive_education_2U_fulfillment'), + 'enable_executive_education_2U_fulfillment', + 'enable_career_engagement_network_on_learner_portal', + 'career_engagement_network_message'), 'description': ('The following default settings should be the same for ' 'the majority of enterprise customers, ' 'and are either rarely used, unlikely to be sold, ' diff --git a/enterprise/admin/forms.py b/enterprise/admin/forms.py index 288a6b65d6..0d9474be54 100644 --- a/enterprise/admin/forms.py +++ b/enterprise/admin/forms.py @@ -403,6 +403,8 @@ class Meta: "enable_executive_education_2U_fulfillment", "hide_labor_market_data", "enable_integrated_customer_learner_portal_search", + "enable_career_engagement_network_on_learner_portal", + "career_engagement_network_message", "enable_analytics_screen", "enable_portal_reporting_config_screen", "enable_portal_saml_configuration_screen", diff --git a/enterprise/api/pagination.py b/enterprise/api/pagination.py index 3b12f1a69d..f8febfe01f 100644 --- a/enterprise/api/pagination.py +++ b/enterprise/api/pagination.py @@ -5,8 +5,39 @@ from collections import OrderedDict from urllib.parse import urlparse +from edx_rest_framework_extensions.paginators import DefaultPagination from rest_framework.response import Response +from enterprise.toggles import enterprise_features + + +class PaginationWithFeatureFlags(DefaultPagination): + """ + Adds a ``features`` dictionary to the default paginated response + provided by edx_rest_framework_extensions. The ``features`` dict + represents a collection of Waffle-based feature flags/samples/switches + that may be used to control whether certain aspects of the system are + enabled or disabled (e.g., feature flag turned on for all staff users but + not turned on for real customers/learners). + """ + + def get_paginated_response(self, data): + """ + Modifies the default paginated response to include ``enterprise_features`` dict. + + Arguments: + self: PaginationWithFeatureFlags instance. + data (dict): Results for current page. + + Returns: + (Response): DRF response object containing ``enterprise_features`` dict. + """ + paginated_response = super().get_paginated_response(data) + paginated_response.data.update({ + 'enterprise_features': enterprise_features(), + }) + return paginated_response + def get_paginated_response(data, request): """ diff --git a/enterprise/api/v1/serializers.py b/enterprise/api/v1/serializers.py index c7b83dbee2..36ecd555e0 100644 --- a/enterprise/api/v1/serializers.py +++ b/enterprise/api/v1/serializers.py @@ -220,7 +220,8 @@ class Meta: 'enable_integrated_customer_learner_portal_search', 'enable_generation_of_api_credentials', 'enable_portal_lms_configurations_screen', 'sender_alias', 'identity_providers', 'enterprise_customer_catalogs', 'reply_to', 'enterprise_notification_banner', 'hide_labor_market_data', - 'modified', 'enable_universal_link', 'enable_browse_and_request', 'admin_users' + 'modified', 'enable_universal_link', 'enable_browse_and_request', 'admin_users', + 'enable_career_engagement_network_on_learner_portal', 'career_engagement_network_message' ) identity_providers = EnterpriseCustomerIdentityProviderSerializer(many=True, read_only=True) diff --git a/enterprise/api/v1/views/enterprise_customer.py b/enterprise/api/v1/views/enterprise_customer.py index 097e17cca8..1685304d17 100644 --- a/enterprise/api/v1/views/enterprise_customer.py +++ b/enterprise/api/v1/views/enterprise_customer.py @@ -26,6 +26,7 @@ from enterprise import models from enterprise.api.filters import EnterpriseLinkedUserFilterBackend +from enterprise.api.pagination import PaginationWithFeatureFlags from enterprise.api.throttles import HighServiceUserThrottle from enterprise.api.v1 import serializers from enterprise.api.v1.decorators import require_at_least_one_query_parameter @@ -54,6 +55,7 @@ class EnterpriseCustomerViewSet(EnterpriseReadWriteModelViewSet): queryset = models.EnterpriseCustomer.active_customers.all() serializer_class = serializers.EnterpriseCustomerSerializer filter_backends = EnterpriseReadWriteModelViewSet.filter_backends + (EnterpriseLinkedUserFilterBackend,) + pagination_class = PaginationWithFeatureFlags USER_ID_FILTER = 'enterprise_customer_users__user_id' FIELDS = ( diff --git a/enterprise/migrations/0185_auto_20230921_1007.py b/enterprise/migrations/0185_auto_20230921_1007.py new file mode 100644 index 0000000000..5a190399f9 --- /dev/null +++ b/enterprise/migrations/0185_auto_20230921_1007.py @@ -0,0 +1,33 @@ +# Generated by Django 3.2.21 on 2023-09-21 10:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('enterprise', '0184_auto_20230914_2057'), + ] + + operations = [ + migrations.AddField( + model_name='enterprisecustomer', + name='career_engagement_network_message', + field=models.TextField(blank=True, help_text='Message text shown on the learner portal dashboard for career engagement network.'), + ), + migrations.AddField( + model_name='enterprisecustomer', + name='enable_career_engagement_network_on_learner_portal', + field=models.BooleanField(default=False, help_text='If checked, the learners will be able to see the link to CEN on the learner portal dashboard.', verbose_name='Allow navigation to career engagement network from learner portal dashboard'), + ), + migrations.AddField( + model_name='historicalenterprisecustomer', + name='career_engagement_network_message', + field=models.TextField(blank=True, help_text='Message text shown on the learner portal dashboard for career engagement network.'), + ), + migrations.AddField( + model_name='historicalenterprisecustomer', + name='enable_career_engagement_network_on_learner_portal', + field=models.BooleanField(default=False, help_text='If checked, the learners will be able to see the link to CEN on the learner portal dashboard.', verbose_name='Allow navigation to career engagement network from learner portal dashboard'), + ), + ] diff --git a/enterprise/models.py b/enterprise/models.py index 59ddc0bf69..9803282c6b 100644 --- a/enterprise/models.py +++ b/enterprise/models.py @@ -366,6 +366,14 @@ class Meta: ) ) + enable_career_engagement_network_on_learner_portal = models.BooleanField( + verbose_name="Allow navigation to career engagement network from learner portal dashboard", + default=False, + help_text=_( + "If checked, the learners will be able to see the link to CEN on the learner portal dashboard." + ) + ) + enable_analytics_screen = models.BooleanField( verbose_name="Display analytics page", default=True, @@ -445,6 +453,13 @@ class Meta: default=False, ) + career_engagement_network_message = models.TextField( + blank=True, + help_text=_( + 'Message text shown on the learner portal dashboard for career engagement network.' + ), + ) + @property def enterprise_customer_identity_provider(self): """ diff --git a/enterprise/toggles.py b/enterprise/toggles.py new file mode 100644 index 0000000000..70b00233b3 --- /dev/null +++ b/enterprise/toggles.py @@ -0,0 +1,36 @@ +""" +Waffle toggles for enterprise features within the LMS. +""" + +from edx_toggles.toggles import WaffleFlag + +ENTERPRISE_NAMESPACE = 'enterprise' +ENTERPRISE_LOG_PREFIX = 'Enterprise: ' + +# .. toggle_name: enterprise.TOP_DOWN_ASSIGNMENT_REAL_TIME_LCM +# .. toggle_implementation: WaffleFlag +# .. toggle_default: False +# .. toggle_description: Enables top-down assignment +# .. toggle_use_cases: open_edx +# .. toggle_creation_date: 2023-09-15 +TOP_DOWN_ASSIGNMENT_REAL_TIME_LCM = WaffleFlag( + f'{ENTERPRISE_NAMESPACE}.top_down_assignment_real_time_lcm', + __name__, + ENTERPRISE_LOG_PREFIX, +) + + +def top_down_assignment_real_time_lcm(): + """ + Returns whether top-down assignment and real time LCM feature flag is enabled. + """ + return TOP_DOWN_ASSIGNMENT_REAL_TIME_LCM.is_enabled() + + +def enterprise_features(): + """ + Returns a dict of enterprise Waffle-based feature flags. + """ + return { + 'top_down_assignment_real_time_lcm': top_down_assignment_real_time_lcm(), + } diff --git a/enterprise/utils.py b/enterprise/utils.py index 2ca8e4ffa5..c278f9a4fa 100644 --- a/enterprise/utils.py +++ b/enterprise/utils.py @@ -46,12 +46,8 @@ from enterprise.logging import getEnterpriseLogger try: - from openedx.features.enterprise_support.enrollments.utils import ( - lms_enroll_user_in_course, - lms_update_or_create_enrollment, - ) + from openedx.features.enterprise_support.enrollments.utils import lms_update_or_create_enrollment except ImportError: - lms_enroll_user_in_course = None lms_update_or_create_enrollment = None try: @@ -1824,30 +1820,16 @@ def customer_admin_enroll_user_with_status( succeeded = False new_enrollment = False enterprise_fulfillment_source_uuid = None - emet_enable_auto_upgrade_enrollment_mode = getattr( - settings, - 'ENABLE_ENTERPRISE_BACKEND_EMET_AUTO_UPGRADE_ENROLLMENT_MODE', - False, - ) try: # enrolls a user in a course per LMS flow, but this method doesn't create enterprise records # yet so we need to create it immediately after calling lms_update_or_create_enrollment. - if emet_enable_auto_upgrade_enrollment_mode: - new_enrollment = lms_update_or_create_enrollment( - user.username, - course_id, - course_mode, - is_active=True, - enterprise_uuid=enterprise_customer.uuid, - ) - else: - new_enrollment = lms_enroll_user_in_course( - user.username, - course_id, - course_mode, - enterprise_customer.uuid, - is_active=True, - ) + new_enrollment = lms_update_or_create_enrollment( + user.username, + course_id, + course_mode, + is_active=True, + enterprise_uuid=enterprise_customer.uuid, + ) succeeded = True LOGGER.info("Successfully enrolled user %s in course %s", user.id, course_id) except (CourseEnrollmentError, CourseUserGroup.DoesNotExist) as error: diff --git a/requirements/base.in b/requirements/base.in index 6cda181f55..b54f17646e 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -26,8 +26,9 @@ edx-django-utils>=3.12.0 edx-drf-extensions edx-opaque-keys[django] edx-rest-api-client -edx-tincan-py35 edx-rbac +edx-tincan-py35 +edx-toggles jsondiff jsonfield path.py diff --git a/requirements/ci.txt b/requirements/ci.txt index db667cb2e9..cca733262c 100644 --- a/requirements/ci.txt +++ b/requirements/ci.txt @@ -6,7 +6,7 @@ # distlib==0.3.7 # via virtualenv -filelock==3.12.3 +filelock==3.12.4 # via # tox # virtualenv @@ -30,7 +30,5 @@ tox==3.28.0 # tox-battery tox-battery==0.6.1 # via -r requirements/ci.in -typing-extensions==4.7.1 - # via filelock virtualenv==20.24.5 # via tox diff --git a/requirements/common_constraints.txt b/requirements/common_constraints.txt index 08e94f34dd..601b0ae550 100644 --- a/requirements/common_constraints.txt +++ b/requirements/common_constraints.txt @@ -1,4 +1,7 @@ + + + # A central location for most common version constraints # (across edx repos) for pip-installation. # diff --git a/requirements/dev.txt b/requirements/dev.txt index 651ca9ee59..f1c86e6f3a 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -95,7 +95,6 @@ bleach==6.0.0 # -r requirements/test-master.txt # -r requirements/test.txt build==1.0.3 - # readme-renderer # via pip-tools celery==5.3.4 # via @@ -170,6 +169,7 @@ code-annotations==1.5.0 # -r requirements/test-master.txt # -r requirements/test.txt # edx-lint + # edx-toggles coverage[toml]==7.3.1 # via # -r requirements/test.txt @@ -180,6 +180,7 @@ cryptography==38.0.4 # -r requirements/test-master.txt # -r requirements/test.txt # django-fernet-fields-v2 + # jwcrypto # pgpy # pyjwt # pyopenssl @@ -192,6 +193,12 @@ defusedxml==0.7.1 # -r requirements/test-master.txt # -r requirements/test.txt # djangorestframework-xml +deprecated==1.2.14 + # via + # -r requirements/doc.txt + # -r requirements/test-master.txt + # -r requirements/test.txt + # jwcrypto diff-cover==7.7.0 # via -r requirements/test.txt dill==0.3.7 @@ -218,6 +225,7 @@ django==3.2.21 # edx-drf-extensions # edx-i18n-tools # edx-rbac + # edx-toggles # jsonfield django-cache-memoize==0.1.10 # via @@ -241,6 +249,7 @@ django-crum==0.7.9 # -r requirements/test.txt # edx-django-utils # edx-rbac + # edx-toggles django-fernet-fields-v2==0.9 # via # -r requirements/doc.txt @@ -251,7 +260,7 @@ django-filter==23.2 # -r requirements/doc.txt # -r requirements/test-master.txt # -r requirements/test.txt -django-ipware==4.0.2 +django-ipware==5.0.0 # via # -r requirements/doc.txt # -r requirements/test-master.txt @@ -267,12 +276,12 @@ django-multi-email-field==0.7.0 # -r requirements/doc.txt # -r requirements/test-master.txt # -r requirements/test.txt -django-oauth-toolkit==1.4.1 +django-oauth-toolkit==1.5.0 # via # -r requirements/doc.txt # -r requirements/test-master.txt # -r requirements/test.txt -django-object-actions==4.1.0 +django-object-actions==4.2.0 # via # -r requirements/doc.txt # -r requirements/test-master.txt @@ -290,6 +299,7 @@ django-waffle==4.0.0 # -r requirements/test.txt # edx-django-utils # edx-drf-extensions + # edx-toggles djangorestframework==3.14.0 # via # -r requirements/doc.txt @@ -332,13 +342,14 @@ edx-django-utils==5.7.0 # django-config-models # edx-drf-extensions # edx-rest-api-client + # edx-toggles edx-drf-extensions==8.9.2 # via # -r requirements/doc.txt # -r requirements/test-master.txt # -r requirements/test.txt # edx-rbac -edx-i18n-tools==1.1.0 +edx-i18n-tools==1.2.0 # via -r requirements/dev.in edx-lint==5.3.4 # via -r requirements/dev.in @@ -363,6 +374,11 @@ edx-tincan-py35==1.0.0 # -r requirements/doc.txt # -r requirements/test-master.txt # -r requirements/test.txt +edx-toggles==5.1.0 + # via + # -r requirements/doc.txt + # -r requirements/test-master.txt + # -r requirements/test.txt factory-boy==3.3.0 # via # -c requirements/constraints.txt @@ -436,6 +452,12 @@ jsonfield==3.1.0 # -r requirements/doc.txt # -r requirements/test-master.txt # -r requirements/test.txt +jwcrypto==1.5.0 + # via + # -r requirements/doc.txt + # -r requirements/test-master.txt + # -r requirements/test.txt + # django-oauth-toolkit kombu==5.3.2 # via # -r requirements/doc.txt @@ -588,7 +610,7 @@ pycryptodomex==3.18.0 # -r requirements/test-master.txt # -r requirements/test.txt # snowflake-connector-python -pydata-sphinx-theme==0.13.3 +pydata-sphinx-theme==0.14.0 # via # -r requirements/doc.txt # sphinx-book-theme @@ -734,6 +756,7 @@ six==1.16.0 # -r requirements/test-master.txt # -r requirements/test.txt # bleach + # django-oauth-toolkit # edx-drf-extensions # edx-lint # edx-rbac @@ -753,7 +776,7 @@ snowballstemmer==2.2.0 # -r requirements/doc.txt # pydocstyle # sphinx -snowflake-connector-python==3.1.1 +snowflake-connector-python==3.2.0 # via # -r requirements/doc.txt # -r requirements/test-master.txt @@ -927,7 +950,12 @@ wheel==0.41.2 # -r requirements/dev.in # pip-tools wrapt==1.15.0 - # via astroid + # via + # -r requirements/doc.txt + # -r requirements/test-master.txt + # -r requirements/test.txt + # astroid + # deprecated yarl==1.9.2 # via # -r requirements/doc.txt diff --git a/requirements/doc.txt b/requirements/doc.txt index cc8d5326f7..79578958ea 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -60,7 +60,6 @@ billiard==4.1.0 # celery bleach==6.0.0 # via -r requirements/test-master.txt - # readme-renderer celery==5.3.4 # via # -c requirements/constraints.txt @@ -104,11 +103,14 @@ click-repl==0.3.0 # -r requirements/test-master.txt # celery code-annotations==1.5.0 - # via -r requirements/test-master.txt + # via + # -r requirements/test-master.txt + # edx-toggles cryptography==38.0.4 # via # -r requirements/test-master.txt # django-fernet-fields-v2 + # jwcrypto # pgpy # pyjwt # pyopenssl @@ -117,6 +119,10 @@ defusedxml==0.7.1 # via # -r requirements/test-master.txt # djangorestframework-xml +deprecated==1.2.14 + # via + # -r requirements/test-master.txt + # jwcrypto django==3.2.21 # via # -c requirements/common_constraints.txt @@ -134,6 +140,7 @@ django==3.2.21 # edx-django-utils # edx-drf-extensions # edx-rbac + # edx-toggles # jsonfield django-cache-memoize==0.1.10 # via -r requirements/test-master.txt @@ -146,11 +153,12 @@ django-crum==0.7.9 # -r requirements/test-master.txt # edx-django-utils # edx-rbac + # edx-toggles django-fernet-fields-v2==0.9 # via -r requirements/test-master.txt django-filter==23.2 # via -r requirements/test-master.txt -django-ipware==4.0.2 +django-ipware==5.0.0 # via -r requirements/test-master.txt django-model-utils==4.3.1 # via @@ -158,9 +166,9 @@ django-model-utils==4.3.1 # edx-rbac django-multi-email-field==0.7.0 # via -r requirements/test-master.txt -django-oauth-toolkit==1.4.1 +django-oauth-toolkit==1.5.0 # via -r requirements/test-master.txt -django-object-actions==4.1.0 +django-object-actions==4.2.0 # via -r requirements/test-master.txt django-simple-history==3.1.1 # via @@ -171,6 +179,7 @@ django-waffle==4.0.0 # -r requirements/test-master.txt # edx-django-utils # edx-drf-extensions + # edx-toggles djangorestframework==3.14.0 # via # -r requirements/test-master.txt @@ -201,6 +210,7 @@ edx-django-utils==5.7.0 # django-config-models # edx-drf-extensions # edx-rest-api-client + # edx-toggles edx-drf-extensions==8.9.2 # via # -r requirements/test-master.txt @@ -215,6 +225,8 @@ edx-rest-api-client==5.6.0 # via -r requirements/test-master.txt edx-tincan-py35==1.0.0 # via -r requirements/test-master.txt +edx-toggles==5.1.0 + # via -r requirements/test-master.txt factory-boy==3.3.0 # via # -c requirements/constraints.txt @@ -251,6 +263,10 @@ jsondiff==2.0.0 # via -r requirements/test-master.txt jsonfield==3.1.0 # via -r requirements/test-master.txt +jwcrypto==1.5.0 + # via + # -r requirements/test-master.txt + # django-oauth-toolkit kombu==5.3.2 # via # -r requirements/test-master.txt @@ -329,7 +345,7 @@ pycryptodomex==3.18.0 # via # -r requirements/test-master.txt # snowflake-connector-python -pydata-sphinx-theme==0.13.3 +pydata-sphinx-theme==0.14.0 # via sphinx-book-theme pygments==2.16.1 # via @@ -407,6 +423,7 @@ six==1.16.0 # via # -r requirements/test-master.txt # bleach + # django-oauth-toolkit # edx-drf-extensions # edx-rbac # python-dateutil @@ -416,7 +433,7 @@ slumber==0.7.1 # edx-rest-api-client snowballstemmer==2.2.0 # via sphinx -snowflake-connector-python==3.1.1 +snowflake-connector-python==3.2.0 # via -r requirements/test-master.txt sortedcontainers==2.4.0 # via @@ -509,6 +526,10 @@ webencodings==0.5.1 # via # -r requirements/test-master.txt # bleach +wrapt==1.15.0 + # via + # -r requirements/test-master.txt + # deprecated yarl==1.9.2 # via # -r requirements/test-master.txt diff --git a/requirements/edx-platform-constraints.txt b/requirements/edx-platform-constraints.txt index 09f089352e..3ac9c705d4 100644 --- a/requirements/edx-platform-constraints.txt +++ b/requirements/edx-platform-constraints.txt @@ -282,9 +282,8 @@ django-filter==23.2 # edx-enterprise # lti-consumer-xblock # openedx-blockstore -django-ipware==4.0.2 +django-ipware==5.0.0 # via - # -c requirements/edx/../constraints.txt # -r requirements/edx/kernel.in # edx-enterprise # edx-proctoring @@ -317,12 +316,12 @@ django-mptt==0.14.0 django-multi-email-field==0.7.0 django-mysql==4.11.0 # via -r requirements/edx/kernel.in -django-oauth-toolkit==1.4.1 +django-oauth-toolkit==1.5.0 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/kernel.in # edx-enterprise -django-object-actions==4.1.0 +django-object-actions==4.2.0 django-pipeline==2.1.0 # via -r requirements/edx/kernel.in django-ratelimit==4.1.0 @@ -334,7 +333,6 @@ django-sekizai==4.1.0 django-ses==3.5.0 # via -r requirements/edx/bundled.in # via - # -c requirements/edx/../constraints.txt # -r requirements/edx/kernel.in # edx-enterprise # edx-name-affirmation @@ -418,7 +416,9 @@ edx-auth-backends==4.2.0 # -r requirements/edx/kernel.in # openedx-blockstore edx-braze-client==0.1.7 - # via -r requirements/edx/bundled.in + # via + # -r requirements/edx/bundled.in + # edx-enterprise edx-bulk-grades==1.0.2 # via # -r requirements/edx/kernel.in @@ -471,7 +471,7 @@ edx-drf-extensions==8.9.2 # edx-when # edxval # openedx-learning -edx-enterprise==4.1.11 +edx-enterprise==4.1.14 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/kernel.in @@ -479,7 +479,7 @@ edx-event-bus-kafka==5.4.0 # via -r requirements/edx/kernel.in edx-event-bus-redis==0.3.1 # via -r requirements/edx/kernel.in -edx-i18n-tools==1.1.0 +edx-i18n-tools==1.2.0 # via ora2 edx-milestones==0.5.0 # via -r requirements/edx/kernel.in @@ -497,7 +497,6 @@ edx-opaque-keys[django]==2.5.0 # edx-milestones # edx-organizations # edx-proctoring - # edx-user-state-client # edx-when # lti-consumer-xblock # openedx-events @@ -518,7 +517,7 @@ edx-rest-api-client==5.6.0 # edx-proctoring edx-search==3.6.0 # via -r requirements/edx/kernel.in -edx-sga==0.22.0 +edx-sga==0.23.0 # via -r requirements/edx/bundled.in edx-submissions==3.6.0 # via @@ -537,8 +536,6 @@ edx-toggles==5.1.0 # ora2 edx-token-utils==0.2.1 # via -r requirements/edx/kernel.in -edx-user-state-client==1.3.2 - # via -r requirements/edx/kernel.in edx-when==2.4.0 # via # -r requirements/edx/kernel.in @@ -558,7 +555,7 @@ event-tracking==2.2.0 # -r requirements/edx/kernel.in # edx-proctoring # edx-search -fastavro==1.8.2 +fastavro==1.8.3 # via openedx-events filelock==3.12.3 # via snowflake-connector-python @@ -640,7 +637,9 @@ jsonschema==4.19.0 jsonschema-specifications==2023.7.1 # via jsonschema jwcrypto==1.5.0 - # via pylti1p3 + # via + # django-oauth-toolkit + # pylti1p3 # via celery laboratory==1.0.2 # via -r requirements/edx/kernel.in @@ -740,6 +739,8 @@ oauthlib==3.2.2 olxcleaner==0.2.1 # via -r requirements/edx/kernel.in openai==0.28.0 +openedx-atlas==0.5.0 + # via -r requirements/edx/kernel.in openedx-blockstore==1.4.0 # via -r requirements/edx/kernel.in openedx-calc==3.0.1 @@ -752,8 +753,9 @@ openedx-django-require==2.1.0 # via -r requirements/edx/kernel.in openedx-django-wiki==2.0.1 # via -r requirements/edx/kernel.in -openedx-events==8.6.0 +openedx-events==8.5.0 # via + # -c requirements/edx/../constraints.txt # -r requirements/edx/kernel.in # edx-event-bus-kafka # edx-event-bus-redis @@ -767,7 +769,7 @@ openedx-mongodbproxy==0.2.0 # via -r requirements/edx/kernel.in optimizely-sdk==4.1.1 # via -r requirements/edx/bundled.in -ora2==5.3.0 +ora2==5.4.0 # via -r requirements/edx/bundled.in oscrypto==1.3.0 # via snowflake-connector-python @@ -950,9 +952,8 @@ random2==1.0.1 # via -r requirements/edx/kernel.in recommender-xblock==2.0.1 # via -r requirements/edx/bundled.in -redis==4.6.0 +redis==5.0.0 # via - # -c requirements/edx/../constraints.txt # -r requirements/edx/kernel.in # walrus referencing==0.30.2 @@ -1030,6 +1031,7 @@ six==1.16.0 # chem # codejail-includes # crowdsourcehinter-xblock + # django-oauth-toolkit # edx-ace # edx-auth-backends # edx-ccx-keys @@ -1057,7 +1059,7 @@ slumber==0.7.1 # edx-bulk-grades # edx-enterprise # edx-rest-api-client -snowflake-connector-python==3.1.1 +snowflake-connector-python==3.2.0 social-auth-app-django==5.0.0 # via # -c requirements/edx/../constraints.txt @@ -1183,7 +1185,6 @@ xblock[django]==1.7.0 # done-xblock # edx-completion # edx-sga - # edx-user-state-client # edx-when # lti-consumer-xblock # ora2 diff --git a/requirements/js_test.txt b/requirements/js_test.txt index b1710d25c6..ec64f8fc82 100644 --- a/requirements/js_test.txt +++ b/requirements/js_test.txt @@ -95,7 +95,7 @@ trio==0.22.2 # trio-websocket trio-websocket==0.10.4 # via selenium -typing-extensions==4.7.1 +typing-extensions==4.8.0 # via # annotated-types # inflect diff --git a/requirements/test-master.txt b/requirements/test-master.txt index 9d1eecd265..64a5d89599 100644 --- a/requirements/test-master.txt +++ b/requirements/test-master.txt @@ -88,11 +88,13 @@ code-annotations==1.5.0 # via # -c requirements/edx-platform-constraints.txt # -r requirements/base.in + # edx-toggles cryptography==38.0.4 # via # -c requirements/edx-platform-constraints.txt # -r requirements/base.in # django-fernet-fields-v2 + # jwcrypto # pgpy # pyjwt # pyopenssl @@ -101,6 +103,10 @@ defusedxml==0.7.1 # via # -c requirements/edx-platform-constraints.txt # djangorestframework-xml +deprecated==1.2.14 + # via + # -c requirements/edx-platform-constraints.txt + # jwcrypto django==3.2.21 # via # -c requirements/common_constraints.txt @@ -119,6 +125,7 @@ django==3.2.21 # edx-django-utils # edx-drf-extensions # edx-rbac + # edx-toggles # jsonfield django-cache-memoize==0.1.10 # via @@ -138,6 +145,7 @@ django-crum==0.7.9 # -r requirements/base.in # edx-django-utils # edx-rbac + # edx-toggles django-fernet-fields-v2==0.9 # via # -c requirements/edx-platform-constraints.txt @@ -146,7 +154,7 @@ django-filter==23.2 # via # -c requirements/edx-platform-constraints.txt # -r requirements/base.in -django-ipware==4.0.2 +django-ipware==5.0.0 # via # -c requirements/edx-platform-constraints.txt # -r requirements/base.in @@ -159,11 +167,11 @@ django-multi-email-field==0.7.0 # via # -c requirements/edx-platform-constraints.txt # -r requirements/base.in -django-oauth-toolkit==1.4.1 +django-oauth-toolkit==1.5.0 # via # -c requirements/edx-platform-constraints.txt # -r requirements/base.in -django-object-actions==4.1.0 +django-object-actions==4.2.0 # via # -c requirements/edx-platform-constraints.txt # -r requirements/base.in @@ -177,6 +185,7 @@ django-waffle==4.0.0 # -r requirements/base.in # edx-django-utils # edx-drf-extensions + # edx-toggles djangorestframework==3.14.0 # via # -c requirements/edx-platform-constraints.txt @@ -203,6 +212,7 @@ edx-django-utils==5.7.0 # django-config-models # edx-drf-extensions # edx-rest-api-client + # edx-toggles edx-drf-extensions==8.9.2 # via # -c requirements/edx-platform-constraints.txt @@ -225,6 +235,10 @@ edx-tincan-py35==1.0.0 # via # -c requirements/edx-platform-constraints.txt # -r requirements/base.in +edx-toggles==5.1.0 + # via + # -c requirements/edx-platform-constraints.txt + # -r requirements/base.in filelock==3.12.3 # via # -c requirements/edx-platform-constraints.txt @@ -252,6 +266,10 @@ jsonfield==3.1.0 # via # -c requirements/edx-platform-constraints.txt # -r requirements/base.in +jwcrypto==1.5.0 + # via + # -c requirements/edx-platform-constraints.txt + # django-oauth-toolkit kombu==5.3.2 # via celery markupsafe==2.1.3 @@ -388,6 +406,7 @@ six==1.16.0 # via # -c requirements/edx-platform-constraints.txt # bleach + # django-oauth-toolkit # edx-drf-extensions # edx-rbac # python-dateutil @@ -396,7 +415,7 @@ slumber==0.7.1 # -c requirements/edx-platform-constraints.txt # -r requirements/base.in # edx-rest-api-client -snowflake-connector-python==3.1.1 +snowflake-connector-python==3.2.0 # via # -c requirements/edx-platform-constraints.txt # -r requirements/base.in @@ -467,6 +486,10 @@ webencodings==0.5.1 # via # -c requirements/edx-platform-constraints.txt # bleach +wrapt==1.15.0 + # via + # -c requirements/edx-platform-constraints.txt + # deprecated yarl==1.9.2 # via # -c requirements/edx-platform-constraints.txt diff --git a/requirements/test.txt b/requirements/test.txt index 8e60e5b1ee..1d80e77236 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -90,13 +90,16 @@ click-plugins==1.1.1 # -r requirements/test-master.txt # celery code-annotations==1.5.0 - # via -r requirements/test-master.txt + # via + # -r requirements/test-master.txt + # edx-toggles coverage[toml]==7.3.1 # via pytest-cov cryptography==38.0.4 # via # -r requirements/test-master.txt # django-fernet-fields-v2 + # jwcrypto # pgpy # pyjwt # pyopenssl @@ -107,6 +110,10 @@ defusedxml==0.7.1 # via # -r requirements/test-master.txt # djangorestframework-xml +deprecated==1.2.14 + # via + # -r requirements/test-master.txt + # jwcrypto diff-cover==7.7.0 # via -r requirements/test.in # via @@ -125,6 +132,7 @@ diff-cover==7.7.0 # edx-django-utils # edx-drf-extensions # edx-rbac + # edx-toggles # jsonfield django-cache-memoize==0.1.10 # via -r requirements/test-master.txt @@ -137,11 +145,12 @@ django-crum==0.7.9 # -r requirements/test-master.txt # edx-django-utils # edx-rbac + # edx-toggles django-fernet-fields-v2==0.9 # via -r requirements/test-master.txt django-filter==23.2 # via -r requirements/test-master.txt -django-ipware==4.0.2 +django-ipware==5.0.0 # via -r requirements/test-master.txt django-model-utils==4.3.1 # via @@ -150,9 +159,9 @@ django-model-utils==4.3.1 # edx-rbac django-multi-email-field==0.7.0 # via -r requirements/test-master.txt -django-oauth-toolkit==1.4.1 +django-oauth-toolkit==1.5.0 # via -r requirements/test-master.txt -django-object-actions==4.1.0 +django-object-actions==4.2.0 # via -r requirements/test-master.txt django-simple-history==3.1.1 # via @@ -163,6 +172,7 @@ django-waffle==4.0.0 # -r requirements/test-master.txt # edx-django-utils # edx-drf-extensions + # edx-toggles djangorestframework==3.14.0 # via # -r requirements/test-master.txt @@ -183,6 +193,7 @@ edx-django-utils==5.7.0 # django-config-models # edx-drf-extensions # edx-rest-api-client + # edx-toggles edx-drf-extensions==8.9.2 # via # -r requirements/test-master.txt @@ -197,6 +208,8 @@ edx-rest-api-client==5.6.0 # via -r requirements/test-master.txt edx-tincan-py35==1.0.0 # via -r requirements/test-master.txt +edx-toggles==5.1.0 + # via -r requirements/test-master.txt factory-boy==3.3.0 # via # -c requirements/constraints.txt @@ -233,6 +246,10 @@ jsondiff==2.0.0 # via -r requirements/test-master.txt jsonfield==3.1.0 # via -r requirements/test-master.txt +jwcrypto==1.5.0 + # via + # -r requirements/test-master.txt + # django-oauth-toolkit # via # -r requirements/test-master.txt # celery @@ -387,6 +404,7 @@ six==1.16.0 # via # -r requirements/test-master.txt # bleach + # django-oauth-toolkit # edx-drf-extensions # edx-rbac # freezegun @@ -397,7 +415,7 @@ slumber==0.7.1 # via # -r requirements/test-master.txt # edx-rest-api-client -snowflake-connector-python==3.1.1 +snowflake-connector-python==3.2.0 # via -r requirements/test-master.txt sortedcontainers==2.4.0 # via @@ -468,6 +486,10 @@ webencodings==0.5.1 # via # -r requirements/test-master.txt # bleach +wrapt==1.15.0 + # via + # -r requirements/test-master.txt + # deprecated yarl==1.9.2 # via # -r requirements/test-master.txt diff --git a/test_utils/factories.py b/test_utils/factories.py index 23d610f5d8..1d6998d92c 100644 --- a/test_utils/factories.py +++ b/test_utils/factories.py @@ -122,6 +122,7 @@ class Meta: hide_labor_market_data = False auth_org_id = factory.LazyAttribute(lambda x: FAKER.lexify(text='??????????')) enable_generation_of_api_credentials = False + career_engagement_network_message = 'Test message' class EnrollmentNotificationEmailTemplateFactory(factory.django.DjangoModelFactory): diff --git a/tests/test_enterprise/api/test_filters.py b/tests/test_enterprise/api/test_filters.py index c4a35d1510..b37b90790c 100644 --- a/tests/test_enterprise/api/test_filters.py +++ b/tests/test_enterprise/api/test_filters.py @@ -223,7 +223,19 @@ def test_filter(self, is_staff, is_linked_to_enterprise, has_access): for key, value in self.enterprise_customer_data.items(): assert enterprise_customer_response[key] == value else: - assert response == {'count': 0, 'next': None, 'previous': None, 'results': []} + mock_empty_200_success_response = { + 'next': None, + 'previous': None, + 'count': 0, + 'num_pages': 1, + 'current_page': 1, + 'start': 0, + 'results': [], + 'enterprise_features': { + 'top_down_assignment_real_time_lcm': False + } + } + assert response == mock_empty_200_success_response @ddt.ddt diff --git a/tests/test_enterprise/api/test_views.py b/tests/test_enterprise/api/test_views.py index 72a6cb7254..5de69468b6 100644 --- a/tests/test_enterprise/api/test_views.py +++ b/tests/test_enterprise/api/test_views.py @@ -15,6 +15,7 @@ import ddt import responses +from edx_toggles.toggles.testutils import override_waffle_flag from faker import Faker from oauth2_provider.models import get_application_model from pytest import mark, raises @@ -57,6 +58,7 @@ PendingEnrollment, PendingEnterpriseCustomerUser, ) +from enterprise.toggles import TOP_DOWN_ASSIGNMENT_REAL_TIME_LCM from enterprise.utils import ( NotConnectedToOpenEdX, get_sso_orchestrator_api_base_url, @@ -1135,6 +1137,7 @@ class TestEnterpriseCustomerViewSet(BaseTestEnterpriseAPIViews): """ Test enterprise customer view set. """ + @ddt.data( ( factories.EnterpriseCustomerFactory, @@ -1177,6 +1180,7 @@ class TestEnterpriseCustomerViewSet(BaseTestEnterpriseAPIViews): 'hide_course_original_price': False, 'enable_analytics_screen': True, 'enable_integrated_customer_learner_portal_search': True, + 'enable_career_engagement_network_on_learner_portal': False, 'enable_portal_lms_configurations_screen': False, 'sender_alias': 'Test Sender Alias', 'identity_providers': [], @@ -1189,6 +1193,7 @@ class TestEnterpriseCustomerViewSet(BaseTestEnterpriseAPIViews): 'enable_browse_and_request': False, 'admin_users': [], 'enable_generation_of_api_credentials': False, + 'career_engagement_network_message': 'Test message', }], ), ( @@ -1234,6 +1239,7 @@ class TestEnterpriseCustomerViewSet(BaseTestEnterpriseAPIViews): 'enable_portal_subscription_management_screen': False, 'hide_course_original_price': False, 'enable_analytics_screen': True, 'enable_integrated_customer_learner_portal_search': True, + 'enable_career_engagement_network_on_learner_portal': False, 'enable_portal_lms_configurations_screen': False, 'sender_alias': 'Test Sender Alias', 'identity_providers': [], 'enterprise_customer_catalogs': [], 'reply_to': 'fake_reply@example.com', @@ -1242,6 +1248,7 @@ class TestEnterpriseCustomerViewSet(BaseTestEnterpriseAPIViews): 'enable_universal_link': False, 'enable_browse_and_request': False, 'admin_users': [], 'enable_generation_of_api_credentials': False, + 'career_engagement_network_message': 'Test message', }, 'active': True, 'user_id': 0, 'user': None, 'data_sharing_consent_records': [], 'groups': [], @@ -1310,6 +1317,7 @@ class TestEnterpriseCustomerViewSet(BaseTestEnterpriseAPIViews): 'hide_course_original_price': False, 'enable_analytics_screen': True, 'enable_integrated_customer_learner_portal_search': True, + 'enable_career_engagement_network_on_learner_portal': False, 'enable_portal_lms_configurations_screen': False, 'sender_alias': 'Test Sender Alias', 'identity_providers': [ @@ -1327,6 +1335,7 @@ class TestEnterpriseCustomerViewSet(BaseTestEnterpriseAPIViews): 'enable_browse_and_request': False, 'admin_users': [], 'enable_generation_of_api_credentials': False, + 'career_engagement_network_message': 'Test message', }], ), ( @@ -1376,6 +1385,7 @@ class TestEnterpriseCustomerViewSet(BaseTestEnterpriseAPIViews): 'hide_course_original_price': False, 'enable_analytics_screen': True, 'enable_integrated_customer_learner_portal_search': True, + 'enable_career_engagement_network_on_learner_portal': False, 'enable_portal_lms_configurations_screen': False, 'sender_alias': 'Test Sender Alias', 'identity_providers': [], @@ -1388,6 +1398,7 @@ class TestEnterpriseCustomerViewSet(BaseTestEnterpriseAPIViews): 'enable_browse_and_request': False, 'admin_users': [], 'enable_generation_of_api_credentials': False, + 'career_engagement_network_message': 'Test message', }], ), ( @@ -1463,50 +1474,58 @@ def test_enterprise_customer_basic_list(self): @ddt.data( # Request missing required permissions query param. - (True, False, [], {}, False, {'detail': 'User is not allowed to access the view.'}), + (True, False, [], {}, False, {'detail': 'User is not allowed to access the view.'}, 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.'}), + {'detail': 'User is not allowed to access the view.'}, False), # Staff user that does have the specified group permission. (True, False, ['enterprise_enrollment_api_access'], {'permissions': ['enterprise_enrollment_api_access']}, - True, None), + True, None, 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.'}), + {'detail': 'User is not allowed to access the view.'}, 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, {'count': 0, 'next': None, 'previous': None, 'results': []}), + False, None, 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.'}), + {'detail': 'User is not allowed to access the view.'}, 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), + True, None, 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), + {'permissions': ['enterprise_enrollment_api_access', 'enterprise_data_api_access']}, True, None, 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, - {'count': 0, 'next': None, 'previous': None, 'results': []}), + None, 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), + {'permissions': ['enterprise_enrollment_api_access'], 'enterprise_id': FAKE_UUIDS[0]}, True, + None, 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, - {'count': 0, 'next': None, 'previous': None, 'results': []}), + None, 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), + {'permissions': ['enterprise_enrollment_api_access'], 'search': 'test'}, True, + None, 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), + {'permissions': ['enterprise_enrollment_api_access'], 'slug': TEST_SLUG}, True, + None, 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, - {'count': 0, 'next': None, 'previous': None, 'results': []}), + None, False), + # Staff user with group permission filtering on slug with results, with + # top down assignment & real-time LCM feature enabled + (True, False, ['enterprise_enrollment_api_access'], + {'permissions': ['enterprise_enrollment_api_access'], 'slug': TEST_SLUG}, True, + None, True), ) @ddt.unpack @mock.patch('enterprise.utils.get_logo_url') @@ -1518,6 +1537,7 @@ def test_enterprise_customer_with_access_to( query_params, has_access_to_enterprise, expected_error, + is_top_down_assignment_real_time_lcm_enabled, mock_get_logo_url, ): """ @@ -1563,10 +1583,13 @@ def test_enterprise_customer_with_access_to( client = APIClient() client.login(username='test_user', password='test_password') - - response = client.get(settings.TEST_SERVER + - ENTERPRISE_CUSTOMER_WITH_ACCESS_TO_ENDPOINT + - '?' + urlencode(query_params, True)) + with override_waffle_flag( + TOP_DOWN_ASSIGNMENT_REAL_TIME_LCM, + active=is_top_down_assignment_real_time_lcm_enabled + ): + response = client.get( + f"{settings.TEST_SERVER}{ENTERPRISE_CUSTOMER_WITH_ACCESS_TO_ENDPOINT}?{urlencode(query_params, True)}" + ) response = self.load_json(response.content) if has_access_to_enterprise: assert response['results'][0] == { @@ -1596,6 +1619,7 @@ def test_enterprise_customer_with_access_to( 'hide_course_original_price': False, 'enable_analytics_screen': False, 'enable_integrated_customer_learner_portal_search': True, + 'enable_career_engagement_network_on_learner_portal': False, 'enable_portal_lms_configurations_screen': False, 'sender_alias': 'Test Sender Alias', 'identity_providers': [], @@ -1608,9 +1632,22 @@ def test_enterprise_customer_with_access_to( 'enable_browse_and_request': False, 'admin_users': [], 'enable_generation_of_api_credentials': False, + 'career_engagement_network_message': 'Test message', } else: - assert response == expected_error + mock_empty_200_success_response = { + 'next': None, + 'previous': None, + 'count': 0, + 'num_pages': 1, + 'current_page': 1, + 'start': 0, + 'results': [], + 'enterprise_features': { + 'top_down_assignment_real_time_lcm': is_top_down_assignment_real_time_lcm_enabled, + } + } + assert response in (expected_error, mock_empty_200_success_response) def test_enterprise_customer_branding_detail(self): """ @@ -4421,12 +4458,8 @@ def test_bulk_enrollment_in_bulk_courses_pending_licenses( @mock.patch('enterprise.api.v1.views.enterprise_customer.track_enrollment') @mock.patch('enterprise.models.EnterpriseCustomer.notify_enrolled_learners') @mock.patch('enterprise.utils.lms_update_or_create_enrollment') - @mock.patch('enterprise.utils.lms_enroll_user_in_course') - @ddt.data(True, False) def test_bulk_enrollment_in_bulk_courses_existing_users( self, - setting_value, - mock_enroll_user_in_course, mock_update_or_create_enrollment, mock_notify_task, mock_track_enroll, @@ -4437,84 +4470,76 @@ def test_bulk_enrollment_in_bulk_courses_existing_users( This tests the case where existing users are supplied, so the enrollments are fulfilled rather than pending. """ - if setting_value: - mock_customer_admin_enroll_user = mock_update_or_create_enrollment - else: - mock_customer_admin_enroll_user = mock_enroll_user_in_course + mock_update_or_create_enrollment.return_value = True - with override_settings(ENABLE_ENTERPRISE_BACKEND_EMET_AUTO_UPGRADE_ENROLLMENT_MODE=setting_value): - mock_customer_admin_enroll_user.return_value = True + user_one = factories.UserFactory(is_active=True) + user_two = factories.UserFactory(is_active=True) - user_one = factories.UserFactory(is_active=True) - user_two = factories.UserFactory(is_active=True) + factories.EnterpriseCustomerFactory( + uuid=FAKE_UUIDS[0], + name="test_enterprise" + ) - factories.EnterpriseCustomerFactory( - uuid=FAKE_UUIDS[0], - name="test_enterprise" - ) + permission = Permission.objects.get(name='Can add Enterprise Customer') + self.user.user_permissions.add(permission) + mock_get_course_mode.return_value = VERIFIED_SUBSCRIPTION_COURSE_MODE - permission = Permission.objects.get(name='Can add Enterprise Customer') - self.user.user_permissions.add(permission) - mock_get_course_mode.return_value = VERIFIED_SUBSCRIPTION_COURSE_MODE + self.assertEqual(len(PendingEnrollment.objects.all()), 0) + body = { + 'enrollments_info': [ + { + 'user_id': user_one.id, + 'course_run_key': 'course-v1:edX+DemoX+Demo_Course', + 'license_uuid': '5a88bdcade7c4ecb838f8111b68e18ac' + }, + { + 'email': user_two.email, + 'course_run_key': 'course-v1:edX+DemoX+Demo_Course', + 'license_uuid': '2c58acdade7c4ede838f7111b42e18ac' + }, + ] + } + response = self.client.post( + settings.TEST_SERVER + ENTERPRISE_CUSTOMER_BULK_ENROLL_LEARNERS_IN_COURSES_ENDPOINT, + data=json.dumps(body), + content_type='application/json', + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + response_json = response.json() + self.assertEqual({ + 'successes': [ + { + 'user_id': user_one.id, + 'email': user_one.email, + 'course_run_key': 'course-v1:edX+DemoX+Demo_Course', + 'created': True, + 'activation_link': None, + 'enterprise_fulfillment_source_uuid': str(EnterpriseCourseEnrollment.objects.filter( + enterprise_customer_user__user_id=user_one.id + ).first().licensedenterprisecourseenrollment_enrollment_fulfillment.uuid) + }, + { + 'user_id': user_two.id, + 'email': user_two.email, + 'course_run_key': 'course-v1:edX+DemoX+Demo_Course', + 'created': True, + 'activation_link': None, + 'enterprise_fulfillment_source_uuid': str(EnterpriseCourseEnrollment.objects.filter( + enterprise_customer_user__user_id=user_two.id + ).first().licensedenterprisecourseenrollment_enrollment_fulfillment.uuid) + }, + ], + 'pending': [], + 'failures': [], + }, response_json) + self.assertEqual(len(EnterpriseCourseEnrollment.objects.all()), 2) + # no notifications to be sent unless 'notify' specifically asked for in payload + mock_notify_task.assert_not_called() + mock_track_enroll.assert_has_calls([ + mock.call(PATHWAY_CUSTOMER_ADMIN_ENROLLMENT, 1, 'course-v1:edX+DemoX+Demo_Course'), + ]) - self.assertEqual(len(PendingEnrollment.objects.all()), 0) - body = { - 'enrollments_info': [ - { - 'user_id': user_one.id, - 'course_run_key': 'course-v1:edX+DemoX+Demo_Course', - 'license_uuid': '5a88bdcade7c4ecb838f8111b68e18ac' - }, - { - 'email': user_two.email, - 'course_run_key': 'course-v1:edX+DemoX+Demo_Course', - 'license_uuid': '2c58acdade7c4ede838f7111b42e18ac' - }, - ] - } - response = self.client.post( - settings.TEST_SERVER + ENTERPRISE_CUSTOMER_BULK_ENROLL_LEARNERS_IN_COURSES_ENDPOINT, - data=json.dumps(body), - content_type='application/json', - ) - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - response_json = response.json() - self.assertEqual({ - 'successes': [ - { - 'user_id': user_one.id, - 'email': user_one.email, - 'course_run_key': 'course-v1:edX+DemoX+Demo_Course', - 'created': True, - 'activation_link': None, - 'enterprise_fulfillment_source_uuid': str(EnterpriseCourseEnrollment.objects.filter( - enterprise_customer_user__user_id=user_one.id - ).first().licensedenterprisecourseenrollment_enrollment_fulfillment.uuid) - }, - { - 'user_id': user_two.id, - 'email': user_two.email, - 'course_run_key': 'course-v1:edX+DemoX+Demo_Course', - 'created': True, - 'activation_link': None, - 'enterprise_fulfillment_source_uuid': str(EnterpriseCourseEnrollment.objects.filter( - enterprise_customer_user__user_id=user_two.id - ).first().licensedenterprisecourseenrollment_enrollment_fulfillment.uuid) - }, - ], - 'pending': [], - 'failures': [], - }, response_json) - self.assertEqual(len(EnterpriseCourseEnrollment.objects.all()), 2) - # no notifications to be sent unless 'notify' specifically asked for in payload - mock_notify_task.assert_not_called() - mock_track_enroll.assert_has_calls([ - mock.call(PATHWAY_CUSTOMER_ADMIN_ENROLLMENT, 1, 'course-v1:edX+DemoX+Demo_Course'), - ]) - if setting_value: - assert mock_update_or_create_enrollment.call_count == 2 - else: - assert mock_enroll_user_in_course.call_count == 2 + assert mock_update_or_create_enrollment.call_count == 2 @mock.patch('enterprise.api.v1.views.enterprise_customer.get_best_mode_from_course_key') @mock.patch('enterprise.api.v1.views.enterprise_customer.track_enrollment') @@ -4581,12 +4606,10 @@ def test_bulk_enrollment_in_bulk_courses_nonexisting_user_id( { 'old_transaction_id': FAKE_UUIDS[4], 'new_transaction_id': FAKE_UUIDS[4], - 'setting_value': True, }, { 'old_transaction_id': str(uuid.uuid4()), 'new_transaction_id': str(uuid.uuid4()), - 'setting_value': False, }, ) @ddt.unpack @@ -4595,100 +4618,92 @@ def test_bulk_enrollment_in_bulk_courses_nonexisting_user_id( 'enterprise.api.v1.views.enterprise_customer.get_best_mode_from_course_key' ) @mock.patch('enterprise.utils.lms_update_or_create_enrollment') - @mock.patch('enterprise.utils.lms_enroll_user_in_course') def test_bulk_enrollment_enroll_after_cancel( self, mock_platform_enrollment, mock_get_course_mode, mock_update_or_create_enrollment, - mock_enroll_user_in_course, old_transaction_id, new_transaction_id, - setting_value, ): """ Test that even after a cancelled enterprise enrollment, an attempt to re-enroll the same learner in content results in expected state and payload. """ - if setting_value: - mock_enrollment_api = mock_update_or_create_enrollment - else: - mock_enrollment_api = mock_enroll_user_in_course - with override_settings(ENABLE_ENTERPRISE_BACKEND_EMET_AUTO_UPGRADE_ENROLLMENT_MODE=setting_value): - mock_platform_enrollment.return_value = True - mock_get_course_mode.return_value = VERIFIED_SUBSCRIPTION_COURSE_MODE - # Needed for the cancel endpoint: - mock_enrollment_api.update_enrollment.return_value = mock.Mock() - - user, enterprise_user, enterprise_customer = \ - self._create_user_and_enterprise_customer('abc@test.com', 'test_password') - permission = Permission.objects.get(name='Can add Enterprise Customer') - user.user_permissions.add(permission) - - course_id = 'course-v1:edX+DemoX+Demo_Course' - enterprise_course_enrollment = factories.EnterpriseCourseEnrollmentFactory( - enterprise_customer_user=enterprise_user, - course_id=course_id, - ) - learner_credit_course_enrollment = factories.LearnerCreditEnterpriseCourseEnrollmentFactory( - enterprise_course_enrollment=enterprise_course_enrollment, - transaction_id=old_transaction_id, - ) - learner_credit_fulfillment_url = reverse( - 'enterprise-subsidy-fulfillment', - kwargs={'fulfillment_source_uuid': str(learner_credit_course_enrollment.uuid)} - ) - cancel_url = learner_credit_fulfillment_url + '/cancel-fulfillment' - enrollment_url = reverse( - 'enterprise-customer-enroll-learners-in-courses', - (str(enterprise_customer.uuid),) + mock_platform_enrollment.return_value = True + mock_get_course_mode.return_value = VERIFIED_SUBSCRIPTION_COURSE_MODE + # Needed for the cancel endpoint: + mock_update_or_create_enrollment.update_enrollment.return_value = mock.Mock() + + user, enterprise_user, enterprise_customer = \ + self._create_user_and_enterprise_customer('abc@test.com', 'test_password') + permission = Permission.objects.get(name='Can add Enterprise Customer') + user.user_permissions.add(permission) + + course_id = 'course-v1:edX+DemoX+Demo_Course' + enterprise_course_enrollment = factories.EnterpriseCourseEnrollmentFactory( + enterprise_customer_user=enterprise_user, + course_id=course_id, + ) + learner_credit_course_enrollment = factories.LearnerCreditEnterpriseCourseEnrollmentFactory( + enterprise_course_enrollment=enterprise_course_enrollment, + transaction_id=old_transaction_id, + ) + learner_credit_fulfillment_url = reverse( + 'enterprise-subsidy-fulfillment', + kwargs={'fulfillment_source_uuid': str(learner_credit_course_enrollment.uuid)} + ) + cancel_url = learner_credit_fulfillment_url + '/cancel-fulfillment' + enrollment_url = reverse( + 'enterprise-customer-enroll-learners-in-courses', + (str(enterprise_customer.uuid),) + ) + enroll_body = { + 'notify': 'true', + 'enrollments_info': [ + { + 'email': user.email, + 'course_run_key': course_id, + 'transaction_id': new_transaction_id, + }, + ] + } + with mock.patch('enterprise.api.v1.views.enterprise_customer.track_enrollment'): + with mock.patch("enterprise.models.EnterpriseCustomer.notify_enrolled_learners"): + cancel_response = self.client.post(settings.TEST_SERVER + cancel_url) + with LogCapture(level=logging.WARNING) as warn_logs: + second_enroll_response = self.client.post( + settings.TEST_SERVER + enrollment_url, + data=json.dumps(enroll_body), + content_type='application/json', + ) + + assert cancel_response.status_code == status.HTTP_200_OK + assert second_enroll_response.status_code == status.HTTP_201_CREATED + + if old_transaction_id == new_transaction_id: + assert any( + 'using the same transaction_id as before' + in log_record.getMessage() for log_record in warn_logs.records ) - enroll_body = { - 'notify': 'true', - 'enrollments_info': [ - { - 'email': user.email, - 'course_run_key': course_id, - 'transaction_id': new_transaction_id, - }, - ] - } - with mock.patch('enterprise.api.v1.views.enterprise_customer.track_enrollment'): - with mock.patch("enterprise.models.EnterpriseCustomer.notify_enrolled_learners"): - cancel_response = self.client.post(settings.TEST_SERVER + cancel_url) - with LogCapture(level=logging.WARNING) as warn_logs: - second_enroll_response = self.client.post( - settings.TEST_SERVER + enrollment_url, - data=json.dumps(enroll_body), - content_type='application/json', - ) - - assert cancel_response.status_code == status.HTTP_200_OK - assert second_enroll_response.status_code == status.HTTP_201_CREATED - - if old_transaction_id == new_transaction_id: - assert any( - 'using the same transaction_id as before' - in log_record.getMessage() for log_record in warn_logs.records - ) - # First, check that the bulk enrollment response looks good: - response_json = second_enroll_response.json() - assert len(response_json.get('successes')) == 1 - assert response_json['successes'][0]['user_id'] == user.id - assert response_json['successes'][0]['email'] == user.email - assert response_json['successes'][0]['course_run_key'] == course_id - assert response_json['successes'][0]['created'] is True - assert uuid.UUID(response_json['successes'][0]['enterprise_fulfillment_source_uuid']) == \ - learner_credit_course_enrollment.uuid - - # Then, check that the db records related to the enrollment look good: - enterprise_course_enrollment.refresh_from_db() - learner_credit_course_enrollment.refresh_from_db() - assert enterprise_course_enrollment.unenrolled_at is None - assert enterprise_course_enrollment.saved_for_later is False - assert learner_credit_course_enrollment.is_revoked is False - assert learner_credit_course_enrollment.transaction_id == uuid.UUID(new_transaction_id) + # First, check that the bulk enrollment response looks good: + response_json = second_enroll_response.json() + assert len(response_json.get('successes')) == 1 + assert response_json['successes'][0]['user_id'] == user.id + assert response_json['successes'][0]['email'] == user.email + assert response_json['successes'][0]['course_run_key'] == course_id + assert response_json['successes'][0]['created'] is True + assert uuid.UUID(response_json['successes'][0]['enterprise_fulfillment_source_uuid']) == \ + learner_credit_course_enrollment.uuid + + # Then, check that the db records related to the enrollment look good: + enterprise_course_enrollment.refresh_from_db() + learner_credit_course_enrollment.refresh_from_db() + assert enterprise_course_enrollment.unenrolled_at is None + assert enterprise_course_enrollment.saved_for_later is False + assert learner_credit_course_enrollment.is_revoked is False + assert learner_credit_course_enrollment.transaction_id == uuid.UUID(new_transaction_id) @ddt.data( { @@ -4703,7 +4718,6 @@ def test_bulk_enrollment_enroll_after_cancel( ] }, 'fulfillment_source': LearnerCreditEnterpriseCourseEnrollment, - 'setting_value': True, }, { 'body': { @@ -4717,19 +4731,15 @@ def test_bulk_enrollment_enroll_after_cancel( ] }, 'fulfillment_source': LicensedEnterpriseCourseEnrollment, - 'setting_value': False, }, ) @ddt.unpack @mock.patch('enterprise.api.v1.views.enterprise_customer.get_best_mode_from_course_key') @mock.patch('enterprise.utils.lms_update_or_create_enrollment') - @mock.patch('enterprise.utils.lms_enroll_user_in_course') def test_bulk_enrollment_includes_fulfillment_source_uuid( self, mock_get_course_mode, mock_update_or_create_enrollment, - mock_enroll_user_in_course, - setting_value, body, fulfillment_source, ): @@ -4737,41 +4747,36 @@ def test_bulk_enrollment_includes_fulfillment_source_uuid( Test that a successful bulk enrollment call to generate subsidy based enrollment records will return the newly generated subsidized enrollment uuid value as part of the response payload. """ - if setting_value: - mock_platform_enrollment = mock_update_or_create_enrollment - else: - mock_platform_enrollment = mock_enroll_user_in_course - with override_settings(ENABLE_ENTERPRISE_BACKEND_EMET_AUTO_UPGRADE_ENROLLMENT_MODE=setting_value): - mock_platform_enrollment.return_value = True + mock_update_or_create_enrollment.return_value = True - user, _, enterprise_customer = self._create_user_and_enterprise_customer( - body.get('enrollments_info')[0].get('email'), 'test_password' - ) + user, _, enterprise_customer = self._create_user_and_enterprise_customer( + body.get('enrollments_info')[0].get('email'), 'test_password' + ) - permission = Permission.objects.get(name='Can add Enterprise Customer') - user.user_permissions.add(permission) - mock_get_course_mode.return_value = VERIFIED_SUBSCRIPTION_COURSE_MODE + permission = Permission.objects.get(name='Can add Enterprise Customer') + user.user_permissions.add(permission) + mock_get_course_mode.return_value = VERIFIED_SUBSCRIPTION_COURSE_MODE - enrollment_url = reverse( - 'enterprise-customer-enroll-learners-in-courses', - (str(enterprise_customer.uuid),) - ) - with mock.patch('enterprise.api.v1.views.enterprise_customer.track_enrollment'): - with mock.patch("enterprise.models.EnterpriseCustomer.notify_enrolled_learners"): - response = self.client.post( - settings.TEST_SERVER + enrollment_url, - data=json.dumps(body), - content_type='application/json', - ) + enrollment_url = reverse( + 'enterprise-customer-enroll-learners-in-courses', + (str(enterprise_customer.uuid),) + ) + with mock.patch('enterprise.api.v1.views.enterprise_customer.track_enrollment'): + with mock.patch("enterprise.models.EnterpriseCustomer.notify_enrolled_learners"): + response = self.client.post( + settings.TEST_SERVER + enrollment_url, + data=json.dumps(body), + content_type='application/json', + ) - self.assertEqual(response.status_code, 201) + self.assertEqual(response.status_code, 201) - response_json = response.json() - self.assertEqual(len(response_json.get('successes')), 1) - self.assertEqual( - str(fulfillment_source.objects.first().uuid), - response_json.get('successes')[0].get('enterprise_fulfillment_source_uuid') - ) + response_json = response.json() + self.assertEqual(len(response_json.get('successes')), 1) + self.assertEqual( + str(fulfillment_source.objects.first().uuid), + response_json.get('successes')[0].get('enterprise_fulfillment_source_uuid') + ) @ddt.data( { diff --git a/tests/test_enterprise/test_utils.py b/tests/test_enterprise/test_utils.py index 21ddfe0c34..d98921e6c0 100644 --- a/tests/test_enterprise/test_utils.py +++ b/tests/test_enterprise/test_utils.py @@ -11,7 +11,6 @@ from django.conf import settings from django.forms.models import model_to_dict -from django.test import override_settings from enterprise.models import EnterpriseCourseEnrollment, LicensedEnterpriseCourseEnrollment from enterprise.utils import ( @@ -101,12 +100,8 @@ def test_get_platform_logo_url(self, logo_url, expected_logo_url, mock_get_logo_ @mock.patch('enterprise.utils.CourseEnrollmentError', new_callable=lambda: StubException) @mock.patch('enterprise.utils.CourseUserGroup', new_callable=lambda: StubModel) @mock.patch('enterprise.utils.lms_update_or_create_enrollment') - @mock.patch('enterprise.utils.lms_enroll_user_in_course') - @ddt.data(True, False) def test_enroll_subsidy_users_in_courses_fails( self, - setting_value, - mock_enroll_user_in_course, mock_update_or_create_enrollment, mock_model, mock_error, @@ -115,50 +110,40 @@ def test_enroll_subsidy_users_in_courses_fails( Test that `enroll_subsidy_users_in_courses` properly handles failure cases where something goes wrong with the user enrollment. """ - if setting_value: - mock_customer_admin_enroll_user_with_status = mock_update_or_create_enrollment - else: - mock_customer_admin_enroll_user_with_status = mock_enroll_user_in_course - - with override_settings(ENABLE_ENTERPRISE_BACKEND_EMET_AUTO_UPGRADE_ENROLLMENT_MODE=setting_value): - self.create_user() - ent_customer = factories.EnterpriseCustomerFactory( - uuid=FAKE_UUIDS[0], - name="test_enterprise" - ) - mock_model.DoesNotExist = Exception - mock_customer_admin_enroll_user_with_status.side_effect = [mock_error('mocked error')] - licensed_users_info = [{ - 'email': self.user.email, - 'course_run_key': 'course-key-v1', - 'course_mode': 'verified', - 'license_uuid': '5b77bdbade7b4fcb838f8111b68e18ae' - }] - result = enroll_subsidy_users_in_courses(ent_customer, licensed_users_info) - self.assertEqual( - { - "successes": [], - "failures": [ - { - "user_id": self.user.id, - "email": self.user.email, - "course_run_key": "course-key-v1", - } - ], - "pending": [], - }, - result, - ) + self.create_user() + ent_customer = factories.EnterpriseCustomerFactory( + uuid=FAKE_UUIDS[0], + name="test_enterprise" + ) + mock_model.DoesNotExist = Exception + mock_update_or_create_enrollment.side_effect = [mock_error('mocked error')] + licensed_users_info = [{ + 'email': self.user.email, + 'course_run_key': 'course-key-v1', + 'course_mode': 'verified', + 'license_uuid': '5b77bdbade7b4fcb838f8111b68e18ae' + }] + result = enroll_subsidy_users_in_courses(ent_customer, licensed_users_info) + self.assertEqual( + { + "successes": [], + "failures": [ + { + "user_id": self.user.id, + "email": self.user.email, + "course_run_key": "course-key-v1", + } + ], + "pending": [], + }, + result, + ) @mock.patch('enterprise.utils.CourseEnrollmentError', new_callable=lambda: StubException) @mock.patch('enterprise.utils.CourseUserGroup', new_callable=lambda: StubModel) @mock.patch('enterprise.utils.lms_update_or_create_enrollment') - @mock.patch('enterprise.utils.lms_enroll_user_in_course') - @ddt.data(True, False) def test_enroll_subsidy_users_in_courses_partially_fails( self, - setting_value, - mock_enroll_user_in_course, mock_update_or_create_enrollment, mock_model, mock_error, @@ -167,294 +152,262 @@ def test_enroll_subsidy_users_in_courses_partially_fails( Test that `enroll_subsidy_users_in_courses` properly handles partial failure states and still creates enrollments for the users that succeed. """ - if setting_value: - mock_customer_admin_enroll_user_with_status = mock_update_or_create_enrollment - else: - mock_customer_admin_enroll_user_with_status = mock_enroll_user_in_course - with override_settings(ENABLE_ENTERPRISE_BACKEND_EMET_AUTO_UPGRADE_ENROLLMENT_MODE=setting_value): - self.create_user() - failure_user = factories.UserFactory() - - ent_customer = factories.EnterpriseCustomerFactory( - uuid=FAKE_UUIDS[0], - name="test_enterprise" - ) - factories.EnterpriseCustomerUserFactory( - user_id=self.user.id, - enterprise_customer=ent_customer, - ) - - licensed_users_info = [ - { + self.create_user() + failure_user = factories.UserFactory() + + ent_customer = factories.EnterpriseCustomerFactory( + uuid=FAKE_UUIDS[0], + name="test_enterprise" + ) + factories.EnterpriseCustomerUserFactory( + user_id=self.user.id, + enterprise_customer=ent_customer, + ) + + licensed_users_info = [ + { + 'email': self.user.email, + 'course_run_key': 'course-key-v1', + 'course_mode': 'verified', + 'license_uuid': '5b77bdbade7b4fcb838f8111b68e18ae' + }, + { + 'email': failure_user.email, + 'course_run_key': 'course-key-v1', + 'course_mode': 'verified', + 'license_uuid': '5b77bdbade7b4fcb838f8111b68e18ae' + } + ] + mock_model.DoesNotExist = Exception + mock_update_or_create_enrollment.side_effect = [True, mock_error('mocked error'), None] + result = enroll_subsidy_users_in_courses(ent_customer, licensed_users_info) + self.assertEqual( + { + 'pending': [], + 'successes': [{ + 'user_id': self.user.id, 'email': self.user.email, 'course_run_key': 'course-key-v1', - 'course_mode': 'verified', - 'license_uuid': '5b77bdbade7b4fcb838f8111b68e18ae' - }, - { + 'user': self.user, + 'created': True, + 'activation_link': None, + 'enterprise_fulfillment_source_uuid': LicensedEnterpriseCourseEnrollment.objects.first().uuid, + }], + 'failures': [{ + 'user_id': failure_user.id, 'email': failure_user.email, 'course_run_key': 'course-key-v1', - 'course_mode': 'verified', - 'license_uuid': '5b77bdbade7b4fcb838f8111b68e18ae' - } - ] - mock_model.DoesNotExist = Exception - mock_customer_admin_enroll_user_with_status.side_effect = [True, mock_error('mocked error'), None] - result = enroll_subsidy_users_in_courses(ent_customer, licensed_users_info) - self.assertEqual( - { - 'pending': [], - 'successes': [{ - 'user_id': self.user.id, - 'email': self.user.email, - 'course_run_key': 'course-key-v1', - 'user': self.user, - 'created': True, - 'activation_link': None, - 'enterprise_fulfillment_source_uuid': LicensedEnterpriseCourseEnrollment.objects.first().uuid, - }], - 'failures': [{ - 'user_id': failure_user.id, - 'email': failure_user.email, - 'course_run_key': 'course-key-v1', - }], - }, - result - ) - self.assertEqual(len(EnterpriseCourseEnrollment.objects.all()), 1) + }], + }, + result + ) + self.assertEqual(len(EnterpriseCourseEnrollment.objects.all()), 1) @mock.patch('enterprise.utils.lms_update_or_create_enrollment') - @mock.patch('enterprise.utils.lms_enroll_user_in_course') - @ddt.data(True, False) def test_enroll_subsidy_users_in_courses_succeeds( self, - setting_value, - mock_enroll_user_in_course, mock_update_or_create_enrollment, ): """ Test that users that already exist are enrolled by enroll_subsidy_users_in_courses and returned under the `succeeded` field. """ - if setting_value: - mock_customer_admin_enroll_user = mock_update_or_create_enrollment - else: - mock_customer_admin_enroll_user = mock_enroll_user_in_course - with override_settings(ENABLE_ENTERPRISE_BACKEND_EMET_AUTO_UPGRADE_ENROLLMENT_MODE=setting_value): - self.create_user() - - ent_customer = factories.EnterpriseCustomerFactory( - uuid=FAKE_UUIDS[0], - name="test_enterprise" - ) - factories.EnterpriseCustomerUserFactory( - user_id=self.user.id, - enterprise_customer=ent_customer, - ) - licensed_users_info = [{ - 'email': self.user.email, - 'course_run_key': 'course-key-v1', - 'course_mode': 'verified', - 'license_uuid': '5b77bdbade7b4fcb838f8111b68e18ae' - }] - - mock_customer_admin_enroll_user.return_value = True - result = enroll_subsidy_users_in_courses(ent_customer, licensed_users_info) - self.assertEqual( - { - 'pending': [], - 'successes': [{ - 'user_id': self.user.id, - 'email': self.user.email, - 'course_run_key': 'course-key-v1', - 'user': self.user, - 'created': True, - 'activation_link': None, - 'enterprise_fulfillment_source_uuid': LicensedEnterpriseCourseEnrollment.objects.first().uuid, - }], - 'failures': [] - }, - result - ) - self.assertEqual(len(EnterpriseCourseEnrollment.objects.all()), 1) + self.create_user() + + ent_customer = factories.EnterpriseCustomerFactory( + uuid=FAKE_UUIDS[0], + name="test_enterprise" + ) + factories.EnterpriseCustomerUserFactory( + user_id=self.user.id, + enterprise_customer=ent_customer, + ) + licensed_users_info = [{ + 'email': self.user.email, + 'course_run_key': 'course-key-v1', + 'course_mode': 'verified', + 'license_uuid': '5b77bdbade7b4fcb838f8111b68e18ae' + }] + + mock_update_or_create_enrollment.return_value = True + result = enroll_subsidy_users_in_courses(ent_customer, licensed_users_info) + self.assertEqual( + { + 'pending': [], + 'successes': [{ + 'user_id': self.user.id, + 'email': self.user.email, + 'course_run_key': 'course-key-v1', + 'user': self.user, + 'created': True, + 'activation_link': None, + 'enterprise_fulfillment_source_uuid': LicensedEnterpriseCourseEnrollment.objects.first().uuid, + }], + 'failures': [] + }, + result + ) + self.assertEqual(len(EnterpriseCourseEnrollment.objects.all()), 1) @mock.patch('enterprise.utils.lms_update_or_create_enrollment') - @mock.patch('enterprise.utils.lms_enroll_user_in_course') - @ddt.data(True, False) def test_enroll_subsidy_users_in_courses_with_user_id_succeeds( self, - setting_value, - mock_enroll_user_in_course, mock_update_or_create_enrollment, ): """ Test that users that already exist are enrolled by enroll_subsidy_users_in_courses and returned under the ``succeeded`` field. Specifically test when a ``user_id`` is supplied. """ - if setting_value: - mock_customer_admin_enroll_user = mock_update_or_create_enrollment - else: - mock_customer_admin_enroll_user = mock_enroll_user_in_course - with override_settings(ENABLE_ENTERPRISE_BACKEND_EMET_AUTO_UPGRADE_ENROLLMENT_MODE=setting_value): - self.create_user() - another_user = factories.UserFactory(is_active=True) - - ent_customer = factories.EnterpriseCustomerFactory( - uuid=FAKE_UUIDS[0], - name="test_enterprise" - ) - factories.EnterpriseCustomerUserFactory( - user_id=self.user.id, - enterprise_customer=ent_customer, - ) - licensed_users_info = [ - { - # Should succeed with only a user_id supplied. - 'user_id': self.user.id, - 'course_run_key': 'course-key-1', - 'course_mode': 'verified', - 'license_uuid': '5b77bdbade7b4fcb838f8111b68e18ae', - }, - { - # Should succeed even with both a user_id and email supplied. - 'user_id': another_user.id, - 'email': another_user.email, - 'course_run_key': 'course-key-2', - 'course_mode': 'verified', - 'license_uuid': '5b77bdbade7b4fcb838f8111b68e18ae', - }, - ] - - mock_customer_admin_enroll_user.return_value = True - - result = enroll_subsidy_users_in_courses(ent_customer, licensed_users_info) - self.assertEqual( - { - 'pending': [], - 'successes': [ - { - 'user_id': self.user.id, - 'email': self.user.email, - 'course_run_key': 'course-key-1', - 'user': self.user, - 'created': True, - 'activation_link': None, - 'enterprise_fulfillment_source_uuid': EnterpriseCourseEnrollment.objects.filter( - enterprise_customer_user__user_id=self.user.id - ).first().licensedenterprisecourseenrollment_enrollment_fulfillment.uuid, - }, - { - 'user_id': another_user.id, - 'email': another_user.email, - 'course_run_key': 'course-key-2', - 'user': another_user, - 'created': True, - 'activation_link': None, - 'enterprise_fulfillment_source_uuid': EnterpriseCourseEnrollment.objects.filter( - enterprise_customer_user__user_id=another_user.id - ).first().licensedenterprisecourseenrollment_enrollment_fulfillment.uuid, - } - ], - 'failures': [], - }, - result - ) - self.assertEqual(len(EnterpriseCourseEnrollment.objects.all()), 2) + self.create_user() + another_user = factories.UserFactory(is_active=True) + + ent_customer = factories.EnterpriseCustomerFactory( + uuid=FAKE_UUIDS[0], + name="test_enterprise" + ) + factories.EnterpriseCustomerUserFactory( + user_id=self.user.id, + enterprise_customer=ent_customer, + ) + licensed_users_info = [ + { + # Should succeed with only a user_id supplied. + 'user_id': self.user.id, + 'course_run_key': 'course-key-1', + 'course_mode': 'verified', + 'license_uuid': '5b77bdbade7b4fcb838f8111b68e18ae', + }, + { + # Should succeed even with both a user_id and email supplied. + 'user_id': another_user.id, + 'email': another_user.email, + 'course_run_key': 'course-key-2', + 'course_mode': 'verified', + 'license_uuid': '5b77bdbade7b4fcb838f8111b68e18ae', + }, + ] + + mock_update_or_create_enrollment.return_value = True + + result = enroll_subsidy_users_in_courses(ent_customer, licensed_users_info) + self.assertEqual( + { + 'pending': [], + 'successes': [ + { + 'user_id': self.user.id, + 'email': self.user.email, + 'course_run_key': 'course-key-1', + 'user': self.user, + 'created': True, + 'activation_link': None, + 'enterprise_fulfillment_source_uuid': EnterpriseCourseEnrollment.objects.filter( + enterprise_customer_user__user_id=self.user.id + ).first().licensedenterprisecourseenrollment_enrollment_fulfillment.uuid, + }, + { + 'user_id': another_user.id, + 'email': another_user.email, + 'course_run_key': 'course-key-2', + 'user': another_user, + 'created': True, + 'activation_link': None, + 'enterprise_fulfillment_source_uuid': EnterpriseCourseEnrollment.objects.filter( + enterprise_customer_user__user_id=another_user.id + ).first().licensedenterprisecourseenrollment_enrollment_fulfillment.uuid, + } + ], + 'failures': [], + }, + result + ) + self.assertEqual(len(EnterpriseCourseEnrollment.objects.all()), 2) @mock.patch('enterprise.utils.lms_update_or_create_enrollment') - @mock.patch('enterprise.utils.lms_enroll_user_in_course') - @ddt.data(True, False) def test_enroll_subsidy_users_in_courses_user_identifier_failures( self, - setting_value, - mock_enroll_user_in_course, mock_update_or_create_enrollment, ): """ """ - if setting_value: - mock_customer_admin_enroll_user = mock_update_or_create_enrollment - else: - mock_customer_admin_enroll_user = mock_enroll_user_in_course - with override_settings(ENABLE_ENTERPRISE_BACKEND_EMET_AUTO_UPGRADE_ENROLLMENT_MODE=setting_value): - self.create_user() - another_user = factories.UserFactory(is_active=True) - - ent_customer = factories.EnterpriseCustomerFactory( - uuid=FAKE_UUIDS[0], - name="test_enterprise" - ) - factories.EnterpriseCustomerUserFactory( - user_id=self.user.id, - enterprise_customer=ent_customer, - ) - licensed_users_info = [ - { - # Should fail due to the user_id not matching the email of the same user. - 'user_id': self.user.id, - 'email': another_user.email, - 'course_run_key': 'course-key-1', - 'course_mode': 'verified', - 'license_uuid': '5b77bdbade7b4fcb838f8111b68e18ae', - }, - { - # Should fail due to the user_id not matching the email of the same user. Special case where the - # user_id does not exist. - 'user_id': self.user.id + 1000, - 'email': self.user.email, - 'course_run_key': 'course-key-2', - 'course_mode': 'verified', - 'license_uuid': '5b77bdbade7b4fcb838f8111b68e18ae', - }, - { - # Should fail due to the user_id not matching the email of the same user. Special case where the - # email does not exist. - 'user_id': self.user.id, - 'email': 'wrong+' + self.user.email, - 'course_run_key': 'course-key-3', - 'course_mode': 'verified', - 'license_uuid': '5b77bdbade7b4fcb838f8111b68e18ae', - }, - { - # Should fail due to providing neither `user_id` nor `email`. - 'course_run_key': 'course-key-4', - 'course_mode': 'verified', - 'license_uuid': '5b77bdbade7b4fcb838f8111b68e18ae', - }, - ] - - mock_customer_admin_enroll_user.return_value = True - - result = enroll_subsidy_users_in_courses(ent_customer, licensed_users_info) - self.assertEqual( - { - 'pending': [], - 'successes': [], - 'failures': [ - { - 'user_id': self.user.id, - 'email': another_user.email, - 'course_run_key': 'course-key-1', - }, - { - 'user_id': self.user.id + 1000, - 'email': self.user.email, - 'course_run_key': 'course-key-2', - }, - { - 'user_id': self.user.id, - 'email': 'wrong+' + self.user.email, - 'course_run_key': 'course-key-3', - }, - { - 'course_run_key': 'course-key-4', - }, - ], - }, - result - ) - self.assertEqual(len(EnterpriseCourseEnrollment.objects.all()), 0) + self.create_user() + another_user = factories.UserFactory(is_active=True) + + ent_customer = factories.EnterpriseCustomerFactory( + uuid=FAKE_UUIDS[0], + name="test_enterprise" + ) + factories.EnterpriseCustomerUserFactory( + user_id=self.user.id, + enterprise_customer=ent_customer, + ) + licensed_users_info = [ + { + # Should fail due to the user_id not matching the email of the same user. + 'user_id': self.user.id, + 'email': another_user.email, + 'course_run_key': 'course-key-1', + 'course_mode': 'verified', + 'license_uuid': '5b77bdbade7b4fcb838f8111b68e18ae', + }, + { + # Should fail due to the user_id not matching the email of the same user. Special case where the + # user_id does not exist. + 'user_id': self.user.id + 1000, + 'email': self.user.email, + 'course_run_key': 'course-key-2', + 'course_mode': 'verified', + 'license_uuid': '5b77bdbade7b4fcb838f8111b68e18ae', + }, + { + # Should fail due to the user_id not matching the email of the same user. Special case where the + # email does not exist. + 'user_id': self.user.id, + 'email': 'wrong+' + self.user.email, + 'course_run_key': 'course-key-3', + 'course_mode': 'verified', + 'license_uuid': '5b77bdbade7b4fcb838f8111b68e18ae', + }, + { + # Should fail due to providing neither `user_id` nor `email`. + 'course_run_key': 'course-key-4', + 'course_mode': 'verified', + 'license_uuid': '5b77bdbade7b4fcb838f8111b68e18ae', + }, + ] + + mock_update_or_create_enrollment.return_value = True + + result = enroll_subsidy_users_in_courses(ent_customer, licensed_users_info) + self.assertEqual( + { + 'pending': [], + 'successes': [], + 'failures': [ + { + 'user_id': self.user.id, + 'email': another_user.email, + 'course_run_key': 'course-key-1', + }, + { + 'user_id': self.user.id + 1000, + 'email': self.user.email, + 'course_run_key': 'course-key-2', + }, + { + 'user_id': self.user.id, + 'email': 'wrong+' + self.user.email, + 'course_run_key': 'course-key-3', + }, + { + 'course_run_key': 'course-key-4', + }, + ], + }, + result + ) + self.assertEqual(len(EnterpriseCourseEnrollment.objects.all()), 0) def test_enroll_pending_licensed_users_in_courses_succeeds(self): """ diff --git a/tests/test_utilities.py b/tests/test_utilities.py index aa0bc0bad2..46fa8490e3 100644 --- a/tests/test_utilities.py +++ b/tests/test_utilities.py @@ -159,6 +159,7 @@ def setUp(self): "enable_portal_learner_credit_management_screen", "enable_executive_education_2U_fulfillment", "enable_integrated_customer_learner_portal_search", + "enable_career_engagement_network_on_learner_portal", "enable_analytics_screen", "enable_slug_login", "contact_email", @@ -170,6 +171,7 @@ def setUp(self): "hide_labor_market_data", "chat_gpt_prompts", "enable_generation_of_api_credentials", + "career_engagement_network_message", "sso_orchestration_records", ] ),