class EnterpriseCustomerCatalogViewSet(EnterpriseReadOnlyModelViewSet): """ API Views for performing search through course discovery at the ``enterprise_catalogs`` API endpoint. """ queryset = models.EnterpriseCustomerCatalog.objects.all() USER_ID_FILTER = 'enterprise_customer__enterprise_customer_users__user_id' FIELDS = ( 'uuid', 'enterprise_customer', ) filter_fields = FIELDS ordering_fields = FIELDS renderer_classes = ( JSONRenderer, XMLRenderer, ) def get_serializer_class(self): action = getattr(self, 'action', None) if action == 'retrieve': return serializers.EnterpriseCustomerCatalogDetailSerializer return serializers.EnterpriseCustomerCatalogSerializer @method_decorator( require_at_least_one_query_parameter('course_run_ids', 'program_uuids')) @detail_route() # pylint: disable=invalid-name,unused-argument def contains_content_items(self, request, pk, course_run_ids, program_uuids): """ Return whether or not the EnterpriseCustomerCatalog contains the specified content. Multiple course_run_ids and/or program_uuids query parameters can be sent to this view to check for their existence in the EnterpriseCustomerCatalog. At least one course run key or program UUID value must be included in the request. """ enterprise_customer_catalog = self.get_object() # Maintain plus characters in course key. course_run_ids = [ unquote(quote_plus(course_run_id)) for course_run_id in course_run_ids ] contains_content_items = True if course_run_ids: contains_content_items = enterprise_customer_catalog.contains_courses( course_run_ids) if program_uuids: contains_content_items = ( contains_content_items and enterprise_customer_catalog.contains_programs(program_uuids)) return Response({'contains_content_items': contains_content_items}) @detail_route(url_path='courses/{}'.format(COURSE_KEY_URL_PATTERN)) def course_detail(self, request, pk, course_key): # pylint: disable=invalid-name,unused-argument """ Return the metadata for the specified course. The course needs to be included in the specified EnterpriseCustomerCatalog in order for metadata to be returned from this endpoint. """ enterprise_customer_catalog = self.get_object() course = enterprise_customer_catalog.get_course(course_key) if not course: raise Http404 context = self.get_serializer_context() context['enterprise_customer_catalog'] = enterprise_customer_catalog serializer = serializers.CourseDetailSerializer(course, context=context) return Response(serializer.data) @detail_route(url_path='course_runs/{}'.format(settings.COURSE_ID_PATTERN)) def course_run_detail(self, request, pk, course_id): # pylint: disable=invalid-name,unused-argument """ Return the metadata for the specified course run. The course run needs to be included in the specified EnterpriseCustomerCatalog in order for metadata to be returned from this endpoint. """ enterprise_customer_catalog = self.get_object() course_run = enterprise_customer_catalog.get_course_run(course_id) if not course_run: raise Http404 context = self.get_serializer_context() context['enterprise_customer_catalog'] = enterprise_customer_catalog serializer = serializers.CourseRunDetailSerializer(course_run, context=context) return Response(serializer.data) @detail_route(url_path='programs/(?P<program_uuid>[^/]+)') def program_detail(self, request, pk, program_uuid): # pylint: disable=invalid-name,unused-argument """ Return the metadata for the specified program. The program needs to be included in the specified EnterpriseCustomerCatalog in order for metadata to be returned from this endpoint. """ enterprise_customer_catalog = self.get_object() program = enterprise_customer_catalog.get_program(program_uuid) if not program: raise Http404 context = self.get_serializer_context() context['enterprise_customer_catalog'] = enterprise_customer_catalog serializer = serializers.ProgramDetailSerializer(program, context=context) return Response(serializer.data)
class EnterpriseCustomerViewSet(EnterpriseReadWriteModelViewSet): """ API views for the ``enterprise-customer`` API endpoint. """ queryset = models.EnterpriseCustomer.active_customers.all() serializer_class = serializers.EnterpriseCustomerSerializer USER_ID_FILTER = 'enterprise_customer_users__user_id' FIELDS = ( 'uuid', 'slug', 'name', 'catalog', 'active', 'site', 'enable_data_sharing_consent', 'enforce_data_sharing_consent', ) filter_fields = FIELDS ordering_fields = FIELDS @method_decorator( require_at_least_one_query_parameter('course_run_ids', 'program_uuids')) @detail_route() # pylint: disable=invalid-name,unused-argument def contains_content_items(self, request, pk, course_run_ids, program_uuids): """ Return whether or not the specified content is available to the EnterpriseCustomer. Multiple course_run_ids and/or program_uuids query parameters can be sent to this view to check for their existence in the EnterpriseCustomerCatalogs associated with this EnterpriseCustomer. At least one course run key or program UUID value must be included in the request. """ enterprise_customer = self.get_object() # Maintain plus characters in course key. course_run_ids = [ unquote(quote_plus(course_run_id)) for course_run_id in course_run_ids ] contains_content_items = False for catalog in enterprise_customer.enterprise_customer_catalogs.all(): contains_course_runs = not course_run_ids or catalog.contains_courses( course_run_ids) contains_program_uuids = not program_uuids or catalog.contains_programs( program_uuids) if contains_course_runs and contains_program_uuids: contains_content_items = True break return Response({'contains_content_items': contains_content_items}) @detail_route() def courses(self, request, pk=None): # pylint: disable=invalid-name,unused-argument """ Retrieve the list of courses contained within the catalog linked to this enterprise. Only courses with active course runs are returned. A course run is considered active if it is currently open for enrollment, or will open in the future. """ enterprise_customer = self.get_object() self.check_object_permissions(request, enterprise_customer) self.ensure_data_exists( request, enterprise_customer.catalog, error_message= "No catalog is associated with Enterprise {enterprise_name} from endpoint '{path}'." .format(enterprise_name=enterprise_customer.name, path=request.get_full_path())) # We have handled potential error cases and are now ready to call out to the Catalog API. catalog_api = CourseCatalogApiClient(request.user, enterprise_customer.site) courses = catalog_api.get_paginated_catalog_courses( enterprise_customer.catalog, request.GET) # An empty response means that there was a problem fetching data from Catalog API, since # a Catalog with no courses has a non empty response indicating that there are no courses. self.ensure_data_exists( request, courses, error_message=( "Unable to fetch API response for catalog courses for " "Enterprise {enterprise_name} from endpoint '{path}'.".format( enterprise_name=enterprise_customer.name, path=request.get_full_path()))) serializer = serializers.EnterpriseCatalogCoursesReadOnlySerializer( courses) # Add enterprise related context for the courses. serializer.update_enterprise_courses( enterprise_customer, catalog_id=enterprise_customer.catalog) return get_paginated_response(serializer.data, request) @detail_route(methods=['post'], permission_classes=[ permissions.IsAuthenticated, HasEnterpriseEnrollmentAPIAccess, ]) # pylint: disable=invalid-name,unused-argument def course_enrollments(self, request, pk): """ Creates a course enrollment for an EnterpriseCustomerUser. """ enterprise_customer = self.get_object() serializer = serializers.EnterpriseCustomerCourseEnrollmentsSerializer( data=request.data, many=True, context={ 'enterprise_customer': enterprise_customer, 'request_user': request.user, }) if serializer.is_valid(): serializer.save() return Response(serializer.data, status=HTTP_200_OK) return Response(serializer.errors, status=HTTP_400_BAD_REQUEST) @method_decorator(require_at_least_one_query_parameter('permissions')) @list_route(permission_classes=[ permissions.IsAuthenticated, IsAdminUserOrInGroup, ]) def with_access_to(self, request, *args, **kwargs): # pylint: disable=invalid-name,unused-argument """ Returns the list of enterprise customers the user has a specified group permission access to. """ return self.list(request, *args, **kwargs)
class EnterpriseCustomerCatalogViewSet(EnterpriseReadOnlyModelViewSet): """ API Views for performing search through course discovery at the ``enterprise_catalogs`` API endpoint. """ queryset = models.EnterpriseCustomerCatalog.objects.all() USER_ID_FILTER = 'enterprise_customer__enterprise_customer_users__user_id' FIELDS = ( 'uuid', 'enterprise_customer', ) filterset_fields = FIELDS ordering_fields = FIELDS renderer_classes = ( JSONRenderer, XMLRenderer, ) @permission_required('enterprise.can_view_catalog', fn=lambda request, *args, **kwargs: None) def list(self, request, *args, **kwargs): return super().list(request, *args, **kwargs) # pylint: disable=no-member @permission_required('enterprise.can_view_catalog', fn=lambda request, *args, **kwargs: get_enterprise_customer_from_catalog_id(kwargs['pk'])) def retrieve(self, request, *args, **kwargs): return super().retrieve(request, *args, **kwargs) def get_serializer_class(self): view_action = getattr(self, 'action', None) if view_action == 'retrieve': return serializers.EnterpriseCustomerCatalogDetailSerializer return serializers.EnterpriseCustomerCatalogSerializer @method_decorator( require_at_least_one_query_parameter('course_run_ids', 'program_uuids')) @action(detail=True) # pylint: disable=invalid-name,unused-argument def contains_content_items(self, request, pk, course_run_ids, program_uuids): """ Return whether or not the EnterpriseCustomerCatalog contains the specified content. Multiple course_run_ids and/or program_uuids query parameters can be sent to this view to check for their existence in the EnterpriseCustomerCatalog. At least one course run key or program UUID value must be included in the request. """ enterprise_customer_catalog = self.get_object() # Maintain plus characters in course key. course_run_ids = [ unquote(quote_plus(course_run_id)) for course_run_id in course_run_ids ] contains_content_items = True if course_run_ids: contains_content_items = enterprise_customer_catalog.contains_courses( course_run_ids) if program_uuids: contains_content_items = ( contains_content_items and enterprise_customer_catalog.contains_programs(program_uuids)) return Response({'contains_content_items': contains_content_items}) @action(detail=True, url_path='courses/{}'.format(COURSE_KEY_URL_PATTERN)) @permission_required('enterprise.can_view_catalog', fn=lambda request, pk, course_key: get_enterprise_customer_from_catalog_id(pk)) def course_detail(self, request, pk, course_key): # pylint: disable=invalid-name,unused-argument """ Return the metadata for the specified course. The course needs to be included in the specified EnterpriseCustomerCatalog in order for metadata to be returned from this endpoint. """ enterprise_customer_catalog = self.get_object() course = enterprise_customer_catalog.get_course(course_key) if not course: error_message = _( '[Enterprise API] CourseKey not found in the Catalog. Course: {course_key}, Catalog: {catalog_id}' ).format( course_key=course_key, catalog_id=enterprise_customer_catalog.uuid, ) LOGGER.warning(error_message) raise Http404 context = self.get_serializer_context() context['enterprise_customer_catalog'] = enterprise_customer_catalog serializer = serializers.CourseDetailSerializer(course, context=context) return Response(serializer.data) @action(detail=True, url_path='course_runs/{}'.format(settings.COURSE_ID_PATTERN)) @permission_required('enterprise.can_view_catalog', fn=lambda request, pk, course_id: get_enterprise_customer_from_catalog_id(pk)) def course_run_detail(self, request, pk, course_id): # pylint: disable=invalid-name,unused-argument """ Return the metadata for the specified course run. The course run needs to be included in the specified EnterpriseCustomerCatalog in order for metadata to be returned from this endpoint. """ enterprise_customer_catalog = self.get_object() course_run = enterprise_customer_catalog.get_course_run(course_id) if not course_run: error_message = _( '[Enterprise API] CourseRun not found in the Catalog. CourseRun: {course_id}, Catalog: {catalog_id}' ).format( course_id=course_id, catalog_id=enterprise_customer_catalog.uuid, ) LOGGER.warning(error_message) raise Http404 context = self.get_serializer_context() context['enterprise_customer_catalog'] = enterprise_customer_catalog serializer = serializers.CourseRunDetailSerializer(course_run, context=context) return Response(serializer.data) @action(detail=True, url_path='programs/(?P<program_uuid>[^/]+)') @permission_required('enterprise.can_view_catalog', fn=lambda request, pk, program_uuid: get_enterprise_customer_from_catalog_id(pk)) def program_detail(self, request, pk, program_uuid): # pylint: disable=invalid-name,unused-argument """ Return the metadata for the specified program. The program needs to be included in the specified EnterpriseCustomerCatalog in order for metadata to be returned from this endpoint. """ enterprise_customer_catalog = self.get_object() program = enterprise_customer_catalog.get_program(program_uuid) if not program: error_message = _( '[Enterprise API] Program not found in the Catalog. Program: {program_uuid}, Catalog: {catalog_id}' ).format( program_uuid=program_uuid, catalog_id=enterprise_customer_catalog.uuid, ) LOGGER.warning(error_message) raise Http404 context = self.get_serializer_context() context['enterprise_customer_catalog'] = enterprise_customer_catalog serializer = serializers.ProgramDetailSerializer(program, context=context) return Response(serializer.data)
class EnterpriseCustomerViewSet(EnterpriseReadWriteModelViewSet): """ API views for the ``enterprise-customer`` API endpoint. """ queryset = models.EnterpriseCustomer.active_customers.all() serializer_class = serializers.EnterpriseCustomerSerializer USER_ID_FILTER = 'enterprise_customer_users__user_id' FIELDS = ( 'uuid', 'slug', 'name', 'active', 'site', 'enable_data_sharing_consent', 'enforce_data_sharing_consent', ) filterset_fields = FIELDS ordering_fields = FIELDS def get_serializer_class(self): if self.action == 'basic_list': return serializers.EnterpriseCustomerBasicSerializer return self.serializer_class @list_route() # pylint: disable=invalid-name,unused-argument def basic_list(self, request, *arg, **kwargs): """ Enterprise Customer's Basic data list without pagination """ startswith = request.GET.get('startswith') queryset = self.get_queryset().order_by('name') if startswith: queryset = queryset.filter(name__istartswith=startswith) serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) @method_decorator( require_at_least_one_query_parameter('course_run_ids', 'program_uuids')) @detail_route() @permission_required( 'enterprise.can_view_catalog', fn=lambda request, pk, course_run_ids, program_uuids: pk) # pylint: disable=invalid-name,unused-argument def contains_content_items(self, request, pk, course_run_ids, program_uuids): """ Return whether or not the specified content is available to the EnterpriseCustomer. Multiple course_run_ids and/or program_uuids query parameters can be sent to this view to check for their existence in the EnterpriseCustomerCatalogs associated with this EnterpriseCustomer. At least one course run key or program UUID value must be included in the request. """ enterprise_customer = self.get_object() # Maintain plus characters in course key. course_run_ids = [ unquote(quote_plus(course_run_id)) for course_run_id in course_run_ids ] contains_content_items = False for catalog in enterprise_customer.enterprise_customer_catalogs.all(): contains_course_runs = not course_run_ids or catalog.contains_courses( course_run_ids) contains_program_uuids = not program_uuids or catalog.contains_programs( program_uuids) if contains_course_runs and contains_program_uuids: contains_content_items = True break return Response({'contains_content_items': contains_content_items}) @detail_route(methods=['post'], permission_classes=[permissions.IsAuthenticated]) @permission_required('enterprise.can_enroll_learners', fn=lambda request, pk: pk) # pylint: disable=invalid-name,unused-argument def course_enrollments(self, request, pk): """ Creates a course enrollment for an EnterpriseCustomerUser. """ enterprise_customer = self.get_object() serializer = serializers.EnterpriseCustomerCourseEnrollmentsSerializer( data=request.data, many=True, context={ 'enterprise_customer': enterprise_customer, 'request_user': request.user, }) if serializer.is_valid(): serializer.save() return Response(serializer.data, status=HTTP_200_OK) return Response(serializer.errors, status=HTTP_400_BAD_REQUEST) @method_decorator(require_at_least_one_query_parameter('permissions')) @list_route( permission_classes=[permissions.IsAuthenticated, IsInEnterpriseGroup]) def with_access_to(self, request, *args, **kwargs): # pylint: disable=invalid-name,unused-argument """ Returns the list of enterprise customers the user has a specified group permission access to. """ self.queryset = self.queryset.order_by('name') enterprise_id = self.request.query_params.get('enterprise_id', None) enterprise_slug = self.request.query_params.get( 'enterprise_slug', None) enterprise_name = self.request.query_params.get('search', None) if enterprise_id is not None: self.queryset = self.queryset.filter(uuid=enterprise_id) elif enterprise_slug is not None: self.queryset = self.queryset.filter(slug=enterprise_slug) elif enterprise_name is not None: self.queryset = self.queryset.filter( name__icontains=enterprise_name) return self.list(request, *args, **kwargs) @list_route() @permission_required('enterprise.can_access_admin_dashboard') def dashboard_list(self, request, *args, **kwargs): # pylint: disable=invalid-name,unused-argument """ Supports listing dashboard enterprises for frontend-app-admin-portal. """ self.queryset = self.queryset.order_by('name') enterprise_id = self.request.query_params.get('enterprise_id', None) enterprise_slug = self.request.query_params.get( 'enterprise_slug', None) enterprise_name = self.request.query_params.get('search', None) if enterprise_id is not None: self.queryset = self.queryset.filter(uuid=enterprise_id) elif enterprise_slug is not None: self.queryset = self.queryset.filter(slug=enterprise_slug) elif enterprise_name is not None: self.queryset = self.queryset.filter( name__icontains=enterprise_name) return self.list(request, *args, **kwargs)
class EnterpriseCustomerViewSet(EnterpriseReadWriteModelViewSet): """ API views for the ``enterprise-customer`` API endpoint. """ queryset = models.EnterpriseCustomer.active_customers.all() serializer_class = serializers.EnterpriseCustomerSerializer filter_backends = EnterpriseReadWriteModelViewSet.filter_backends + ( EnterpriseLinkedUserFilterBackend, ) USER_ID_FILTER = 'enterprise_customer_users__user_id' FIELDS = ( 'uuid', 'slug', 'name', 'active', 'site', 'enable_data_sharing_consent', 'enforce_data_sharing_consent', ) filterset_fields = FIELDS ordering_fields = FIELDS def get_serializer_class(self): if self.action == 'basic_list': return serializers.EnterpriseCustomerBasicSerializer return self.serializer_class @action(detail=False) # pylint: disable=invalid-name,unused-argument def basic_list(self, request, *arg, **kwargs): """ Enterprise Customer's Basic data list without pagination """ startswith = request.GET.get('startswith') queryset = self.get_queryset().order_by('name') if startswith: queryset = queryset.filter(name__istartswith=startswith) serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) @method_decorator( require_at_least_one_query_parameter('course_run_ids', 'program_uuids')) @action(detail=True) @permission_required( 'enterprise.can_view_catalog', fn=lambda request, pk, course_run_ids, program_uuids: pk) # pylint: disable=invalid-name,unused-argument def contains_content_items(self, request, pk, course_run_ids, program_uuids): """ Return whether or not the specified content is available to the EnterpriseCustomer. Multiple course_run_ids and/or program_uuids query parameters can be sent to this view to check for their existence in the EnterpriseCustomerCatalogs associated with this EnterpriseCustomer. At least one course run key or program UUID value must be included in the request. """ enterprise_customer = self.get_object() # Maintain plus characters in course key. course_run_ids = [ unquote(quote_plus(course_run_id)) for course_run_id in course_run_ids ] contains_content_items = False for catalog in enterprise_customer.enterprise_customer_catalogs.all(): contains_course_runs = not course_run_ids or catalog.contains_courses( course_run_ids) contains_program_uuids = not program_uuids or catalog.contains_programs( program_uuids) if contains_course_runs and contains_program_uuids: contains_content_items = True break return Response({'contains_content_items': contains_content_items}) @action(methods=['post'], permission_classes=[permissions.IsAuthenticated], detail=True) @permission_required('enterprise.can_enroll_learners', fn=lambda request, pk: pk) # pylint: disable=invalid-name,unused-argument def course_enrollments(self, request, pk): """ Creates a course enrollment for an EnterpriseCustomerUser. """ enterprise_customer = self.get_object() serializer = serializers.EnterpriseCustomerCourseEnrollmentsSerializer( data=request.data, many=True, context={ 'enterprise_customer': enterprise_customer, 'request_user': request.user, }) if serializer.is_valid(): serializer.save() return Response(serializer.data, status=HTTP_200_OK) return Response(serializer.errors, status=HTTP_400_BAD_REQUEST) @action(detail=True, methods=['post'], permission_classes=[permissions.IsAuthenticated]) @permission_required('enterprise.can_enroll_learners', fn=lambda request, pk: pk) # pylint: disable=invalid-name,unused-argument def enterprise_learners(self, request, pk): """ Creates a set of enterprise_learners by enrolling them in the specified course. """ enterprise_customer = self.get_object() serializer = serializers.EnterpriseCustomerBulkEnrollmentsSerializer( data=request.data, context={ 'enterprise_customer': enterprise_customer, 'request_user': request.user, }) if serializer.is_valid(raise_exception=True): singular_email = serializer.validated_data.get('email') emails = set() already_linked_emails = [] duplicate_emails = [] errors = [] if singular_email: emails.add(singular_email) try: for email in emails: try: already_linked = validate_email_to_link( email, ignore_existing=True) except ValidationError as error: errors.append(error) else: if already_linked: already_linked_emails.append( (email, already_linked.enterprise_customer)) elif email in emails: duplicate_emails.append(email) else: emails.add(email) except ValidationError as exc: errors.append(exc) if errors: return Response(errors, status=HTTP_400_BAD_REQUEST) for email in emails: models.EnterpriseCustomerUser.objects.link_user( enterprise_customer, email) course_run_key = serializer.validated_data.get('course_run_key') mode = serializer.validated_data.get('course_mode') if course_run_key: this_customer_linked_emails = [ email for email, customer in already_linked_emails if customer == enterprise_customer ] linked_learners = list(emails) + this_customer_linked_emails if linked_learners: discount = serializer.validated_data.get('discount', 0.0) enrollment_reason = serializer.validated_data.get('reason') succeeded, pending, _ = enroll_users_in_course( enterprise_customer=enterprise_customer, course_id=course_run_key, course_mode=mode, emails=emails, enrollment_requester=request.user, enrollment_reason=enrollment_reason, discount=discount, sales_force_id=serializer.validated_data.get( 'salesforce_id'), ) if serializer.validated_data.get('notify'): enterprise_customer.notify_enrolled_learners( catalog_api_user=request.user, course_id=course_run_key, users=succeeded + pending, ) paid_modes = ['verified', 'professional'] if mode in paid_modes: enrollments = [{ "lms_user_id": success.id, "email": success.email, "username": success.username, "course_run_key": course_run_key, "discount_percentage": float(discount), "enterprise_customer_name": enterprise_customer.name, "enterprise_customer_uuid": str(enterprise_customer.uuid), "mode": mode, "sales_force_id": serializer.validated_data.get('salesforce_id'), } for success in succeeded] EcommerceApiClient(get_ecommerce_worker_user( )).create_manual_enrollment_orders(enrollments) return Response(status=HTTP_202_ACCEPTED) return Response(status=HTTP_400_BAD_REQUEST) @method_decorator(require_at_least_one_query_parameter('permissions')) @action( permission_classes=[permissions.IsAuthenticated, IsInEnterpriseGroup], detail=False) def with_access_to(self, request, *args, **kwargs): # pylint: disable=invalid-name,unused-argument """ Returns the list of enterprise customers the user has a specified group permission access to. """ self.queryset = self.queryset.order_by('name') enterprise_id = self.request.query_params.get('enterprise_id', None) enterprise_slug = self.request.query_params.get( 'enterprise_slug', None) enterprise_name = self.request.query_params.get('search', None) if enterprise_id is not None: self.queryset = self.queryset.filter(uuid=enterprise_id) elif enterprise_slug is not None: self.queryset = self.queryset.filter(slug=enterprise_slug) elif enterprise_name is not None: self.queryset = self.queryset.filter( name__icontains=enterprise_name) return self.list(request, *args, **kwargs) @action(detail=False) @permission_required('enterprise.can_access_admin_dashboard') def dashboard_list(self, request, *args, **kwargs): # pylint: disable=invalid-name,unused-argument """ Supports listing dashboard enterprises for frontend-app-admin-portal. """ self.queryset = self.queryset.order_by('name') enterprise_id = self.request.query_params.get('enterprise_id', None) enterprise_slug = self.request.query_params.get( 'enterprise_slug', None) enterprise_name = self.request.query_params.get('search', None) if enterprise_id is not None: self.queryset = self.queryset.filter(uuid=enterprise_id) elif enterprise_slug is not None: self.queryset = self.queryset.filter(slug=enterprise_slug) elif enterprise_name is not None: self.queryset = self.queryset.filter( name__icontains=enterprise_name) return self.list(request, *args, **kwargs)