diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 6d976535d..0ed5747b0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,6 +17,10 @@ Unreleased ---------- * nothing unreleased +[4.30.0] +-------- +* feat: REST APIs for default-enterprise-enrollment-intentions + [4.29.0] -------- * feat: Create django admin for default enrollments diff --git a/enterprise/__init__.py b/enterprise/__init__.py index ab6503366..47ad52738 100644 --- a/enterprise/__init__.py +++ b/enterprise/__init__.py @@ -2,4 +2,4 @@ Your project description goes here. """ -__version__ = "4.29.0" +__version__ = "4.30.0" diff --git a/enterprise/api/v1/serializers.py b/enterprise/api/v1/serializers.py index 75fc918eb..7cb8053db 100644 --- a/enterprise/api/v1/serializers.py +++ b/enterprise/api/v1/serializers.py @@ -1894,3 +1894,133 @@ def get_role_assignments(self, obj): return role_assignments_by_ecu_id else: return None + + +class DefaultEnterpriseEnrollmentIntentionSerializer(serializers.ModelSerializer): + """ + Serializer for the DefaultEnterpriseEnrollmentIntention model. + """ + + course_run_key = serializers.SerializerMethodField() + is_course_run_enrollable = serializers.SerializerMethodField() + course_run_normalized_metadata = serializers.SerializerMethodField() + applicable_enterprise_catalog_uuids = serializers.SerializerMethodField() + + class Meta: + model = models.DefaultEnterpriseEnrollmentIntention + fields = ( + 'uuid', + 'content_key', + 'enterprise_customer', + 'course_key', + 'course_run_key', + 'is_course_run_enrollable', + 'applicable_enterprise_catalog_uuids', + 'course_run_normalized_metadata', + 'created', + 'modified', + ) + + def get_course_run_key(self, obj): + """ + Get the course run key for the enrollment intention + """ + return obj.course_run_key + + def get_is_course_run_enrollable(self, obj): + """ + Get the course run enrollable status for the enrollment intention + """ + return obj.is_course_run_enrollable + + def get_course_run_normalized_metadata(self, obj): + """ + Get the course run for the enrollment intention + """ + return obj.course_run_normalized_metadata + + def get_applicable_enterprise_catalog_uuids(self, obj): + return obj.applicable_enterprise_catalog_uuids + + +class DefaultEnterpriseEnrollmentIntentionLearnerStatusSerializer(serializers.Serializer): + """ + Serializer for the DefaultEnterpriseEnrollmentIntentionLearnerStatus model. + """ + + lms_user_id = serializers.IntegerField() + user_email = serializers.EmailField() + enterprise_customer_uuid = serializers.UUIDField() + enrollment_statuses = serializers.SerializerMethodField() + metadata = serializers.SerializerMethodField() + + def needs_enrollment_counts(self): + """ + Return the counts of needs_enrollment. + """ + needs_enrollment = self.context.get('needs_enrollment', {}) + needs_enrollment_enrollable = needs_enrollment.get('enrollable', []) + needs_enrollment_not_enrollable = needs_enrollment.get('not_enrollable', []) + + return { + 'enrollable': len(needs_enrollment_enrollable), + 'not_enrollable': len(needs_enrollment_not_enrollable), + } + + def already_enrolled_count(self): + """ + Return the count of already enrolled. + """ + already_enrolled = self.context.get('already_enrolled', {}) + return len(already_enrolled) + + def total_default_enrollment_intention_count(self): + """ + Return the total count of default enrollment intentions. + """ + needs_enrollment_counts = self.needs_enrollment_counts() + total_needs_enrollment_enrollable = needs_enrollment_counts['enrollable'] + total_needs_enrollment_not_enrollable = needs_enrollment_counts['not_enrollable'] + return total_needs_enrollment_enrollable + total_needs_enrollment_not_enrollable + self.already_enrolled_count() + + def get_enrollment_statuses(self, obj): # pylint: disable=unused-argument + """ + Serialize the enrollment statuses by converting querysets to serialized data. + """ + needs_enrollment = self.context.get('needs_enrollment', {}) + needs_enrollment_enrollable = needs_enrollment.get('enrollable', []) + needs_enrollment_not_enrollable = needs_enrollment.get('not_enrollable', []) + already_enrolled = self.context.get('already_enrolled', {}) + + needs_enrollment_enrollable_data = DefaultEnterpriseEnrollmentIntentionSerializer( + needs_enrollment_enrollable, + many=True + ).data + needs_enrollment_unenrollable_data = DefaultEnterpriseEnrollmentIntentionSerializer( + needs_enrollment_not_enrollable, + many=True + ).data + already_enrolled_data = DefaultEnterpriseEnrollmentIntentionSerializer( + already_enrolled, + many=True + ).data + + return { + 'needs_enrollment': { + 'enrollable': needs_enrollment_enrollable_data, + 'not_enrollable': needs_enrollment_unenrollable_data, + }, + 'already_enrolled': already_enrolled_data, + } + + def get_metadata(self, obj): # pylint: disable=unused-argument + """ + Return the metadata for the default enterprise enrollment intention, including + number of default enterprise enrollment intentions that need enrollment, are already + enrolled by the learner. + """ + return { + 'total_default_enterprise_course_enrollments': self.total_default_enrollment_intention_count(), + 'total_needs_enrollment': self.needs_enrollment_counts(), + 'total_already_enrolled': self.already_enrolled_count(), + } diff --git a/enterprise/api/v1/urls.py b/enterprise/api/v1/urls.py index 866ed4292..3550fd852 100644 --- a/enterprise/api/v1/urls.py +++ b/enterprise/api/v1/urls.py @@ -26,6 +26,7 @@ pending_enterprise_customer_admin_user, pending_enterprise_customer_user, plotly_auth, + default_enterprise_enrollments, ) router = DefaultRouter() @@ -82,6 +83,11 @@ router.register( "enterprise_group", enterprise_group.EnterpriseGroupViewSet, 'enterprise-group' ) +router.register( + "default-enterprise-enrollment-intentions", + default_enterprise_enrollments.DefaultEnterpriseEnrollmentIntentionViewSet, + 'default-enterprise-enrollment-intentions' +) urlpatterns = [ diff --git a/enterprise/api/v1/views/default_enterprise_enrollments.py b/enterprise/api/v1/views/default_enterprise_enrollments.py new file mode 100644 index 000000000..23cffba9a --- /dev/null +++ b/enterprise/api/v1/views/default_enterprise_enrollments.py @@ -0,0 +1,182 @@ +""" +Views for default enterprise enrollments. +""" + +from uuid import UUID + +from rest_framework.decorators import action +from rest_framework.exceptions import ValidationError +from rest_framework.response import Response +from rest_framework import status, viewsets +from edx_rbac.mixins import PermissionRequiredForListingMixin + +from django.contrib.auth import get_user_model + +from enterprise import models +from enterprise.api.v1 import serializers +from enterprise.api.v1.views.base_views import EnterpriseViewSet +from enterprise.constants import ( + DEFAULT_ENTERPRISE_ENROLLMENT_INTENTIONS_PERMISSION, + ENTERPRISE_LEARNER_ROLE, + ENTERPRISE_ADMIN_ROLE, + ENTERPRISE_OPERATOR_ROLE, + DEFAULT_ENTERPRISE_ENROLLMENT_INTENTIONS_ROLE, +) + + +class DefaultEnterpriseEnrollmentIntentionViewSet( + PermissionRequiredForListingMixin, + EnterpriseViewSet, + viewsets.ModelViewSet, +): + """ + API views for default enterprise enrollment intentions + """ + + permission_required = DEFAULT_ENTERPRISE_ENROLLMENT_INTENTIONS_PERMISSION + list_lookup_field = 'enterprise_customer__uuid' + allowed_roles = [DEFAULT_ENTERPRISE_ENROLLMENT_INTENTIONS_ROLE] + serializer_class = serializers.DefaultEnterpriseEnrollmentIntentionSerializer + http_method_names = ['get', 'post', 'delete'] + + @property + def requested_enterprise_customer_uuid(self): + """ + Get and validate the enterprise customer UUID from the query parameters. + """ + if not (enterprise_customer_uuid := self.request.query_params.get('enterprise_customer_uuid')): + raise ValidationError({"detail": "enterprise_customer_uuid is a required query parameter."}) + + try: + return UUID(enterprise_customer_uuid) + except ValueError as exc: + raise ValidationError({ + "detail": "enterprise_customer_uuid query parameter is not a valid UUID." + }) from exc + + @property + def requested_lms_user_id(self): + """ + Get the (optional) LMS user ID from the request. + """ + return self.request.query_params.get('lms_user_id') + + @property + def base_queryset(self): + """ + Required by the `PermissionRequiredForListingMixin`. + For non-list actions, this is what's returned by `get_queryset()`. + For list actions, some non-strict subset of this is what's returned by `get_queryset()`. + """ + kwargs = {} + if self.requested_enterprise_customer_uuid: + kwargs['enterprise_customer'] = self.requested_enterprise_customer_uuid + return models.DefaultEnterpriseEnrollmentIntention.objects.filter(**kwargs) + + @property + def user_for_learner_status(self): + """ + Get the user for learner status based on the request. + """ + if self.request.user.is_staff and self.requested_lms_user_id is not None: + User = get_user_model() + try: + return User.objects.get(id=self.requested_lms_user_id) + except User.DoesNotExist: + return None + + return self.request.user + + def get_permission_object(self): + """ + Used for "retrieve" actions. Determines the context (enterprise UUID) to check + against for role-based permissions. + """ + return str(self.requested_enterprise_customer_uuid) + + @action(detail=False, methods=['get'], url_path='learner-status') + def learner_status(self, request): # pylint: disable=unused-argument + """ + Get the status of the learner's enrollment in the default enterprise course. + """ + # Validate the enterprise customer uuid. + try: + enterprise_customer_uuid = self.requested_enterprise_customer_uuid + except ValidationError as exc: + return Response(exc, status=status.HTTP_400_BAD_REQUEST) + + # Validate the user for learner status exists and is associated + # with the enterprise customer. + if not (user_for_learner_status := self.user_for_learner_status): + return Response( + {'detail': f'User with lms_user_id {self.requested_lms_user_id} not found.'}, + status=status.HTTP_400_BAD_REQUEST, + ) + + try: + enterprise_customer_user = models.EnterpriseCustomerUser.objects.get( + user_id=user_for_learner_status.id, + enterprise_customer=enterprise_customer_uuid, + ) + except models.EnterpriseCustomerUser.DoesNotExist: + return Response( + { + 'detail': ( + f'User with lms_user_id {user_for_learner_status.id} is not associated with ' + f'the enterprise customer {enterprise_customer_uuid}.' + ), + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + # Retrieve configured default enrollment intentions for the enterprise customer + default_enrollment_intentions_for_customer = models.DefaultEnterpriseEnrollmentIntention.objects.filter( + enterprise_customer=enterprise_customer_uuid, + ) + + # Retrieve the course enrollments for the learner + enterprise_course_enrollments_for_learner = models.EnterpriseCourseEnrollment.objects.filter( + enterprise_customer_user=enterprise_customer_user, + ) + enrolled_course_ids_for_learner = enterprise_course_enrollments_for_learner.values_list('course_id', flat=True) + + already_enrolled = [] + needs_enrollment_enrollable = [] + needs_enrollment_not_enrollable = [] + + # Iterate through the default enrollment intentions and categorize them based + # on the learner's enrollment status (already enrolled, needs enrollment, etc.) + # and whether the course run is enrollable. + for default_enrollment_intention in default_enrollment_intentions_for_customer: + course_run_key = default_enrollment_intention.course_run_key + is_course_run_enrollable = default_enrollment_intention.is_course_run_enrollable + applicable_enterprise_catalog_uuids = default_enrollment_intention.applicable_enterprise_catalog_uuids + + if course_run_key in enrolled_course_ids_for_learner: + # Learner is already enrolled in this course run + already_enrolled.append(default_enrollment_intention) + elif is_course_run_enrollable and applicable_enterprise_catalog_uuids: + # Learner needs enrollment, the course run is enrollable, and there are applicable catalogs + needs_enrollment_enrollable.append(default_enrollment_intention) + else: + # Learner needs enrollment, but the course run is not enrollable and/or there are no applicable catalogs + needs_enrollment_not_enrollable.append(default_enrollment_intention) + + serializer_data = { + 'lms_user_id': user_for_learner_status.id, + 'user_email': user_for_learner_status.email, + 'enterprise_customer_uuid': enterprise_customer_uuid, + } + serializer_context = { + 'needs_enrollment': { + 'enrollable': needs_enrollment_enrollable, + 'not_enrollable': needs_enrollment_not_enrollable, + }, + 'already_enrolled': already_enrolled, + } + serializer = serializers.DefaultEnterpriseEnrollmentIntentionLearnerStatusSerializer( + data=serializer_data, + context=serializer_context, + ) + serializer.is_valid(raise_exception=True) + return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/enterprise/api_client/enterprise_catalog.py b/enterprise/api_client/enterprise_catalog.py index a3e41f960..1ac011172 100644 --- a/enterprise/api_client/enterprise_catalog.py +++ b/enterprise/api_client/enterprise_catalog.py @@ -376,11 +376,14 @@ def enterprise_contains_content_items(self, enterprise_uuid, content_ids): The endpoint does not differentiate between course_run_ids and program_uuids so they can be used interchangeably. The two query parameters are left in for backwards compatability with edx-enterprise. """ - query_params = {'course_run_ids': content_ids} + query_params = { + 'course_run_ids': content_ids, + 'get_catalogs_containing_specified_content_ids': True, + } api_url = self.get_api_url(f"{self.ENTERPRISE_CUSTOMER_ENDPOINT}/{enterprise_uuid}/contains_content_items") response = self.client.get(api_url, params=query_params) response.raise_for_status() - return response.json()['contains_content_items'] + return response.json() @UserAPIClient.refresh_token def get_content_metadata_content_identifier(self, enterprise_uuid, content_id): diff --git a/enterprise/cache_utils.py b/enterprise/cache_utils.py index 7e8252460..fae42400b 100644 --- a/enterprise/cache_utils.py +++ b/enterprise/cache_utils.py @@ -19,7 +19,7 @@ def versioned_cache_key(*args): """ components = [str(arg) for arg in args] components.append(code_version) - if stamp_from_settings := getattr(settings, 'CACHE_KEY_VERSION_STAMP', None): + if stamp_from_settings := getattr(settings, 'ENTERPRISE_CACHE_KEY_VERSION_STAMP', None): components.append(stamp_from_settings) decoded_cache_key = CACHE_KEY_SEP.join(components) return hashlib.sha512(decoded_cache_key.encode()).hexdigest() diff --git a/enterprise/constants.py b/enterprise/constants.py index 87d2cec4a..127627303 100644 --- a/enterprise/constants.py +++ b/enterprise/constants.py @@ -150,6 +150,11 @@ class CourseModes: ENTERPRISE_REPORTING_CONFIG_ADMIN_ROLE = 'reporting_config_admin' ENTERPRISE_FULFILLMENT_OPERATOR_ROLE = 'fulfillment_operator' ENTERPRISE_SSO_ORCHESTRATOR_OPERATOR_ROLE = 'sso_orchestrator_operator' + +# Default enterprise enrollment roles/permissions +DEFAULT_ENTERPRISE_ENROLLMENT_INTENTIONS_ROLE = 'default_enterprise_enrollment_intentions_learner' +DEFAULT_ENTERPRISE_ENROLLMENT_INTENTIONS_PERMISSION = 'enterprise.can_view_default_enterprise_enrollment_intentions' + # Provisioning admins roles: PROVISIONING_ENTERPRISE_CUSTOMER_ADMIN_ROLE = 'provisioning_enterprise_customer_admin' PROVISIONING_PENDING_ENTERPRISE_CUSTOMER_ADMIN_ROLE = 'provisioning_pending_enterprise_customer_users_admin' diff --git a/enterprise/content_metadata/api.py b/enterprise/content_metadata/api.py index 4023c1603..381b72cd3 100644 --- a/enterprise/content_metadata/api.py +++ b/enterprise/content_metadata/api.py @@ -58,3 +58,40 @@ def get_and_cache_customer_content_metadata(enterprise_customer_uuid, content_ke ) TieredCache.set_all_tiers(cache_key, result, timeout or DEFAULT_CACHE_TIMEOUT) return result + +def get_and_cache_enterprise_contains_content_items(enterprise_customer_uuid, content_keys, timeout=None): + """ + Returns whether the provided content keys are present in the catalogs + associated with the provided enterprise customer, in addition to a list + of catalog UUIDs containing the content keys. + + The response is cached in a ``TieredCache``. + + Returns: Dict containing `contains_content_items` and `catalog_list` properties. + Raises: An HTTPError if there's a problem checking catalog inclusion + via the enterprise-catalog service. + """ + cache_key = versioned_cache_key('get_enterprise_contains_content_items', enterprise_customer_uuid, content_keys) + cached_response = TieredCache.get_cached_response(cache_key) + if cached_response.is_found: + logger.info(f'cache hit for enterprise customer {enterprise_customer_uuid} and content keys {content_keys}') + return cached_response.value + + try: + result = EnterpriseCatalogApiClient().enterprise_contains_content_items( + enterprise_uuid=enterprise_customer_uuid, + content_ids=content_keys, + ) + except HTTPError as exc: + raise exc + + if not result: + logger.warning('No content items found for customer %s', enterprise_customer_uuid) + return {} + + logger.info( + 'Fetched content catalog inclusion for enterprise customer %s and content keys %s. Result = %s', + enterprise_customer_uuid, content_keys, result, + ) + TieredCache.set_all_tiers(cache_key, result, timeout or DEFAULT_CACHE_TIMEOUT) + return result diff --git a/enterprise/models.py b/enterprise/models.py index ea0619acd..fa54d1394 100644 --- a/enterprise/models.py +++ b/enterprise/models.py @@ -61,7 +61,10 @@ FulfillmentTypes, json_serialized_course_modes, ) -from enterprise.content_metadata.api import get_and_cache_customer_content_metadata +from enterprise.content_metadata.api import ( + get_and_cache_customer_content_metadata, + get_and_cache_enterprise_contains_content_items, +) from enterprise.errors import LinkUserToEnterpriseError from enterprise.event_bus import send_learner_credit_course_enrollment_revoked_event from enterprise.logging import getEnterpriseLogger @@ -761,10 +764,11 @@ def catalog_contains_course(self, course_run_id): Returns: bool: Whether the enterprise catalog includes the given course run. """ - if EnterpriseCatalogApiClient().enterprise_contains_content_items(self.uuid, [course_run_id]): - return True - - return False + contains_content_items_response = EnterpriseCatalogApiClient().enterprise_contains_content_items( + self.uuid, + [course_run_id], + ) + return contains_content_items_response.get('contains_content_items', False) def enroll_user_pending_registration_with_status(self, email, course_mode, *course_ids, **kwargs): """ @@ -2558,6 +2562,19 @@ def course_run(self): {} ) + @property + def course_run_normalized_metadata(self): # pragma: no cover + """ + Normalized metadata for the course run. + """ + metadata = self.content_metadata_for_content_key + if not metadata: + return {} + + course_run_key = self.course_run_key + normalized_metadata_by_run = metadata.get('normalized_metadata_by_run', {}) + return normalized_metadata_by_run.get(course_run_key, {}) + @property def course_key(self): """ @@ -2579,7 +2596,7 @@ def is_course_run_enrollable(self): # pragma: no cover """ Whether the course run is enrollable. """ - return False + return self.course_run.get('is_enrollable', False) @property def course_run_enroll_by_date(self): # pragma: no cover @@ -2588,6 +2605,17 @@ def course_run_enroll_by_date(self): # pragma: no cover """ return datetime.datetime.min + @property + def applicable_enterprise_catalog_uuids(self): + """ + Returns a list of UUIDs for applicable enterprise catalogs. + """ + contains_content_items_response = get_and_cache_enterprise_contains_content_items( + enterprise_customer_uuid=self.enterprise_customer.uuid, + content_keys=[self.course_run_key], + ) + return contains_content_items_response.get('catalog_list', []) + def determine_content_type(self): """ Determines the content_type for a given content_key by validating the return value @@ -2636,9 +2664,7 @@ def clean(self): 'content key already exists, but is soft-deleted. Please restore ' 'it here.', ).format(existing_record_admin_url=existing_record_admin_url) - raise ValidationError({ - 'content_key': mark_safe(message) - }) + raise ValidationError({'content_key': mark_safe(message)}) if not self.course_run: # NOTE: This validation check also acts as an inferred check on the derived content_type diff --git a/enterprise/rules.py b/enterprise/rules.py index 7b2a3a695..e7b1b44fb 100644 --- a/enterprise/rules.py +++ b/enterprise/rules.py @@ -19,6 +19,8 @@ PENDING_ENT_CUSTOMER_ADMIN_PROVISIONING_ADMIN_ACCESS_PERMISSION, PROVISIONING_ENTERPRISE_CUSTOMER_ADMIN_ROLE, PROVISIONING_PENDING_ENTERPRISE_CUSTOMER_ADMIN_ROLE, + DEFAULT_ENTERPRISE_ENROLLMENT_INTENTIONS_ROLE, + DEFAULT_ENTERPRISE_ENROLLMENT_INTENTIONS_PERMISSION, ) from enterprise.models import EnterpriseFeatureUserRoleAssignment @@ -241,6 +243,25 @@ def has_explicit_access_to_reporting_api(user, obj): obj, ) +@rules.predicate +def has_implicit_access_to_default_enterprise_enrollment_intentions(user, obj): # pylint: disable=unused-argument + """ + Check that if request user has implicit access to `ENTERPRISE_REPORTING_CONFIG_ADMIN_ROLE` feature role. + + Params: + user: An ``auth.User`` instance. + obj: The string version of an ``EnterpriseCustomer.uuid``. + + Returns: + boolean: whether the request user has access or not + """ + request = crum.get_current_request() + decoded_jwt = get_decoded_jwt(request) or get_decoded_jwt_from_auth(request) + return request_user_has_implicit_access_via_jwt( + decoded_jwt, + DEFAULT_ENTERPRISE_ENROLLMENT_INTENTIONS_ROLE, + str(obj) + ) rules.add_perm('enterprise.can_access_admin_dashboard', has_implicit_access_to_dashboard | has_explicit_access_to_dashboard) @@ -270,3 +291,8 @@ def has_explicit_access_to_reporting_api(user, obj): PENDING_ENT_CUSTOMER_ADMIN_PROVISIONING_ADMIN_ACCESS_PERMISSION, has_implicit_access_to_provisioning_pending_enterprise_customer_admin_users, ) + +rules.add_perm( + DEFAULT_ENTERPRISE_ENROLLMENT_INTENTIONS_PERMISSION, + has_implicit_access_to_default_enterprise_enrollment_intentions, +) diff --git a/enterprise/settings/test.py b/enterprise/settings/test.py index 8952ae926..c50c7ee8b 100644 --- a/enterprise/settings/test.py +++ b/enterprise/settings/test.py @@ -12,6 +12,7 @@ from enterprise.constants import ( ENTERPRISE_ADMIN_ROLE, + ENTERPRISE_LEARNER_ROLE, ENTERPRISE_CATALOG_ADMIN_ROLE, ENTERPRISE_DASHBOARD_ADMIN_ROLE, ENTERPRISE_ENROLLMENT_API_ADMIN_ROLE, @@ -21,6 +22,7 @@ PROVISIONING_ENTERPRISE_CUSTOMER_ADMIN_ROLE, PROVISIONING_PENDING_ENTERPRISE_CUSTOMER_ADMIN_ROLE, SYSTEM_ENTERPRISE_PROVISIONING_ADMIN_ROLE, + DEFAULT_ENTERPRISE_ENROLLMENT_INTENTIONS_ROLE, ) @@ -232,10 +234,14 @@ def root(*args): # For testing edx-rbac rules. This is not the actual value of the setting in prod. SYSTEM_TO_FEATURE_ROLE_MAPPING = { + ENTERPRISE_LEARNER_ROLE: [ + DEFAULT_ENTERPRISE_ENROLLMENT_INTENTIONS_ROLE, + ], ENTERPRISE_ADMIN_ROLE: [ ENTERPRISE_DASHBOARD_ADMIN_ROLE, ENTERPRISE_CATALOG_ADMIN_ROLE, - ENTERPRISE_ENROLLMENT_API_ADMIN_ROLE + ENTERPRISE_ENROLLMENT_API_ADMIN_ROLE, + DEFAULT_ENTERPRISE_ENROLLMENT_INTENTIONS_ROLE, ], ENTERPRISE_OPERATOR_ROLE: [ ENTERPRISE_DASHBOARD_ADMIN_ROLE, @@ -243,6 +249,7 @@ def root(*args): ENTERPRISE_ENROLLMENT_API_ADMIN_ROLE, ENTERPRISE_FULFILLMENT_OPERATOR_ROLE, ENTERPRISE_SSO_ORCHESTRATOR_OPERATOR_ROLE, + DEFAULT_ENTERPRISE_ENROLLMENT_INTENTIONS_ROLE, ], SYSTEM_ENTERPRISE_PROVISIONING_ADMIN_ROLE: [ PROVISIONING_ENTERPRISE_CUSTOMER_ADMIN_ROLE, diff --git a/test_utils/factories.py b/test_utils/factories.py index ded8506fa..49f4c0974 100644 --- a/test_utils/factories.py +++ b/test_utils/factories.py @@ -1156,6 +1156,5 @@ class Meta: uuid = factory.LazyAttribute(lambda x: UUID(FAKER.uuid4())) enterprise_customer = factory.SubFactory(EnterpriseCustomerFactory) - content_type = "course" - content_key = "edX+demoX" - factory.SubFactory(EnterpriseCourseEnrollmentFactory) + content_type = DefaultEnterpriseEnrollmentIntention.COURSE + content_key = "edX+DemoX" diff --git a/test_utils/fake_catalog_api.py b/test_utils/fake_catalog_api.py index 44d3f3343..3ecf6bf5f 100644 --- a/test_utils/fake_catalog_api.py +++ b/test_utils/fake_catalog_api.py @@ -136,6 +136,8 @@ 'pacing_type': 'instructor_paced', 'type': 'verified', 'status': 'published', + "is_enrollable": True, + "is_marketable": True, 'course': 'edX+DemoX', 'full_description': 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', 'announcement': None, @@ -265,6 +267,7 @@ FAKE_COURSE = { 'key': 'edX+DemoX', + 'course_type': 'course', 'uuid': 'a9e8bb52-0c8d-4579-8496-1a8becb0a79c', 'title': 'edX Demonstration Course', 'course_runs': [FAKE_COURSE_RUN], @@ -298,7 +301,8 @@ 'content_type': 'course', 'enrollment_url': FAKE_URL, 'programs': [], - 'content_last_modified': '2020-08-18T00:32:33.754662Z' + 'content_last_modified': '2020-08-18T00:32:33.754662Z', + 'advertised_course_run_uuid': FAKE_COURSE_RUN.get('uuid'), } FAKE_PROGRAM_RESPONSE1 = { @@ -1272,7 +1276,6 @@ "is_program_eligible_for_one_click_purchase": True } - FAKE_SEARCH_ALL_RESULTS = { "count": 3, "next": None, @@ -1376,6 +1379,11 @@ 'catalog_modified': '2020-07-16T15:11:10.521611Z' } +FAKE_ENTERPRISE_CONTAINS_CONTENT_ITEMS_RESPONSE = { + 'contains_content_items': False, + 'catalog_list': [] +} + def get_catalog_courses(catalog_id): """ @@ -1644,6 +1652,17 @@ def get_fake_content_metadata_no_program(): return list(content_metadata.values()) +def get_fake_enterprise_contains_content_items_response(contains_content_items=False, catalog_list=None): + """ + Returns a fake response from EnterpriseCatalogApiClient.enterprise_contains_content_items. + """ + mock_response = FAKE_ENTERPRISE_CONTAINS_CONTENT_ITEMS_RESPONSE.copy() + if contains_content_items is not None: + mock_response['contains_content_items'] = contains_content_items + if catalog_list: + mock_response['catalog_list'] = catalog_list + return mock_response + class CourseDiscoveryApiTestMixin: """ Mixin for course discovery API test classes. diff --git a/tests/test_enterprise/api/test_views.py b/tests/test_enterprise/api/test_views.py index fa90d7acf..5afecbdae 100644 --- a/tests/test_enterprise/api/test_views.py +++ b/tests/test_enterprise/api/test_views.py @@ -170,7 +170,15 @@ 'licensed-enterprise-course-enrollment-bulk-licensed-enrollments-expiration' ) VERIFIED_SUBSCRIPTION_COURSE_MODE = 'verified' - +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): """ @@ -9738,3 +9746,380 @@ 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() + + def create_mock_default_enterprise_enrollment_intention( + self, + mock_catalog_api_client, + content_metadata=None, + contains_content_items=False, + catalog_list=None, + ): + """ + Create a mock default enterprise enrollment intention. + """ + mock_content_metadata = content_metadata or fake_catalog_api.FAKE_COURSE + mock_contains_content_items = contains_content_items + mock_catalog_list = ( + catalog_list + if catalog_list is not None + else [fake_catalog_api.FAKE_CATALOG_RESULT.get('uuid')] + ) + + mock_catalog_api_client.return_value = mock.Mock( + get_content_metadata_content_identifier=mock.Mock( + return_value=mock_content_metadata, + ), + enterprise_contains_content_items=mock.Mock( + return_value=fake_catalog_api.get_fake_enterprise_contains_content_items_response( + contains_content_items=mock_contains_content_items, + catalog_list=mock_catalog_list, + ), + ), + ) + enrollment_intention = factories.DefaultEnterpriseEnrollmentIntentionFactory( + enterprise_customer=self.enterprise_customer, + content_key=mock_content_metadata.get('key', 'edX+DemoX'), + ) + return enrollment_intention + + 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 = self.create_mock_default_enterprise_enrollment_intention(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 = self.create_mock_default_enterprise_enrollment_intention(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)) + self.create_mock_default_enterprise_enrollment_intention(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 = self.create_mock_default_enterprise_enrollment_intention(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 = self.create_mock_default_enterprise_enrollment_intention( + 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') + def test_default_enterprise_enrollment_intentions_learner_status_enrollable(self, mock_catalog_api_client): + """ + Test default enterprise enrollment intentions for specific learner linked to enterprise customer. + """ + self.set_jwt_cookie(ENTERPRISE_LEARNER_ROLE, str(self.enterprise_customer.uuid)) + enrollment_intention = self.create_mock_default_enterprise_enrollment_intention(mock_catalog_api_client) + 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': [ + { + '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': True, + 'applicable_enterprise_catalog_uuids': [fake_catalog_api.FAKE_CATALOG_RESULT.get('uuid')], + 'course_run_normalized_metadata': {}, # TODO + 'created': enrollment_intention.created.strftime("%Y-%m-%dT%H:%M:%SZ"), + 'modified': enrollment_intention.modified.strftime("%Y-%m-%dT%H:%M:%SZ") + } + ], + 'not_enrollable': [], + }, + 'already_enrolled': [], + } + assert response_data['metadata'] == { + 'total_default_enterprise_course_enrollments': 1, + 'total_needs_enrollment': { + 'enrollable': 1, + 'not_enrollable': 0, + }, + 'total_already_enrolled': 0, + } + + @mock.patch('enterprise.content_metadata.api.EnterpriseCatalogApiClient') + def test_default_enrollment_intentions_learner_status_content_not_enrollable(self, 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 = self.create_mock_default_enterprise_enrollment_intention( + mock_catalog_api_client, + content_metadata=mock_course, + ) + 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': [ + { + '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': False, + 'applicable_enterprise_catalog_uuids': [fake_catalog_api.FAKE_CATALOG_RESULT.get('uuid')], + 'course_run_normalized_metadata': {}, # TODO + 'created': enrollment_intention.created.strftime("%Y-%m-%dT%H:%M:%SZ"), + 'modified': enrollment_intention.modified.strftime("%Y-%m-%dT%H:%M:%SZ") + } + ], + }, + 'already_enrolled': [], + } + assert response_data['metadata'] == { + 'total_default_enterprise_course_enrollments': 1, + 'total_needs_enrollment': { + 'enrollable': 0, + 'not_enrollable': 1, + }, + 'total_already_enrolled': 0, + } + + @mock.patch('enterprise.content_metadata.api.EnterpriseCatalogApiClient') + def test_default_enrollment_intentions_learner_status_content_not_in_catalog(self, 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)) + enrollment_intention = self.create_mock_default_enterprise_enrollment_intention( + mock_catalog_api_client, + contains_content_items=False, + catalog_list=[], + ) + 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': [ + { + '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': True, + 'applicable_enterprise_catalog_uuids': [], + 'course_run_normalized_metadata': {}, # TODO + 'created': enrollment_intention.created.strftime("%Y-%m-%dT%H:%M:%SZ"), + 'modified': enrollment_intention.modified.strftime("%Y-%m-%dT%H:%M:%SZ") + } + ], + }, + 'already_enrolled': [], + } + assert response_data['metadata'] == { + 'total_default_enterprise_course_enrollments': 1, + 'total_needs_enrollment': { + 'enrollable': 0, + 'not_enrollable': 1, + }, + 'total_already_enrolled': 0, + } + + @mock.patch('enterprise.content_metadata.api.EnterpriseCatalogApiClient') + def test_default_enrollment_intentions_learner_status_already_enrolled(self, mock_catalog_api_client): + """ + Test default enterprise enrollment intentions (already enrolled) for + specific learner linked to enterprise customer. + """ + self.set_jwt_cookie(ENTERPRISE_LEARNER_ROLE, str(self.enterprise_customer.uuid)) + enrollment_intention = self.create_mock_default_enterprise_enrollment_intention(mock_catalog_api_client) + 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'), + ) + 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': [ + { + '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': True, + 'applicable_enterprise_catalog_uuids': [fake_catalog_api.FAKE_CATALOG_RESULT.get('uuid')], + 'course_run_normalized_metadata': {}, # TODO + 'created': enrollment_intention.created.strftime("%Y-%m-%dT%H:%M:%SZ"), + 'modified': enrollment_intention.modified.strftime("%Y-%m-%dT%H:%M:%SZ") + } + ], + } + assert response_data['metadata'] == { + 'total_default_enterprise_course_enrollments': 1, + 'total_needs_enrollment': { + 'enrollable': 0, + 'not_enrollable': 0, + }, + 'total_already_enrolled': 1, + } diff --git a/tests/test_enterprise/api_client/test_enterprise_catalog.py b/tests/test_enterprise/api_client/test_enterprise_catalog.py index a3a830a7b..98be0c5f1 100644 --- a/tests/test_enterprise/api_client/test_enterprise_catalog.py +++ b/tests/test_enterprise/api_client/test_enterprise_catalog.py @@ -416,11 +416,11 @@ def test_contains_content_items(): @responses.activate @mock.patch('enterprise.api_client.client.JwtBuilder', mock.Mock()) def test_enterprise_contains_content_items(): - url = _url("enterprise-customer/{enterprise_uuid}/contains_content_items/?course_run_ids=demoX".format( - enterprise_uuid=TEST_ENTERPRISE_ID - )) + query_params = '?course_run_ids=demoX&get_catalogs_containing_specified_content_ids=True' + url = _url(f"enterprise-customer/{TEST_ENTERPRISE_ID}/contains_content_items/{query_params}") expected_response = { 'contains_content_items': True, + 'catalog_list': [], } responses.add( responses.GET, @@ -429,7 +429,10 @@ def test_enterprise_contains_content_items(): ) client = enterprise_catalog.EnterpriseCatalogApiClient('staff-user-goes-here') actual_response = client.enterprise_contains_content_items(TEST_ENTERPRISE_ID, ['demoX']) - assert actual_response == expected_response['contains_content_items'] + actual_contains_content_items = actual_response['contains_content_items'] + actual_catalog_list = actual_response['catalog_list'] + assert actual_contains_content_items == expected_response['contains_content_items'] + assert actual_catalog_list == expected_response['catalog_list'] @responses.activate diff --git a/tests/test_enterprise/test_rules.py b/tests/test_enterprise/test_rules.py index 313008b17..e2f89cebe 100644 --- a/tests/test_enterprise/test_rules.py +++ b/tests/test_enterprise/test_rules.py @@ -12,6 +12,8 @@ ENTERPRISE_CATALOG_ADMIN_ROLE, ENTERPRISE_DASHBOARD_ADMIN_ROLE, ENTERPRISE_ENROLLMENT_API_ADMIN_ROLE, + ENTERPRISE_LEARNER_ROLE, + DEFAULT_ENTERPRISE_ENROLLMENT_INTENTIONS_PERMISSION, ) from enterprise.models import EnterpriseFeatureRole, EnterpriseFeatureUserRoleAssignment from test_utils import TEST_UUID, APITest, factories @@ -26,12 +28,14 @@ class TestEnterpriseRBACPermissions(APITest): @mock.patch('enterprise.rules.crum.get_current_request') @ddt.data( - 'enterprise.can_access_admin_dashboard', - 'enterprise.can_view_catalog', - 'enterprise.can_enroll_learners', + ('enterprise.can_access_admin_dashboard', ENTERPRISE_ADMIN_ROLE), + ('enterprise.can_view_catalog', ENTERPRISE_ADMIN_ROLE), + ('enterprise.can_enroll_learners', ENTERPRISE_ADMIN_ROLE), + (DEFAULT_ENTERPRISE_ENROLLMENT_INTENTIONS_PERMISSION, ENTERPRISE_LEARNER_ROLE), ) - def test_has_implicit_access(self, permission, get_current_request_mock): - get_current_request_mock.return_value = self.get_request_with_jwt_cookie(ENTERPRISE_ADMIN_ROLE, TEST_UUID) + @ddt.unpack + def test_has_implicit_access(self, permission, enterprise_role, get_current_request_mock): + get_current_request_mock.return_value = self.get_request_with_jwt_cookie(enterprise_role, TEST_UUID) assert self.user.has_perm(permission, TEST_UUID) @mock.patch('enterprise.rules.crum.get_current_request')