def is_course_in_catalog(self, catalog_id, course_id):
        """
        Determine if the given course or course run ID is contained in the catalog with the given ID.

        Args:
            catalog_id (int): The ID of the catalog
            course_id (str): The ID of the course or course run

        Returns:
            bool: Whether the course or course run is contained in the given catalog
        """
        try:
            # Determine if we have a course run ID, rather than a plain course ID
            course_run_id = str(CourseKey.from_string(course_id))
        except InvalidKeyError:
            course_run_id = None

        endpoint = self.client.catalogs(catalog_id).contains

        if course_run_id:
            resp = endpoint.get(course_run_id=course_run_id)
        else:
            resp = endpoint.get(course_id=course_id)

        return resp.get('courses', {}).get(course_id, False)

def is_course_in_catalog ( self , catalog_id , course_id ) : try : course_run_id = str ( CourseKey . from_string ( course_id ) ) except InvalidKeyError : course_run_id = None endpoint = self . client . catalogs ( catalog_id ) . contains if course_run_id : resp = endpoint . get ( course_run_id = course_run_id ) else : resp = endpoint . get ( course_id = course_id ) return resp . get ( 'courses' , { } ) . get ( course_id , False )

Determine if the given course or course run ID is contained in the catalog with the given ID .


def _load_data(self, resource, default=DEFAULT_VALUE_SAFEGUARD, **kwargs):
        """
        Load data from API client.

        Arguments:
            resource(string): type of resource to load
            default(any): value to return if API query returned empty result. Sensible values: [], {}, None etc.

        Returns:
            dict: Deserialized response from Course Catalog API

        """
        default_val = default if default != self.DEFAULT_VALUE_SAFEGUARD else {}
        try:
            return get_edx_api_data(
                api_config=CatalogIntegration.current(),
                resource=resource,
                api=self.client,
                **kwargs
            ) or default_val
        except (SlumberBaseException, ConnectionError, Timeout) as exc:
            LOGGER.exception(
                'Failed to load data from resource [%s] with kwargs [%s] due to: [%s]',
                resource, kwargs, str(exc)
            )
            return default_val


def get_content_metadata(self, enterprise_customer):
        """
        Return all content metadata contained in the catalogs associated with the EnterpriseCustomer.

        Arguments:
            enterprise_customer (EnterpriseCustomer): The EnterpriseCustomer to return content metadata for.

        Returns:
            list: List of dicts containing content metadata.
        """
        content_metadata = OrderedDict()

        # TODO: This if block can be removed when we get rid of discovery service-based catalogs.
        if enterprise_customer.catalog:
            response = self._load_data(
                self.ENTERPRISE_CUSTOMER_ENDPOINT,
                detail_resource='courses',
                resource_id=str(enterprise_customer.uuid),
                traverse_pagination=True,
            )
            for course in response['results']:
                for course_run in course['course_runs']:
                    course_run['content_type'] = 'courserun'  # Make this look like a search endpoint result.
                    content_metadata[course_run['key']] = course_run

        for enterprise_customer_catalog in enterprise_customer.enterprise_customer_catalogs.all():
            response = self._load_data(
                self.ENTERPRISE_CUSTOMER_CATALOGS_ENDPOINT,
                resource_id=str(enterprise_customer_catalog.uuid),
                traverse_pagination=True,
                querystring={'page_size': 1000},
            )

            for item in response['results']:
                content_id = utils.get_content_metadata_item_id(item)
                content_metadata[content_id] = item

        return content_metadata.values()

def get_content_metadata ( self , enterprise_customer ) : content_metadata = OrderedDict ( ) if enterprise_customer . catalog : response = self . _load_data ( self . ENTERPRISE_CUSTOMER_ENDPOINT , detail_resource = 'courses' , resource_id = str ( enterprise_customer . uuid ) , traverse_pagination = True , ) for course in response [ 'results' ] : for course_run in course [ 'course_runs' ] : course_run [ 'content_type' ] = 'courserun' content_metadata [ course_run [ 'key' ] ] = course_run for enterprise_customer_catalog in enterprise_customer . enterprise_customer_catalogs . all ( ) : response = self . _load_data ( self . ENTERPRISE_CUSTOMER_CATALOGS_ENDPOINT , resource_id = str ( enterprise_customer_catalog . uuid ) , traverse_pagination = True , querystring = { 'page_size' : 1000 } , ) for item in response [ 'results' ] : content_id = utils . get_content_metadata_item_id ( item ) content_metadata [ content_id ] = item return content_metadata . values ( )

Return all content metadata contained in the catalogs associated with the EnterpriseCustomer .


def _load_data(
            self,
            resource,
            detail_resource=None,
            resource_id=None,
            querystring=None,
            traverse_pagination=False,
            default=DEFAULT_VALUE_SAFEGUARD,
    ):
        """
        Loads a response from a call to one of the Enterprise endpoints.

        :param resource: The endpoint resource name.
        :param detail_resource: The sub-resource to append to the path.
        :param resource_id: The resource ID for the specific detail to get from the endpoint.
        :param querystring: Optional query string parameters.
        :param traverse_pagination: Whether to traverse pagination or return paginated response.
        :param default: The default value to return in case of no response content.
        :return: Data returned by the API.
        """
        default_val = default if default != self.DEFAULT_VALUE_SAFEGUARD else {}
        querystring = querystring if querystring else {}

        cache_key = utils.get_cache_key(
            resource=resource,
            querystring=querystring,
            traverse_pagination=traverse_pagination,
            resource_id=resource_id
        )
        response = cache.get(cache_key)
        if not response:
            # Response is not cached, so make a call.
            endpoint = getattr(self.client, resource)(resource_id)
            endpoint = getattr(endpoint, detail_resource) if detail_resource else endpoint
            response = endpoint.get(**querystring)
            if traverse_pagination:
                results = utils.traverse_pagination(response, endpoint)
                response = {
                    'count': len(results),
                    'next': 'None',
                    'previous': 'None',
                    'results': results,
                }
            if response:
                # Now that we've got a response, cache it.
                cache.set(cache_key, response, settings.ENTERPRISE_API_CACHE_TIMEOUT)
        return response or default_val

def _load_data ( self , resource , detail_resource = None , resource_id = None , querystring = None , traverse_pagination = False , default = DEFAULT_VALUE_SAFEGUARD , ) : default_val = default if default != self . DEFAULT_VALUE_SAFEGUARD else { } querystring = querystring if querystring else { } cache_key = utils . get_cache_key ( resource = resource , querystring = querystring , traverse_pagination = traverse_pagination , resource_id = resource_id ) response = cache . get ( cache_key ) if not response : endpoint = getattr ( self . client , resource ) ( resource_id ) endpoint = getattr ( endpoint , detail_resource ) if detail_resource else endpoint response = endpoint . get ( ** querystring ) if traverse_pagination : results = utils . traverse_pagination ( response , endpoint ) response = { 'count' : len ( results ) , 'next' : 'None' , 'previous' : 'None' , 'results' : results , } if response : cache . set ( cache_key , response , settings . ENTERPRISE_API_CACHE_TIMEOUT ) return response or default_val

Loads a response from a call to one of the Enterprise endpoints .


def _partition_items(self, channel_metadata_item_map):
        """
        Return items that need to be created, updated, and deleted along with the
        current ContentMetadataItemTransmissions.
        """
        items_to_create = {}
        items_to_update = {}
        items_to_delete = {}
        transmission_map = {}
        export_content_ids = channel_metadata_item_map.keys()

        # Get the items that were previously transmitted to the integrated channel.
        # If we are not transmitting something that was previously transmitted,
        # we need to delete it from the integrated channel.
        for transmission in self._get_transmissions():
            transmission_map[transmission.content_id] = transmission
            if transmission.content_id not in export_content_ids:
                items_to_delete[transmission.content_id] = transmission.channel_metadata

        # Compare what is currently being transmitted to what was transmitted
        # previously, identifying items that need to be created or updated.
        for item in channel_metadata_item_map.values():
            content_id = item.content_id
            channel_metadata = item.channel_metadata
            transmitted_item = transmission_map.get(content_id, None)
            if transmitted_item is not None:
                if diff(channel_metadata, transmitted_item.channel_metadata):
                    items_to_update[content_id] = channel_metadata
            else:
                items_to_create[content_id] = channel_metadata

        LOGGER.info(
            'Preparing to transmit creation of [%s] content metadata items with plugin configuration [%s]: [%s]',
            len(items_to_create),
            self.enterprise_configuration,
            items_to_create.keys(),
        )
        LOGGER.info(
            'Preparing to transmit update of [%s] content metadata items with plugin configuration [%s]: [%s]',
            len(items_to_update),
            self.enterprise_configuration,
            items_to_update.keys(),
        )
        LOGGER.info(
            'Preparing to transmit deletion of [%s] content metadata items with plugin configuration [%s]: [%s]',
            len(items_to_delete),
            self.enterprise_configuration,
            items_to_delete.keys(),
        )

        return items_to_create, items_to_update, items_to_delete, transmission_map

Return items that need to be created updated and deleted along with the current ContentMetadataItemTransmissions .


def _serialize_items(self, channel_metadata_items):
        """
        Serialize content metadata items for a create transmission to the integrated channel.
        """
        return json.dumps(
            self._prepare_items_for_transmission(channel_metadata_items),
            sort_keys=True
        ).encode('utf-8')

def _serialize_items ( self , channel_metadata_items ) : return json . dumps ( self . _prepare_items_for_transmission ( channel_metadata_items ) , sort_keys = True ) . encode ( 'utf-8' )

Serialize content metadata items for a create transmission to the integrated channel .


def _transmit_create(self, channel_metadata_item_map):
        """
        Transmit content metadata creation to integrated channel.
        """
        for chunk in chunks(channel_metadata_item_map, self.enterprise_configuration.transmission_chunk_size):
            serialized_chunk = self._serialize_items(list(chunk.values()))
            try:
                self.client.create_content_metadata(serialized_chunk)
            except ClientError as exc:
                LOGGER.error(
                    'Failed to update [%s] content metadata items for integrated channel [%s] [%s]',
                    len(chunk),
                    self.enterprise_configuration.enterprise_customer.name,
                    self.enterprise_configuration.channel_code,
                )
                LOGGER.error(exc)
            else:
                self._create_transmissions(chunk)

Transmit content metadata creation to integrated channel .


def _transmit_update(self, channel_metadata_item_map, transmission_map):
        """
        Transmit content metadata update to integrated channel.
        """
        for chunk in chunks(channel_metadata_item_map, self.enterprise_configuration.transmission_chunk_size):
            serialized_chunk = self._serialize_items(list(chunk.values()))
            try:
                self.client.update_content_metadata(serialized_chunk)
            except ClientError as exc:
                LOGGER.error(
                    'Failed to update [%s] content metadata items for integrated channel [%s] [%s]',
                    len(chunk),
                    self.enterprise_configuration.enterprise_customer.name,
                    self.enterprise_configuration.channel_code,
                )
                LOGGER.error(exc)
            else:
                self._update_transmissions(chunk, transmission_map)

Transmit content metadata update to integrated channel .


def _transmit_delete(self, channel_metadata_item_map):
        """
        Transmit content metadata deletion to integrated channel.
        """
        for chunk in chunks(channel_metadata_item_map, self.enterprise_configuration.transmission_chunk_size):
            serialized_chunk = self._serialize_items(list(chunk.values()))
            try:
                self.client.delete_content_metadata(serialized_chunk)
            except ClientError as exc:
                LOGGER.error(
                    'Failed to delete [%s] content metadata items for integrated channel [%s] [%s]',
                    len(chunk),
                    self.enterprise_configuration.enterprise_customer.name,
                    self.enterprise_configuration.channel_code,
                )
                LOGGER.error(exc)
            else:
                self._delete_transmissions(chunk.keys())

Transmit content metadata deletion to integrated channel .


def _get_transmissions(self):
        """
        Return the ContentMetadataItemTransmision models for previously
        transmitted content metadata items.
        """
        # pylint: disable=invalid-name
        ContentMetadataItemTransmission = apps.get_model(
            'integrated_channel',
            'ContentMetadataItemTransmission'
        )
        return ContentMetadataItemTransmission.objects.filter(
            enterprise_customer=self.enterprise_configuration.enterprise_customer,
            integrated_channel_code=self.enterprise_configuration.channel_code()
        )

def _get_transmissions ( self ) : ContentMetadataItemTransmission = apps . get_model ( 'integrated_channel' , 'ContentMetadataItemTransmission' ) return ContentMetadataItemTransmission . objects . filter ( enterprise_customer = self . enterprise_configuration . enterprise_customer , integrated_channel_code = self . enterprise_configuration . channel_code ( ) )

Return the ContentMetadataItemTransmision models for previously transmitted content metadata items .


def _create_transmissions(self, content_metadata_item_map):
        """
        Create ContentMetadataItemTransmision models for the given content metadata items.
        """
        # pylint: disable=invalid-name
        ContentMetadataItemTransmission = apps.get_model(
            'integrated_channel',
            'ContentMetadataItemTransmission'
        )
        transmissions = []
        for content_id, channel_metadata in content_metadata_item_map.items():
            transmissions.append(
                ContentMetadataItemTransmission(
                    enterprise_customer=self.enterprise_configuration.enterprise_customer,
                    integrated_channel_code=self.enterprise_configuration.channel_code(),
                    content_id=content_id,
                    channel_metadata=channel_metadata
                )
            )
        ContentMetadataItemTransmission.objects.bulk_create(transmissions)

def _create_transmissions ( self , content_metadata_item_map ) : ContentMetadataItemTransmission = apps . get_model ( 'integrated_channel' , 'ContentMetadataItemTransmission' ) transmissions = [ ] for content_id , channel_metadata in content_metadata_item_map . items ( ) : transmissions . append ( ContentMetadataItemTransmission ( enterprise_customer = self . enterprise_configuration . enterprise_customer , integrated_channel_code = self . enterprise_configuration . channel_code ( ) , content_id = content_id , channel_metadata = channel_metadata ) ) ContentMetadataItemTransmission . objects . bulk_create ( transmissions )

Create ContentMetadataItemTransmision models for the given content metadata items .


def _update_transmissions(self, content_metadata_item_map, transmission_map):
        """
        Update ContentMetadataItemTransmision models for the given content metadata items.
        """
        for content_id, channel_metadata in content_metadata_item_map.items():
            transmission = transmission_map[content_id]
            transmission.channel_metadata = channel_metadata
            transmission.save()

def _update_transmissions ( self , content_metadata_item_map , transmission_map ) : for content_id , channel_metadata in content_metadata_item_map . items ( ) : transmission = transmission_map [ content_id ] transmission . channel_metadata = channel_metadata transmission . save ( )

Update ContentMetadataItemTransmision models for the given content metadata items .


def _delete_transmissions(self, content_metadata_item_ids):
        """
        Delete ContentMetadataItemTransmision models associated with the given content metadata items.
        """
        # pylint: disable=invalid-name
        ContentMetadataItemTransmission = apps.get_model(
            'integrated_channel',
            'ContentMetadataItemTransmission'
        )
        ContentMetadataItemTransmission.objects.filter(
            enterprise_customer=self.enterprise_configuration.enterprise_customer,
            integrated_channel_code=self.enterprise_configuration.channel_code(),
            content_id__in=content_metadata_item_ids
        ).delete()

def _delete_transmissions ( self , content_metadata_item_ids ) : ContentMetadataItemTransmission = apps . get_model ( 'integrated_channel' , 'ContentMetadataItemTransmission' ) ContentMetadataItemTransmission . objects . filter ( enterprise_customer = self . enterprise_configuration . enterprise_customer , integrated_channel_code = self . enterprise_configuration . channel_code ( ) , content_id__in = content_metadata_item_ids ) . delete ( )

Delete ContentMetadataItemTransmision models associated with the given content metadata items .


def deprecated(extra):
    """
    Flag a method as deprecated.

    :param extra: Extra text you'd like to display after the default text.
    """
    def decorator(func):
        """
        Return a decorated function that emits a deprecation warning on use.
        """
        @wraps(func)
        def wrapper(*args, **kwargs):
            """
            Wrap the function.
            """
            message = 'You called the deprecated function `{function}`. {extra}'.format(
                function=func.__name__,
                extra=extra
            )
            frame = inspect.currentframe().f_back
            warnings.warn_explicit(
                message,
                category=DeprecationWarning,
                filename=inspect.getfile(frame.f_code),
                lineno=frame.f_lineno
            )
            return func(*args, **kwargs)
        return wrapper
    return decorator


def ignore_warning(warning):
    """
    Ignore any emitted warnings from a function.

    :param warning: The category of warning to ignore.
    """
    def decorator(func):
        """
        Return a decorated function whose emitted warnings are ignored.
        """
        @wraps(func)
        def wrapper(*args, **kwargs):
            """
            Wrap the function.
            """
            warnings.simplefilter('ignore', warning)
            return func(*args, **kwargs)
        return wrapper
    return decorator

def ignore_warning ( warning ) : def decorator ( func ) : @ wraps ( func ) def wrapper ( * args , ** kwargs ) : warnings . simplefilter ( 'ignore' , warning ) return func ( * args , ** kwargs ) return wrapper return decorator

Ignore any emitted warnings from a function .


def enterprise_login_required(view):
    """
    View decorator for allowing authenticated user with valid enterprise UUID.

    This decorator requires enterprise identifier as a parameter
    `enterprise_uuid`.

    This decorator will throw 404 if no kwarg `enterprise_uuid` is provided to
    the decorated view .

    If there is no enterprise in database against the kwarg `enterprise_uuid`
    or if the user is not authenticated then it will redirect the user to the
    enterprise-linked SSO login page.

    Usage::
        @enterprise_login_required()
        def my_view(request, enterprise_uuid):
            # Some functionality ...

        OR

        class MyView(View):
            ...
            @method_decorator(enterprise_login_required)
            def get(self, request, enterprise_uuid):
                # Some functionality ...

    """
    @wraps(view)
    def wrapper(request, *args, **kwargs):
        """
        Wrap the decorator.
        """
        if 'enterprise_uuid' not in kwargs:
            raise Http404

        enterprise_uuid = kwargs['enterprise_uuid']
        enterprise_customer = get_enterprise_customer_or_404(enterprise_uuid)

        # Now verify if the user is logged in. If user is not logged in then
        # send the user to the login screen to sign in with an
        # Enterprise-linked IdP and the pipeline will get them back here.
        if not request.user.is_authenticated:
            parsed_current_url = urlparse(request.get_full_path())
            parsed_query_string = parse_qs(parsed_current_url.query)
            parsed_query_string.update({
                'tpa_hint': enterprise_customer.identity_provider,
                FRESH_LOGIN_PARAMETER: 'yes'
            })
            next_url = '{current_path}?{query_string}'.format(
                current_path=quote(parsed_current_url.path),
                query_string=urlencode(parsed_query_string, doseq=True)
            )
            return redirect(
                '{login_url}?{params}'.format(
                    login_url='/login',
                    params=urlencode(
                        {'next': next_url}
                    )
                )
            )

        # Otherwise, they can proceed to the original view.
        return view(request, *args, **kwargs)

    return wrapper

def enterprise_login_required ( view ) : @ wraps ( view ) def wrapper ( request , * args , ** kwargs ) : if 'enterprise_uuid' not in kwargs : raise Http404 enterprise_uuid = kwargs [ 'enterprise_uuid' ] enterprise_customer = get_enterprise_customer_or_404 ( enterprise_uuid ) if not request . user . is_authenticated : parsed_current_url = urlparse ( request . get_full_path ( ) ) parsed_query_string = parse_qs ( parsed_current_url . query ) parsed_query_string . update ( { 'tpa_hint' : enterprise_customer . identity_provider , FRESH_LOGIN_PARAMETER : 'yes' } ) next_url = '{current_path}?{query_string}' . format ( current_path = quote ( parsed_current_url . path ) , query_string = urlencode ( parsed_query_string , doseq = True ) ) return redirect ( '{login_url}?{params}' . format ( login_url = '/login' , params = urlencode ( { 'next' : next_url } ) ) ) return view ( request , * args , ** kwargs ) return wrapper

View decorator for allowing authenticated user with valid enterprise UUID .


def force_fresh_session(view):
    """
    View decorator which terminates stale TPA sessions.

    This decorator forces the user to obtain a new session
    the first time they access the decorated view. This prevents
    TPA-authenticated users from hijacking the session of another
    user who may have been previously logged in using the same
    browser window.

    This decorator should be used in conjunction with the
    enterprise_login_required decorator.

    Usage::
        @enterprise_login_required
        @force_fresh_session()
        def my_view(request, enterprise_uuid):
            # Some functionality ...

        OR

        class MyView(View):
            ...
            @method_decorator(enterprise_login_required)
            @method_decorator(force_fresh_session)
            def get(self, request, enterprise_uuid):
                # Some functionality ...
    """
    @wraps(view)
    def wrapper(request, *args, **kwargs):
        """
        Wrap the function.
        """
        if not request.GET.get(FRESH_LOGIN_PARAMETER):
            # The enterprise_login_required decorator promises to set the fresh login URL
            # parameter for this URL when it was the agent that initiated the login process;
            # if that parameter isn't set, we can safely assume that the session is "stale";
            # that isn't necessarily an issue, though. Redirect the user to
            # log out and then come back here - the enterprise_login_required decorator will
            # then take effect prior to us arriving back here again.
            enterprise_customer = get_enterprise_customer_or_404(kwargs.get('enterprise_uuid'))
            provider_id = enterprise_customer.identity_provider or ''
            sso_provider = get_identity_provider(provider_id)
            if sso_provider:
                # Parse the current request full path, quote just the path portion,
                # then reconstruct the full path string.
                # The path and query portions should be the only non-empty strings here.
                scheme, netloc, path, params, query, fragment = urlparse(request.get_full_path())
                redirect_url = urlunparse((scheme, netloc, quote(path), params, query, fragment))

                return redirect(
                    '{logout_url}?{params}'.format(
                        logout_url='/logout',
                        params=urlencode(
                            {'redirect_url': redirect_url}
                        )
                    )
                )
        return view(request, *args, **kwargs)

    return wrapper

def force_fresh_session ( view ) : @ wraps ( view ) def wrapper ( request , * args , ** kwargs ) : if not request . GET . get ( FRESH_LOGIN_PARAMETER ) : enterprise_customer = get_enterprise_customer_or_404 ( kwargs . get ( 'enterprise_uuid' ) ) provider_id = enterprise_customer . identity_provider or '' sso_provider = get_identity_provider ( provider_id ) if sso_provider : scheme , netloc , path , params , query , fragment = urlparse ( request . get_full_path ( ) ) redirect_url = urlunparse ( ( scheme , netloc , quote ( path ) , params , query , fragment ) ) return redirect ( '{logout_url}?{params}' . format ( logout_url = '/logout' , params = urlencode ( { 'redirect_url' : redirect_url } ) ) ) return view ( request , * args , ** kwargs ) return wrapper

View decorator which terminates stale TPA sessions .


def validate_username(self, value):
        """
        Verify that the username has a matching user, and that the user has an associated EnterpriseCustomerUser.
        """
        try:
            user = User.objects.get(username=value)
        except User.DoesNotExist:
            raise serializers.ValidationError("User does not exist")

        try:
            enterprise_customer_user = models.EnterpriseCustomerUser.objects.get(user_id=user.pk)
        except models.EnterpriseCustomerUser.DoesNotExist:
            raise serializers.ValidationError("User has no EnterpriseCustomerUser")

        self.enterprise_customer_user = enterprise_customer_user
        return value

def
validate_username
(
self
,
value
)
:
try
:
user
=
User
.
objects
.
get
(
username
=
value
)
except
User
.
DoesNotExist
:
raise
serializers
.
ValidationError
(
"User does not exist"
)
try
:
enterprise_customer_user
=
models
.
EnterpriseCustomerUser
.
objects
.
get
(
user_id
=
user
.
pk
)
except
models
.
EnterpriseCustomerUser
.
DoesNotExist
:
raise
serializers
.
ValidationError
(
"User has no EnterpriseCustomerUser"
)
self
.
enterprise_customer_user
=
enterprise_customer_user
return
value

Verify that the username has a matching user and that the user has an associated EnterpriseCustomerUser .


def save(self):  # pylint: disable=arguments-differ
        """
        Save the model with the found EnterpriseCustomerUser.
        """
        course_id = self.validated_data['course_id']

        __, created = models.EnterpriseCourseEnrollment.objects.get_or_create(
            enterprise_customer_user=self.enterprise_customer_user,
            course_id=course_id,
        )
        if created:
            track_enrollment('rest-api-enrollment', self.enterprise_customer_user.user_id, course_id)

def save ( self ) : course_id = self . validated_data [ 'course_id' ] __ , created = models . EnterpriseCourseEnrollment . objects . get_or_create ( enterprise_customer_user = self . enterprise_customer_user , course_id = course_id , ) if created : track_enrollment ( 'rest-api-enrollment' , self . enterprise_customer_user . user_id , course_id )

Save the model with the found EnterpriseCustomerUser .


def to_representation(self, instance):
        """
        Serialize the EnterpriseCustomerCatalog object.

        Arguments:
            instance (EnterpriseCustomerCatalog): The EnterpriseCustomerCatalog to serialize.

        Returns:
            dict: The EnterpriseCustomerCatalog converted to a dict.
        """
        request = self.context['request']
        enterprise_customer = instance.enterprise_customer

        representation = super(EnterpriseCustomerCatalogDetailSerializer, self).to_representation(instance)

        # Retrieve the EnterpriseCustomerCatalog search results from the discovery service.
        paginated_content = instance.get_paginated_content(request.GET)
        count = paginated_content['count']
        search_results = paginated_content['results']

        for item in search_results:
            content_type = item['content_type']
            marketing_url = item.get('marketing_url')
            if marketing_url:
                item['marketing_url'] = utils.update_query_parameters(
                    marketing_url, utils.get_enterprise_utm_context(enterprise_customer)
                )
            # Add the Enterprise enrollment URL to each content item returned from the discovery service.
            if content_type == 'course':
                item['enrollment_url'] = instance.get_course_enrollment_url(item['key'])
            if content_type == 'courserun':
                item['enrollment_url'] = instance.get_course_run_enrollment_url(item['key'])
            if content_type == 'program':
                item['enrollment_url'] = instance.get_program_enrollment_url(item['uuid'])

        # Build pagination URLs
        previous_url = None
        next_url = None
        page = int(request.GET.get('page', '1'))
        request_uri = request.build_absolute_uri()
        if paginated_content['previous']:
            previous_url = utils.update_query_parameters(request_uri, {'page': page - 1})
        if paginated_content['next']:
            next_url = utils.update_query_parameters(request_uri, {'page': page + 1})

        representation['count'] = count
        representation['previous'] = previous_url
        representation['next'] = next_url
        representation['results'] = search_results

        return representation

Serialize the EnterpriseCustomerCatalog object .


def get_groups(self, obj):
        """
        Return the enterprise related django groups that this user is a part of.
        """
        if obj.user:
            return [group.name for group in obj.user.groups.filter(name__in=ENTERPRISE_PERMISSION_GROUPS)]
        return []

def get_groups ( self , obj ) : if obj . user : return [ group . name for group in obj . user . groups . filter ( name__in = ENTERPRISE_PERMISSION_GROUPS ) ] return [ ]

Return the enterprise related django groups that this user is a part of .


def validate_username(self, value):
        """
        Verify that the username has a matching user.
        """
        try:
            self.user = User.objects.get(username=value)
        except User.DoesNotExist:
            raise serializers.ValidationError("User does not exist")

        return value

def
validate_username
(
self
,
value
)
:
try
:
self
.
user
=
User
.
objects
.
get
(
username
=
value
)
except
User
.
DoesNotExist
:
raise
serializers
.
ValidationError
(
"User does not exist"
)
return
value

Verify that the username has a matching user .


def save(self):  # pylint: disable=arguments-differ
        """
        Save the EnterpriseCustomerUser.
        """
        enterprise_customer = self.validated_data['enterprise_customer']

        ecu = models.EnterpriseCustomerUser(
            user_id=self.user.pk,
            enterprise_customer=enterprise_customer,
        )
        ecu.save()

def save ( self ) : enterprise_customer = self . validated_data [ 'enterprise_customer' ] ecu = models . EnterpriseCustomerUser ( user_id = self . user . pk , enterprise_customer = enterprise_customer , ) ecu . save ( )


def to_representation(self, instance):
        """
        Return the updated course data dictionary.

        Arguments:
            instance (dict): The course data.

        Returns:
            dict: The updated course data.
        """
        updated_course = copy.deepcopy(instance)
        enterprise_customer_catalog = self.context['enterprise_customer_catalog']
        updated_course['enrollment_url'] = enterprise_customer_catalog.get_course_enrollment_url(
            updated_course['key']
        )
        for course_run in updated_course['course_runs']:
            course_run['enrollment_url'] = enterprise_customer_catalog.get_course_run_enrollment_url(
                course_run['key']
            )
        return updated_course

def to_representation ( self , instance ) : updated_course = copy . deepcopy ( instance ) enterprise_customer_catalog = self . context [ 'enterprise_customer_catalog' ] updated_course [ 'enrollment_url' ] = enterprise_customer_catalog . get_course_enrollment_url ( updated_course [ 'key' ] ) for course_run in updated_course [ 'course_runs' ] : course_run [ 'enrollment_url' ] = enterprise_customer_catalog . get_course_run_enrollment_url ( course_run [ 'key' ] ) return updated_course

Return the updated course data dictionary .


def to_representation(self, instance):
        """
        Return the updated course run data dictionary.

        Arguments:
            instance (dict): The course run data.

        Returns:
            dict: The updated course run data.
        """
        updated_course_run = copy.deepcopy(instance)
        enterprise_customer_catalog = self.context['enterprise_customer_catalog']
        updated_course_run['enrollment_url'] = enterprise_customer_catalog.get_course_run_enrollment_url(
            updated_course_run['key']
        )
        return updated_course_run

def to_representation ( self , instance ) : updated_course_run = copy . deepcopy ( instance ) enterprise_customer_catalog = self . context [ 'enterprise_customer_catalog' ] updated_course_run [ 'enrollment_url' ] = enterprise_customer_catalog . get_course_run_enrollment_url ( updated_course_run [ 'key' ] ) return updated_course_run

Return the updated course run data dictionary .


def to_representation(self, instance):
        """
        Return the updated program data dictionary.

        Arguments:
            instance (dict): The program data.

        Returns:
            dict: The updated program data.
        """
        updated_program = copy.deepcopy(instance)
        enterprise_customer_catalog = self.context['enterprise_customer_catalog']
        updated_program['enrollment_url'] = enterprise_customer_catalog.get_program_enrollment_url(
            updated_program['uuid']
        )
        for course in updated_program['courses']:
            course['enrollment_url'] = enterprise_customer_catalog.get_course_enrollment_url(course['key'])
            for course_run in course['course_runs']:
                course_run['enrollment_url'] = enterprise_customer_catalog.get_course_run_enrollment_url(
                    course_run['key']
                )
        return updated_program

def to_representation ( self , instance ) : updated_program = copy . deepcopy ( instance ) enterprise_customer_catalog = self . context [ 'enterprise_customer_catalog' ] updated_program [ 'enrollment_url' ] = enterprise_customer_catalog . get_program_enrollment_url ( updated_program [ 'uuid' ] ) for course in updated_program [ 'courses' ] : course [ 'enrollment_url' ] = enterprise_customer_catalog . get_course_enrollment_url ( course [ 'key' ] ) for course_run in course [ 'course_runs' ] : course_run [ 'enrollment_url' ] = enterprise_customer_catalog . get_course_run_enrollment_url ( course_run [ 'key' ] ) return updated_program

Return the updated program data dictionary .


def to_internal_value(self, data):
        """
        This implements the same relevant logic as ListSerializer except that if one or more items fail validation,
        processing for other items that did not fail will continue.
        """

        if not isinstance(data, list):
            message = self.error_messages['not_a_list'].format(
                input_type=type(data).__name__
            )
            raise serializers.ValidationError({
                api_settings.NON_FIELD_ERRORS_KEY: [message]
            })

        ret = []

        for item in data:
            try:
                validated = self.child.run_validation(item)
            except serializers.ValidationError as exc:
                ret.append(exc.detail)
            else:
                ret.append(validated)

        return ret

def to_internal_value ( self , data ) : if not isinstance ( data , list ) : message = self . error_messages [ 'not_a_list' ] . format ( input_type = type ( data ) . __name__ ) raise serializers . ValidationError ( { api_settings . NON_FIELD_ERRORS_KEY : [ message ] } ) ret = [ ] for item in data : try : validated = self . child . run_validation ( item ) except serializers . ValidationError as exc : ret . append ( exc . detail ) else : ret . append ( validated ) return ret

This implements the same relevant logic as ListSerializer except that if one or more items fail validation processing for other items that did not fail will continue .


def create(self, validated_data):
        """
        This selectively calls the child create method based on whether or not validation failed for each payload.
        """
        ret = []
        for attrs in validated_data:
            if 'non_field_errors' not in attrs and not any(isinstance(attrs[field], list) for field in attrs):
                ret.append(self.child.create(attrs))
            else:
                ret.append(attrs)

        return ret

def create ( self , validated_data ) : ret = [ ] for attrs in validated_data : if 'non_field_errors' not in attrs and not any ( isinstance ( attrs [ field ] , list ) for field in attrs ) : ret . append ( self . child . create ( attrs ) ) else : ret . append ( attrs ) return ret

This selectively calls the child create method based on whether or not validation failed for each payload .


def to_representation(self, data):
        """
        This selectively calls to_representation on each result that was processed by create.
        """
        return [
            self.child.to_representation(item) if 'detail' in item else item for item in data
        ]

def to_representation ( self , data ) : return [ self . child . to_representation ( item ) if 'detail' in item else item for item in data ]

This selectively calls to_representation on each result that was processed by create .


def create(self, validated_data):
        """
        Perform the enrollment for existing enterprise customer users, or create the pending objects for new users.
        """
        enterprise_customer = self.context.get('enterprise_customer')
        lms_user = validated_data.get('lms_user_id')
        tpa_user = validated_data.get('tpa_user_id')
        user_email = validated_data.get('user_email')
        course_run_id = validated_data.get('course_run_id')
        course_mode = validated_data.get('course_mode')
        cohort = validated_data.get('cohort')
        email_students = validated_data.get('email_students')
        is_active = validated_data.get('is_active')

        enterprise_customer_user = lms_user or tpa_user or user_email

        if isinstance(enterprise_customer_user, models.EnterpriseCustomerUser):
            validated_data['enterprise_customer_user'] = enterprise_customer_user
            try:
                if is_active:
                    enterprise_customer_user.enroll(course_run_id, course_mode, cohort=cohort)
                else:
                    enterprise_customer_user.unenroll(course_run_id)
            except (CourseEnrollmentDowngradeError, CourseEnrollmentPermissionError, HttpClientError) as exc:
                validated_data['detail'] = str(exc)
                return validated_data

            if is_active:
                track_enrollment('enterprise-customer-enrollment-api', enterprise_customer_user.user_id, course_run_id)
        else:
            if is_active:
                enterprise_customer_user = enterprise_customer.enroll_user_pending_registration(
                    user_email,
                    course_mode,
                    course_run_id,
                    cohort=cohort
                )
            else:
                enterprise_customer.clear_pending_registration(user_email, course_run_id)

        if email_students:
            enterprise_customer.notify_enrolled_learners(
                self.context.get('request_user'),
                course_run_id,
                [enterprise_customer_user]
            )

        validated_data['detail'] = 'success'

        return validated_data

def create ( self , validated_data ) : enterprise_customer = self . context . get ( 'enterprise_customer' ) lms_user = validated_data . get ( 'lms_user_id' ) tpa_user = validated_data . get ( 'tpa_user_id' ) user_email = validated_data . get ( 'user_email' ) course_run_id = validated_data . get ( 'course_run_id' ) course_mode = validated_data . get ( 'course_mode' ) cohort = validated_data . get ( 'cohort' ) email_students = validated_data . get ( 'email_students' ) is_active = validated_data . get ( 'is_active' ) enterprise_customer_user = lms_user or tpa_user or user_email if isinstance ( enterprise_customer_user , models . EnterpriseCustomerUser ) : validated_data [ 'enterprise_customer_user' ] = enterprise_customer_user try : if is_active : enterprise_customer_user . enroll ( course_run_id , course_mode , cohort = cohort ) else : enterprise_customer_user . unenroll ( course_run_id ) except ( CourseEnrollmentDowngradeError , CourseEnrollmentPermissionError , HttpClientError ) as exc : validated_data [ 'detail' ] = str ( exc ) return validated_data if is_active : track_enrollment ( 'enterprise-customer-enrollment-api' , enterprise_customer_user . user_id , course_run_id ) else : if is_active : enterprise_customer_user = enterprise_customer . enroll_user_pending_registration ( user_email , course_mode , course_run_id , cohort = cohort ) else : enterprise_customer . clear_pending_registration ( user_email , course_run_id ) if email_students : enterprise_customer . notify_enrolled_learners ( self . context . get ( 'request_user' ) , course_run_id , [ enterprise_customer_user ] ) validated_data [ 'detail' ] = 'success' return validated_data

Perform the enrollment for existing enterprise customer users or create the pending objects for new users .


def validate_lms_user_id(self, value):
        """
        Validates the lms_user_id, if is given, to see if there is an existing EnterpriseCustomerUser for it.
        """
        enterprise_customer = self.context.get('enterprise_customer')

        try:
            # Ensure the given user is associated with the enterprise.
            return models.EnterpriseCustomerUser.objects.get(
                user_id=value,
                enterprise_customer=enterprise_customer
            )
        except models.EnterpriseCustomerUser.DoesNotExist:
            pass

        return None

def validate_lms_user_id ( self , value ) : enterprise_customer = self . context . get ( 'enterprise_customer' ) try : return models . EnterpriseCustomerUser . objects . get ( user_id = value , enterprise_customer = enterprise_customer ) except models . EnterpriseCustomerUser . DoesNotExist : pass return None

Validates the lms_user_id if is given to see if there is an existing EnterpriseCustomerUser for it .


def validate_tpa_user_id(self, value):
        """
        Validates the tpa_user_id, if is given, to see if there is an existing EnterpriseCustomerUser for it.

        It first uses the third party auth api to find the associated username to do the lookup.
        """
        enterprise_customer = self.context.get('enterprise_customer')

        try:
            tpa_client = ThirdPartyAuthApiClient()
            username = tpa_client.get_username_from_remote_id(
                enterprise_customer.identity_provider, value
            )
            user = User.objects.get(username=username)
            return models.EnterpriseCustomerUser.objects.get(
                user_id=user.id,
                enterprise_customer=enterprise_customer
            )
        except (models.EnterpriseCustomerUser.DoesNotExist, User.DoesNotExist):
            pass

        return None

def validate_tpa_user_id ( self , value ) : enterprise_customer = self . context . get ( 'enterprise_customer' ) try : tpa_client = ThirdPartyAuthApiClient ( ) username = tpa_client . get_username_from_remote_id ( enterprise_customer . identity_provider , value ) user = User . objects . get ( username = username ) return models . EnterpriseCustomerUser . objects . get ( user_id = user . id , enterprise_customer = enterprise_customer ) except ( models . EnterpriseCustomerUser . DoesNotExist , User . DoesNotExist ) : pass return None

Validates the tpa_user_id if is given to see if there is an existing EnterpriseCustomerUser for it .


def validate_user_email(self, value):
        """
        Validates the user_email, if given, to see if an existing EnterpriseCustomerUser exists for it.

        If it does not, it does not fail validation, unlike for the other field validation methods above.
        """
        enterprise_customer = self.context.get('enterprise_customer')

        try:
            user = User.objects.get(email=value)
            return models.EnterpriseCustomerUser.objects.get(
                user_id=user.id,
                enterprise_customer=enterprise_customer
            )
        except (models.EnterpriseCustomerUser.DoesNotExist, User.DoesNotExist):
            pass

        return value

def validate_user_email ( self , value ) : enterprise_customer = self . context . get ( 'enterprise_customer' ) try : user = User . objects . get ( email = value ) return models . EnterpriseCustomerUser . objects . get ( user_id = user . id , enterprise_customer = enterprise_customer ) except ( models . EnterpriseCustomerUser . DoesNotExist , User . DoesNotExist ) : pass return value

Validates the user_email if given to see if an existing EnterpriseCustomerUser exists for it .


def validate_course_run_id(self, value):
        """
        Validates that the course run id is part of the Enterprise Customer's catalog.
        """
        enterprise_customer = self.context.get('enterprise_customer')

        if not enterprise_customer.catalog_contains_course(value):
            raise serializers.ValidationError(
                'The course run id {course_run_id} is not in the catalog '
                'for Enterprise Customer {enterprise_customer}'.format(
                    course_run_id=value,
                    enterprise_customer=enterprise_customer.name,
                )
            )

        return value

Validates that the course run id is part of the Enterprise Customer s catalog .


def validate(self, data):  # pylint: disable=arguments-differ
        """
        Validate that at least one of the user identifier fields has been passed in.
        """
        lms_user_id = data.get('lms_user_id')
        tpa_user_id = data.get('tpa_user_id')
        user_email = data.get('user_email')
        if not lms_user_id and not tpa_user_id and not user_email:
            raise serializers.ValidationError(
                'At least one of the following fields must be specified and map to an EnterpriseCustomerUser: '
                'lms_user_id, tpa_user_id, user_email'
            )

        return data

Validate that at least one of the user identifier fields has been passed in .


def get_paginated_response(data, request):
    """
    Update pagination links in course catalog data and return DRF Response.

    Arguments:
        data (dict): Dictionary containing catalog courses.
        request (HttpRequest): Current request object.

    Returns:
        (Response): DRF response object containing pagination links.
    """
    url = urlparse(request.build_absolute_uri())._replace(query=None).geturl()

    next_page = None
    previous_page = None

    if data['next']:
        next_page = "{base_url}?{query_parameters}".format(
            base_url=url,
            query_parameters=urlparse(data['next']).query,
        )
        next_page = next_page.rstrip('?')
    if data['previous']:
        previous_page = "{base_url}?{query_parameters}".format(
            base_url=url,
            query_parameters=urlparse(data['previous'] or "").query,
        )
        previous_page = previous_page.rstrip('?')

    return Response(OrderedDict([
        ('count', data['count']),
        ('next', next_page),
        ('previous', previous_page),
        ('results', data['results'])
    ]))

def get_paginated_response ( data , request ) : url = urlparse ( request . build_absolute_uri ( ) ) . _replace ( query = None ) . geturl ( ) next_page = None previous_page = None if data [ 'next' ] : next_page = "{base_url}?{query_parameters}" . format ( base_url = url , query_parameters = urlparse ( data [ 'next' ] ) . query , ) next_page = next_page . rstrip ( '?' ) if data [ 'previous' ] : previous_page = "{base_url}?{query_parameters}" . format ( base_url = url , query_parameters = urlparse ( data [ 'previous' ] or "" ) . query , ) previous_page = previous_page . rstrip ( '?' ) return Response ( OrderedDict ( [ ( 'count' , data [ 'count' ] ) , ( 'next' , next_page ) , ( 'previous' , previous_page ) , ( 'results' , data [ 'results' ] ) ] ) )

Update pagination links in course catalog data and return DRF Response .


def create_switch(apps, schema_editor):
    """Create the `role_based_access_control` switch if it does not already exist."""
    Switch = apps.get_model('waffle', 'Switch')
    Switch.objects.update_or_create(name=ENTERPRISE_ROLE_BASED_ACCESS_CONTROL_SWITCH, defaults={'active': False})

Create the role_based_access_control switch if it does not already exist .


def delete_switch(apps, schema_editor):
    """Delete the `role_based_access_control` switch."""
    Switch = apps.get_model('waffle', 'Switch')
    Switch.objects.filter(name=ENTERPRISE_ROLE_BASED_ACCESS_CONTROL_SWITCH).delete()

Delete the role_based_access_control switch .


def create_switch(apps, schema_editor):
    """Create and activate the SAP_USE_ENTERPRISE_ENROLLMENT_PAGE switch if it does not already exist."""
    Switch = apps.get_model('waffle', 'Switch')
    Switch.objects.get_or_create(name='SAP_USE_ENTERPRISE_ENROLLMENT_PAGE', defaults={'active': False})

def create_switch ( apps , schema_editor ) : Switch = apps . get_model ( 'waffle' , 'Switch' ) Switch . objects . get_or_create ( name = 'SAP_USE_ENTERPRISE_ENROLLMENT_PAGE' , defaults = { 'active' : False } )

Create and activate the SAP_USE_ENTERPRISE_ENROLLMENT_PAGE switch if it does not already exist .


def transmit(self, payload, **kwargs):
        """
        Send a completion status call to SAP SuccessFactors using the client.

        Args:
            payload: The learner completion data payload to send to SAP SuccessFactors
        """
        kwargs['app_label'] = 'sap_success_factors'
        kwargs['model_name'] = 'SapSuccessFactorsLearnerDataTransmissionAudit'
        kwargs['remote_user_id'] = 'sapsf_user_id'
        super(SapSuccessFactorsLearnerTransmitter, self).transmit(payload, **kwargs)

Send a completion status call to SAP SuccessFactors using the client .


def handle_transmission_error(self, learner_data, request_exception):
        """Handle the case where the employee on SAPSF's side is marked as inactive."""
        try:
            sys_msg = request_exception.response.content
        except AttributeError:
            pass
        else:
            if 'user account is inactive' in sys_msg:
                ecu = EnterpriseCustomerUser.objects.get(
                    enterprise_enrollments__id=learner_data.enterprise_course_enrollment_id)
                ecu.active = False
                ecu.save()
                LOGGER.warning(
                    'User %s with ID %s and email %s is a former employee of %s '
                    'and has been marked inactive in SAPSF. Now marking inactive internally.',
                    ecu.username, ecu.user_id, ecu.user_email, ecu.enterprise_customer
                )
                return
        super(SapSuccessFactorsLearnerTransmitter, self).handle_transmission_error(learner_data, request_exception)

Handle the case where the employee on SAPSF s side is marked as inactive .


def allow_request(self, request, view):
        """
        Modify throttling for service users.

        Updates throttling rate if the request is coming from the service user, and
        defaults to UserRateThrottle's configured setting otherwise.

        Updated throttling rate comes from `DEFAULT_THROTTLE_RATES` key in `REST_FRAMEWORK`
        setting. service user throttling is specified in `DEFAULT_THROTTLE_RATES` by `service_user` key

        Example Setting:
            ```
            REST_FRAMEWORK = {
                ...
                'DEFAULT_THROTTLE_RATES': {
                    ...
                    'service_user': '50/day'
                }
            }
            ```
        """
        service_users = get_service_usernames()

        # User service user throttling rates for service user.
        if request.user.username in service_users:
            self.update_throttle_scope()

        return super(ServiceUserThrottle, self).allow_request(request, view)

def allow_request ( self , request , view ) : service_users = get_service_usernames ( ) if request . user . username in service_users : self . update_throttle_scope ( ) return super ( ServiceUserThrottle , self ) . allow_request ( request , view )


def update_throttle_scope(self):
        """
        Update throttle scope so that service user throttle rates are applied.
        """
        self.scope = SERVICE_USER_SCOPE
        self.rate = self.get_rate()
        self.num_requests, self.duration = self.parse_rate(self.rate)

def update_throttle_scope ( self ) : self . scope = SERVICE_USER_SCOPE self . rate = self . get_rate ( ) self . num_requests , self . duration = self . parse_rate ( self . rate )

Update throttle scope so that service user throttle rates are applied .


def get_course_final_price(self, mode, currency='$', enterprise_catalog_uuid=None):
        """
        Get course mode's SKU discounted price after applying any entitlement available for this user.

        Returns:
            str: Discounted price of the course mode.

        """
        try:
            price_details = self.client.baskets.calculate.get(
                sku=[mode['sku']],
                username=self.user.username,
                catalog=enterprise_catalog_uuid,
            )
        except (SlumberBaseException, ConnectionError, Timeout) as exc:
            LOGGER.exception('Failed to get price details for sku %s due to: %s', mode['sku'], str(exc))
            price_details = {}
        price = price_details.get('total_incl_tax', mode['min_price'])
        if price != mode['min_price']:
            return format_price(price, currency)
        return mode['original_price']

Get course mode s SKU discounted price after applying any entitlement available for this user .


def update_enterprise_courses(self, enterprise_customer, course_container_key='results', **kwargs):
        """
        This method adds enterprise-specific metadata for each course.

        We are adding following field in all the courses.
            tpa_hint: a string for identifying Identity Provider.
            enterprise_id: the UUID of the enterprise
            **kwargs: any additional data one would like to add on a per-use basis.

        Arguments:
            enterprise_customer: The customer whose data will be used to fill the enterprise context.
            course_container_key: The key used to find the container for courses in the serializer's data dictionary.
        """
        enterprise_context = {
            'tpa_hint': enterprise_customer and enterprise_customer.identity_provider,
            'enterprise_id': enterprise_customer and str(enterprise_customer.uuid),
        }
        enterprise_context.update(**kwargs)

        courses = []
        for course in self.data[course_container_key]:
            courses.append(
                self.update_course(course, enterprise_customer, enterprise_context)
            )
        self.data[course_container_key] = courses

def update_enterprise_courses ( self , enterprise_customer , course_container_key = 'results' , ** kwargs ) : enterprise_context = { 'tpa_hint' : enterprise_customer and enterprise_customer . identity_provider , 'enterprise_id' : enterprise_customer and str ( enterprise_customer . uuid ) , } enterprise_context . update ( ** kwargs ) courses = [ ] for course in self . data [ course_container_key ] : courses . append ( self . update_course ( course , enterprise_customer , enterprise_context ) ) self . data [ course_container_key ] = courses

This method adds enterprise - specific metadata for each course .


def update_course(self, course, enterprise_customer, enterprise_context):
        """
        Update course metadata of the given course and return updated course.

        Arguments:
            course (dict): Course Metadata returned by course catalog API
            enterprise_customer (EnterpriseCustomer): enterprise customer instance.
            enterprise_context (dict): Enterprise context to be added to course runs and URLs..

        Returns:
            (dict): Updated course metadata
        """
        course['course_runs'] = self.update_course_runs(
            course_runs=course.get('course_runs') or [],
            enterprise_customer=enterprise_customer,
            enterprise_context=enterprise_context,
        )

        # Update marketing urls in course metadata to include enterprise related info (i.e. our global context).
        marketing_url = course.get('marketing_url')
        if marketing_url:
            query_parameters = dict(enterprise_context, **utils.get_enterprise_utm_context(enterprise_customer))
            course.update({'marketing_url': utils.update_query_parameters(marketing_url, query_parameters)})

        # Finally, add context to the course as a whole.
        course.update(enterprise_context)
        return course

def update_course ( self , course , enterprise_customer , enterprise_context ) : course [ 'course_runs' ] = self . update_course_runs ( course_runs = course . get ( 'course_runs' ) or [ ] , enterprise_customer = enterprise_customer , enterprise_context = enterprise_context , ) marketing_url = course . get ( 'marketing_url' ) if marketing_url : query_parameters = dict ( enterprise_context , ** utils . get_enterprise_utm_context ( enterprise_customer ) ) course . update ( { 'marketing_url' : utils . update_query_parameters ( marketing_url , query_parameters ) } ) course . update ( enterprise_context ) return course

Update course metadata of the given course and return updated course .


def update_course_runs(self, course_runs, enterprise_customer, enterprise_context):
        """
        Update Marketing urls in course metadata and return updated course.

        Arguments:
            course_runs (list): List of course runs.
            enterprise_customer (EnterpriseCustomer): enterprise customer instance.
            enterprise_context (dict): The context to inject into URLs.

        Returns:
            (dict): Dictionary containing updated course metadata.
        """
        updated_course_runs = []
        for course_run in course_runs:
            track_selection_url = utils.get_course_track_selection_url(
                course_run=course_run,
                query_parameters=dict(enterprise_context, **utils.get_enterprise_utm_context(enterprise_customer)),
            )

            enrollment_url = enterprise_customer.get_course_run_enrollment_url(course_run.get('key'))

            course_run.update({
                'enrollment_url': enrollment_url,
                'track_selection_url': track_selection_url,
            })

            # Update marketing urls in course metadata to include enterprise related info.
            marketing_url = course_run.get('marketing_url')
            if marketing_url:
                query_parameters = dict(enterprise_context, **utils.get_enterprise_utm_context(enterprise_customer))
                course_run.update({'marketing_url': utils.update_query_parameters(marketing_url, query_parameters)})

            # Add updated course run to the list.
            updated_course_runs.append(course_run)
        return updated_course_runs

def update_course_runs ( self , course_runs , enterprise_customer , enterprise_context ) : updated_course_runs = [ ] for course_run in course_runs : track_selection_url = utils . get_course_track_selection_url ( course_run = course_run , query_parameters = dict ( enterprise_context , ** utils . get_enterprise_utm_context ( enterprise_customer ) ) , ) enrollment_url = enterprise_customer . get_course_run_enrollment_url ( course_run . get ( 'key' ) ) course_run . update ( { 'enrollment_url' : enrollment_url , 'track_selection_url' : track_selection_url , } ) marketing_url = course_run . get ( 'marketing_url' ) if marketing_url : query_parameters = dict ( enterprise_context , ** utils . get_enterprise_utm_context ( enterprise_customer ) ) course_run . update ( { 'marketing_url' : utils . update_query_parameters ( marketing_url , query_parameters ) } ) updated_course_runs . append ( course_run ) return updated_course_runs

Update Marketing urls in course metadata and return updated course .


def export(self):
        """
        Collect learner data for the ``EnterpriseCustomer`` where data sharing consent is granted.

        Yields a learner data object for each enrollment, containing:

        * ``enterprise_enrollment``: ``EnterpriseCourseEnrollment`` object.
        * ``completed_date``: datetime instance containing the course/enrollment completion date; None if not complete.
          "Course completion" occurs for instructor-paced courses when course certificates are issued, and
          for self-paced courses, when the course end date is passed, or when the learner achieves a passing grade.
        * ``grade``: string grade recorded for the learner in the course.
        """
        # Fetch the consenting enrollment data, including the enterprise_customer_user.
        # Order by the course_id, to avoid fetching course API data more than we have to.
        enrollment_queryset = EnterpriseCourseEnrollment.objects.select_related(
            'enterprise_customer_user'
        ).filter(
            enterprise_customer_user__enterprise_customer=self.enterprise_customer,
            enterprise_customer_user__active=True,
        ).order_by('course_id')

        # Fetch course details from the Course API, and cache between calls.
        course_details = None
        for enterprise_enrollment in enrollment_queryset:

            course_id = enterprise_enrollment.course_id

            # Fetch course details from Courses API
            # pylint: disable=unsubscriptable-object
            if course_details is None or course_details['course_id'] != course_id:
                if self.course_api is None:
                    self.course_api = CourseApiClient()
                course_details = self.course_api.get_course_details(course_id)

            if course_details is None:
                # Course not found, so we have nothing to report.
                LOGGER.error("No course run details found for enrollment [%d]: [%s]",
                             enterprise_enrollment.pk, course_id)
                continue

            consent = DataSharingConsent.objects.proxied_get(
                username=enterprise_enrollment.enterprise_customer_user.username,
                course_id=enterprise_enrollment.course_id,
                enterprise_customer=enterprise_enrollment.enterprise_customer_user.enterprise_customer
            )

            if not consent.granted or enterprise_enrollment.audit_reporting_disabled:
                continue

            # For instructor-paced courses, let the certificate determine course completion
            if course_details.get('pacing') == 'instructor':
                completed_date, grade, is_passing = self._collect_certificate_data(enterprise_enrollment)

            # For self-paced courses, check the Grades API
            else:
                completed_date, grade, is_passing = self._collect_grades_data(enterprise_enrollment, course_details)

            records = self.get_learner_data_records(
                enterprise_enrollment=enterprise_enrollment,
                completed_date=completed_date,
                grade=grade,
                is_passing=is_passing,
            )
            if records:
                # There are some cases where we won't receive a record from the above
                # method; right now, that should only happen if we have an Enterprise-linked
                # user for the integrated channel, and transmission of that user's
                # data requires an upstream user identifier that we don't have (due to a
                # failure of SSO or similar). In such a case, `get_learner_data_record`
                # would return None, and we'd simply skip yielding it here.
                for record in records:
                    yield record

Collect learner data for the EnterpriseCustomer where data sharing consent is granted .


def get_learner_data_records(self, enterprise_enrollment, completed_date=None, grade=None, is_passing=False):
        """
        Generate a learner data transmission audit with fields properly filled in.
        """
        # pylint: disable=invalid-name
        LearnerDataTransmissionAudit = apps.get_model('integrated_channel', 'LearnerDataTransmissionAudit')
        completed_timestamp = None
        course_completed = False
        if completed_date is not None:
            completed_timestamp = parse_datetime_to_epoch_millis(completed_date)
            course_completed = is_passing

        return [
            LearnerDataTransmissionAudit(
                enterprise_course_enrollment_id=enterprise_enrollment.id,
                course_id=enterprise_enrollment.course_id,
                course_completed=course_completed,
                completed_timestamp=completed_timestamp,
                grade=grade,
            )
        ]

def get_learner_data_records ( self , enterprise_enrollment , completed_date = None , grade = None , is_passing = False ) : LearnerDataTransmissionAudit = apps . get_model ( 'integrated_channel' , 'LearnerDataTransmissionAudit' ) completed_timestamp = None course_completed = False if completed_date is not None : completed_timestamp = parse_datetime_to_epoch_millis ( completed_date ) course_completed = is_passing return [ LearnerDataTransmissionAudit ( enterprise_course_enrollment_id = enterprise_enrollment . id , course_id = enterprise_enrollment . course_id , course_completed = course_completed , completed_timestamp = completed_timestamp , grade = grade , ) ]

Generate a learner data transmission audit with fields properly filled in .


def _collect_certificate_data(self, enterprise_enrollment):
        """
        Collect the learner completion data from the course certificate.

        Used for Instructor-paced courses.

        If no certificate is found, then returns the completed_date = None, grade = In Progress, on the idea that a
        certificate will eventually be generated.

        Args:
            enterprise_enrollment (EnterpriseCourseEnrollment): the enterprise enrollment record for which we need to
            collect completion/grade data

        Returns:
            completed_date: Date the course was completed, this is None if course has not been completed.
            grade: Current grade in the course.
            is_passing: Boolean indicating if the grade is a passing grade or not.
        """

        if self.certificates_api is None:
            self.certificates_api = CertificatesApiClient(self.user)

        course_id = enterprise_enrollment.course_id
        username = enterprise_enrollment.enterprise_customer_user.user.username

        try:
            certificate = self.certificates_api.get_course_certificate(course_id, username)
            completed_date = certificate.get('created_date')
            if completed_date:
                completed_date = parse_datetime(completed_date)
            else:
                completed_date = timezone.now()

            # For consistency with _collect_grades_data, we only care about Pass/Fail grades. This could change.
            is_passing = certificate.get('is_passing')
            grade = self.grade_passing if is_passing else self.grade_failing

        except HttpNotFoundError:
            completed_date = None
            grade = self.grade_incomplete
            is_passing = False

        return completed_date, grade, is_passing

def _collect_certificate_data ( self , enterprise_enrollment ) : if self . certificates_api is None : self . certificates_api = CertificatesApiClient ( self . user ) course_id = enterprise_enrollment . course_id username = enterprise_enrollment . enterprise_customer_user . user . username try : certificate = self . certificates_api . get_course_certificate ( course_id , username ) completed_date = certificate . get ( 'created_date' ) if completed_date : completed_date = parse_datetime ( completed_date ) else : completed_date = timezone . now ( ) is_passing = certificate . get ( 'is_passing' ) grade = self . grade_passing if is_passing else self . grade_failing except HttpNotFoundError : completed_date = None grade = self . grade_incomplete is_passing = False return completed_date , grade , is_passing

Collect the learner completion data from the course certificate .


def _collect_grades_data(self, enterprise_enrollment, course_details):
        """
        Collect the learner completion data from the Grades API.

        Used for self-paced courses.

        Args:
            enterprise_enrollment (EnterpriseCourseEnrollment): the enterprise enrollment record for which we need to
            collect completion/grade data
            course_details (dict): the course details for the course in the enterprise enrollment record.

        Returns:
            completed_date: Date the course was completed, this is None if course has not been completed.
            grade: Current grade in the course.
            is_passing: Boolean indicating if the grade is a passing grade or not.
        """
        if self.grades_api is None:
            self.grades_api = GradesApiClient(self.user)

        course_id = enterprise_enrollment.course_id
        username = enterprise_enrollment.enterprise_customer_user.user.username

        try:
            grades_data = self.grades_api.get_course_grade(course_id, username)

        except HttpNotFoundError as error:
            # Grade not found, so we have nothing to report.
            if hasattr(error, 'content'):
                response_content = json.loads(error.content)
                if response_content.get('error_code', '') == 'user_not_enrolled':
                    # This means the user has an enterprise enrollment record but is not enrolled in the course yet
                    LOGGER.info(
                        "User [%s] not enrolled in course [%s], enterprise enrollment [%d]",
                        username,
                        course_id,
                        enterprise_enrollment.pk
                    )
                    return None, None, None

            LOGGER.error("No grades data found for [%d]: [%s], [%s]", enterprise_enrollment.pk, course_id, username)
            return None, None, None

        # Prepare to process the course end date and pass/fail grade
        course_end_date = course_details.get('end')
        if course_end_date is not None:
            course_end_date = parse_datetime(course_end_date)
        now = timezone.now()
        is_passing = grades_data.get('passed')

        # We can consider a course complete if:
        # * the course's end date has passed
        if course_end_date is not None and course_end_date < now:
            completed_date = course_end_date
            grade = self.grade_passing if is_passing else self.grade_failing

        # * Or, the learner has a passing grade (as of now)
        elif is_passing:
            completed_date = now
            grade = self.grade_passing

        # Otherwise, the course is still in progress
        else:
            completed_date = None
            grade = self.grade_incomplete

        return completed_date, grade, is_passing

Collect the learner completion data from the Grades API .


def get_enterprise_user_id(self, obj):
        """
        Get enterprise user id from user object.

        Arguments:
            obj (User): Django User object

        Returns:
            (int): Primary Key identifier for enterprise user object.
        """
        # An enterprise learner can not belong to multiple enterprise customer at the same time
        # but if such scenario occurs we will pick the first.
        enterprise_learner = EnterpriseCustomerUser.objects.filter(user_id=obj.id).first()

        return enterprise_learner and enterprise_learner.id

def get_enterprise_user_id ( self , obj ) : enterprise_learner = EnterpriseCustomerUser . objects . filter ( user_id = obj . id ) . first ( ) return enterprise_learner and enterprise_learner . id

Get enterprise user id from user object .


def get_enterprise_sso_uid(self, obj):
        """
        Get enterprise SSO UID.

        Arguments:
            obj (User): Django User object

        Returns:
            (str): string containing UUID for enterprise customer's Identity Provider.
        """
        # An enterprise learner can not belong to multiple enterprise customer at the same time
        # but if such scenario occurs we will pick the first.
        enterprise_learner = EnterpriseCustomerUser.objects.filter(user_id=obj.id).first()

        return enterprise_learner and enterprise_learner.get_remote_id()

def get_enterprise_sso_uid ( self , obj ) : enterprise_learner = EnterpriseCustomerUser . objects . filter ( user_id = obj . id ) . first ( ) return enterprise_learner and enterprise_learner . get_remote_id ( )


def get_course_duration(self, obj):
        """
        Get course's duration as a timedelta.

        Arguments:
            obj (CourseOverview): CourseOverview object

        Returns:
            (timedelta): Duration of a course.
        """
        duration = obj.end - obj.start if obj.start and obj.end else None
        if duration:
            return strfdelta(duration, '{W} weeks {D} days.')
        return ''

def
get_course_duration
(
self
,
obj
)
:
duration
=
obj
.
end
obj
.
start
if
obj
.
start
and
obj
.
end
else
if
duration
:
return
strfdelta
(
duration
,
'{W} weeks {D} days.'
)
return
''


def _remove_failed_items(self, failed_items, items_to_create, items_to_update, items_to_delete):
        """
        Remove content metadata items from the `items_to_create`, `items_to_update`, `items_to_delete` dicts.

        Arguments:
            failed_items (list): Failed Items to be removed.
            items_to_create (dict): dict containing the items created successfully.
            items_to_update (dict): dict containing the items updated successfully.
            items_to_delete (dict): dict containing the items deleted successfully.
        """
        for item in failed_items:
            content_metadata_id = item['courseID']
            items_to_create.pop(content_metadata_id, None)
            items_to_update.pop(content_metadata_id, None)
            items_to_delete.pop(content_metadata_id, None)

def _remove_failed_items ( self , failed_items , items_to_create , items_to_update , items_to_delete ) : for item in failed_items : content_metadata_id = item [ 'courseID' ] items_to_create . pop ( content_metadata_id , None ) items_to_update . pop ( content_metadata_id , None ) items_to_delete . pop ( content_metadata_id , None )

Remove content metadata items from the items_to_create items_to_update items_to_delete dicts .


def parse_arguments(*args, **options):  # pylint: disable=unused-argument
        """
        Parse and validate arguments for send_course_enrollments command.

        Arguments:
            *args: Positional arguments passed to the command
            **options: optional arguments passed to the command

        Returns:
            A tuple containing parsed values for
            1. days (int): Integer showing number of days to lookup enterprise enrollments,
                course completion etc and send to xAPI LRS
            2. enterprise_customer_uuid (EnterpriseCustomer): Enterprise Customer if present then
                send xAPI statements just for this enterprise.
        """
        days = options.get('days', 1)
        enterprise_customer_uuid = options.get('enterprise_customer_uuid')
        enterprise_customer = None

        if enterprise_customer_uuid:
            try:
                # pylint: disable=no-member
                enterprise_customer = EnterpriseCustomer.objects.get(uuid=enterprise_customer_uuid)
            except EnterpriseCustomer.DoesNotExist:
                raise CommandError('Enterprise customer with uuid "{enterprise_customer_uuid}" does not exist.'.format(
                    enterprise_customer_uuid=enterprise_customer_uuid
                ))

        return days, enterprise_customer

Parse and validate arguments for send_course_enrollments command .


def handle(self, *args, **options):
        """
        Send xAPI statements.
        """
        if not CourseEnrollment:
            raise NotConnectedToOpenEdX("This package must be installed in an OpenEdX environment.")

        days, enterprise_customer = self.parse_arguments(*args, **options)

        if enterprise_customer:
            try:
                lrs_configuration = XAPILRSConfiguration.objects.get(
                    active=True,
                    enterprise_customer=enterprise_customer
                )
            except XAPILRSConfiguration.DoesNotExist:
                raise CommandError('No xAPI Configuration found for "{enterprise_customer}"'.format(
                    enterprise_customer=enterprise_customer.name
                ))

            # Send xAPI analytics data to the configured LRS
            self.send_xapi_statements(lrs_configuration, days)
        else:
            for lrs_configuration in XAPILRSConfiguration.objects.filter(active=True):
                self.send_xapi_statements(lrs_configuration, days)


def get_course_enrollments(self, enterprise_customer, days):
        """
        Get course enrollments for all the learners of given enterprise customer.

        Arguments:
            enterprise_customer (EnterpriseCustomer): Include Course enrollments for learners
                of this enterprise customer.
            days (int): Include course enrollment of this number of days.

        Returns:
            (list): A list of CourseEnrollment objects.
        """
        return CourseEnrollment.objects.filter(
            created__gt=datetime.datetime.now() - datetime.timedelta(days=days)
        ).filter(
            user_id__in=enterprise_customer.enterprise_customer_users.values_list('user_id', flat=True)
        )

def get_course_enrollments ( self , enterprise_customer , days ) : return CourseEnrollment . objects . filter ( created__gt = datetime . datetime . now ( ) - datetime . timedelta ( days = days ) ) . filter ( user_id__in = enterprise_customer . enterprise_customer_users . values_list ( 'user_id' , flat = True ) )

Get course enrollments for all the learners of given enterprise customer .


def course_modal(context, course=None):
    """
    Django template tag that returns course information to display in a modal.

    You may pass in a particular course if you like. Otherwise, the modal will look for course context
    within the parent context.

    Usage:
        {% course_modal %}
        {% course_modal course %}
    """
    if course:
        context.update({
            'course_image_uri': course.get('course_image_uri', ''),
            'course_title': course.get('course_title', ''),
            'course_level_type': course.get('course_level_type', ''),
            'course_short_description': course.get('course_short_description', ''),
            'course_effort': course.get('course_effort', ''),
            'course_full_description': course.get('course_full_description', ''),
            'expected_learning_items': course.get('expected_learning_items', []),
            'staff': course.get('staff', []),
            'premium_modes': course.get('premium_modes', []),
        })
    return context

def course_modal ( context , course = None ) : if course : context . update ( { 'course_image_uri' : course . get ( 'course_image_uri' , '' ) , 'course_title' : course . get ( 'course_title' , '' ) , 'course_level_type' : course . get ( 'course_level_type' , '' ) , 'course_short_description' : course . get ( 'course_short_description' , '' ) , 'course_effort' : course . get ( 'course_effort' , '' ) , 'course_full_description' : course . get ( 'course_full_description' , '' ) , 'expected_learning_items' : course . get ( 'expected_learning_items' , [ ] ) , 'staff' : course . get ( 'staff' , [ ] ) , 'premium_modes' : course . get ( 'premium_modes' , [ ] ) , } ) return context

Django template tag that returns course information to display in a modal .


def link_to_modal(link_text, index, autoescape=True):  # pylint: disable=unused-argument
    """
    Django template filter that returns an anchor with attributes useful for course modal selection.

    General Usage:
        {{ link_text|link_to_modal:index }}

    Examples:
        {{ course_title|link_to_modal:forloop.counter0 }}
        {{ course_title|link_to_modal:3 }}
        {{ view_details_text|link_to_modal:0 }}
    """
    link = (
        '<a'
        ' href="#!"'
        ' class="text-underline view-course-details-link"'
        ' id="view-course-details-link-{index}"'
        ' data-toggle="modal"'
        ' data-target="#course-details-modal-{index}"'
        '>{link_text}</a>'
    ).format(
        index=index,
        link_text=link_text,
    )
    return mark_safe(link)

Django template filter that returns an anchor with attributes useful for course modal selection .


def populate_data_sharing_consent(apps, schema_editor):
    """
    Populates the ``DataSharingConsent`` model with the ``enterprise`` application's consent data.

    Consent data from the ``enterprise`` application come from the ``EnterpriseCourseEnrollment`` model.
    """
    DataSharingConsent = apps.get_model('consent', 'DataSharingConsent')
    EnterpriseCourseEnrollment = apps.get_model('enterprise', 'EnterpriseCourseEnrollment')
    User = apps.get_model('auth', 'User')
    for enrollment in EnterpriseCourseEnrollment.objects.all():
        user = User.objects.get(pk=enrollment.enterprise_customer_user.user_id)
        data_sharing_consent, __ = DataSharingConsent.objects.get_or_create(
            username=user.username,
            enterprise_customer=enrollment.enterprise_customer_user.enterprise_customer,
            course_id=enrollment.course_id,
        )
        if enrollment.consent_granted is not None:
            data_sharing_consent.granted = enrollment.consent_granted
        else:
            # Check UDSCA instead.
            consent_state = enrollment.enterprise_customer_user.data_sharing_consent.first()
            if consent_state is not None:
                data_sharing_consent.granted = consent_state.state in ['enabled', 'external']
            else:
                data_sharing_consent.granted = False
        data_sharing_consent.save()

def populate_data_sharing_consent ( apps , schema_editor ) : DataSharingConsent = apps . get_model ( 'consent' , 'DataSharingConsent' ) EnterpriseCourseEnrollment = apps . get_model ( 'enterprise' , 'EnterpriseCourseEnrollment' ) User = apps . get_model ( 'auth' , 'User' ) for enrollment in EnterpriseCourseEnrollment . objects . all ( ) : user = User . objects . get ( pk = enrollment . enterprise_customer_user . user_id ) data_sharing_consent , __ = DataSharingConsent . objects . get_or_create ( username = user . username , enterprise_customer = enrollment . enterprise_customer_user . enterprise_customer , course_id = enrollment . course_id , ) if enrollment . consent_granted is not None : data_sharing_consent . granted = enrollment . consent_granted else : consent_state = enrollment . enterprise_customer_user . data_sharing_consent . first ( ) if consent_state is not None : data_sharing_consent . granted = consent_state . state in [ 'enabled' , 'external' ] else : data_sharing_consent . granted = False data_sharing_consent . save ( )

Populates the DataSharingConsent model with the enterprise application s consent data .


def create_course_completion(self, user_id, payload):  # pylint: disable=unused-argument
        """
        Send a completion status payload to the Degreed Completion Status endpoint

        Args:
            user_id: Unused.
            payload: JSON encoded object (serialized from DegreedLearnerDataTransmissionAudit)
                containing completion status fields per Degreed documentation.

        Returns:
            A tuple containing the status code and the body of the response.
        Raises:
            HTTPError: if we received a failure response code from Degreed
        """
        return self._post(
            urljoin(
                self.enterprise_configuration.degreed_base_url,
                self.global_degreed_config.completion_status_api_path
            ),
            payload,
            self.COMPLETION_PROVIDER_SCOPE
        )

def create_course_completion ( self , user_id , payload ) : return self . _post ( urljoin ( self . enterprise_configuration . degreed_base_url , self . global_degreed_config . completion_status_api_path ) , payload , self . COMPLETION_PROVIDER_SCOPE )


def delete_course_completion(self, user_id, payload):  # pylint: disable=unused-argument
        """
        Delete a completion status previously sent to the Degreed Completion Status endpoint

        Args:
            user_id: Unused.
            payload: JSON encoded object (serialized from DegreedLearnerDataTransmissionAudit)
                containing the required completion status fields for deletion per Degreed documentation.

        Returns:
            A tuple containing the status code and the body of the response.
        Raises:
            HTTPError: if we received a failure response code from Degreed
        """
        return self._delete(
            urljoin(
                self.enterprise_configuration.degreed_base_url,
                self.global_degreed_config.completion_status_api_path
            ),
            payload,
            self.COMPLETION_PROVIDER_SCOPE
        )

def delete_course_completion ( self , user_id , payload ) : return self . _delete ( urljoin ( self . enterprise_configuration . degreed_base_url , self . global_degreed_config . completion_status_api_path ) , payload , self . COMPLETION_PROVIDER_SCOPE )


def _sync_content_metadata(self, serialized_data, http_method):
        """
        Synchronize content metadata using the Degreed course content API.

        Args:
            serialized_data: JSON-encoded object containing content metadata.
            http_method: The HTTP method to use for the API request.

        Raises:
            ClientError: If Degreed API request fails.
        """
        try:
            status_code, response_body = getattr(self, '_' + http_method)(
                urljoin(self.enterprise_configuration.degreed_base_url, self.global_degreed_config.course_api_path),
                serialized_data,
                self.CONTENT_PROVIDER_SCOPE
            )
        except requests.exceptions.RequestException as exc:
            raise ClientError(
                'DegreedAPIClient request failed: {error} {message}'.format(
                    error=exc.__class__.__name__,
                    message=str(exc)
                )
            )

        if status_code >= 400:
            raise ClientError(
                'DegreedAPIClient request failed with status {status_code}: {message}'.format(
                    status_code=status_code,
                    message=response_body
                )
            )

Synchronize content metadata using the Degreed course content API .


def _post(self, url, data, scope):
        """
        Make a POST request using the session object to a Degreed endpoint.

        Args:
            url (str): The url to send a POST request to.
            data (str): The json encoded payload to POST.
            scope (str): Must be one of the scopes Degreed expects:
                        - `CONTENT_PROVIDER_SCOPE`
                        - `COMPLETION_PROVIDER_SCOPE`
        """
        self._create_session(scope)
        response = self.session.post(url, data=data)
        return response.status_code, response.text

def _post ( self , url , data , scope ) : self . _create_session ( scope ) response = self . session . post ( url , data = data ) return response . status_code , response . text

Make a POST request using the session object to a Degreed endpoint .


def _delete(self, url, data, scope):
        """
        Make a DELETE request using the session object to a Degreed endpoint.

        Args:
            url (str): The url to send a DELETE request to.
            data (str): The json encoded payload to DELETE.
            scope (str): Must be one of the scopes Degreed expects:
                        - `CONTENT_PROVIDER_SCOPE`
                        - `COMPLETION_PROVIDER_SCOPE`
        """
        self._create_session(scope)
        response = self.session.delete(url, data=data)
        return response.status_code, response.text

def _delete ( self , url , data , scope ) : self . _create_session ( scope ) response = self . session . delete ( url , data = data ) return response . status_code , response . text

Make a DELETE request using the session object to a Degreed endpoint .


def _create_session(self, scope):
        """
        Instantiate a new session object for use in connecting with Degreed
        """
        now = datetime.datetime.utcnow()
        if self.session is None or self.expires_at is None or now >= self.expires_at:
            # Create a new session with a valid token
            if self.session:
                self.session.close()
            oauth_access_token, expires_at = self._get_oauth_access_token(
                self.enterprise_configuration.key,
                self.enterprise_configuration.secret,
                self.enterprise_configuration.degreed_user_id,
                self.enterprise_configuration.degreed_user_password,
                scope
            )
            session = requests.Session()
            session.timeout = self.SESSION_TIMEOUT
            session.headers['Authorization'] = 'Bearer {}'.format(oauth_access_token)
            session.headers['content-type'] = 'application/json'
            self.session = session
            self.expires_at = expires_at

def
_create_session
(
self
,
scope
)
:
now
=
datetime
.
datetime
.
utcnow
(
)
if
self
.
session
is
or
self
.
expires_at
is
or
now
>=
self
.
expires_at
:
if
self
.
session
:
self
.
session
.
close
(
)
oauth_access_token
,
expires_at
=
self
.
_get_oauth_access_token
(
self
.
enterprise_configuration
.
key
,
self
.
enterprise_configuration
.
secret
,
self
.
enterprise_configuration
.
degreed_user_id
,
self
.
enterprise_configuration
.
degreed_user_password
,
scope
)
session
=
requests
.
Session
(
)
session
.
timeout
=
self
.
SESSION_TIMEOUT
session
.
headers
[
'Authorization'
]
=
'Bearer {}'
.
format
(
oauth_access_token
)
session
.
headers
[
'content-type'
]
=
'application/json'
self
.
session
=
session
self
.
expires_at
=
expires_at


def ensure_data_exists(self, request, data, error_message=None):
        """
        Ensure that the wrapped API client's response brings us valid data. If not, raise an error and log it.
        """
        if not data:
            error_message = (
                error_message or "Unable to fetch API response from endpoint '{}'.".format(request.get_full_path())
            )
            LOGGER.error(error_message)
            raise NotFound(error_message)

Ensure that the wrapped API client s response brings us valid data . If not raise an error and log it .


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})

def contains_content_items ( self , request , pk , course_run_ids , program_uuids ) : enterprise_customer = self . get_object ( ) 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 } )

Return whether or not the specified content is available to the EnterpriseCustomer .


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)

Retrieve the list of courses contained within the catalog linked to this enterprise .


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)

Creates a course enrollment for an EnterpriseCustomerUser .