Beispiel #1
0
    def test_get_course_catalogs_for_single_catalog_with_id(self):
        """
        Verify that method "get_course_catalogs" returns proper response for a
        single catalog by its id.
        """
        self.mock_access_token_response()
        self.mock_catalog_detail_endpoint(
            self.site_configuration.discovery_api_url)

        catalog_id = 1
        resource = "catalogs"
        cache_key = get_cache_key(site_domain=self.site.domain,
                                  resource="{}-{}".format(
                                      resource, catalog_id))
        course_catalogs_cached_response = TieredCache.get_cached_response(
            cache_key)
        self.assertFalse(course_catalogs_cached_response.is_found)

        response = get_course_catalogs(self.request.site, catalog_id)
        self.assertEqual(response['name'], 'All Courses')

        course_cached_response = TieredCache.get_cached_response(cache_key)
        self.assertEqual(course_cached_response.value, response)

        # Verify the API was actually hit (not the cache)
        self._assert_num_requests(2)
Beispiel #2
0
    def test_get_course_run_info_from_catalog(self, course_run):
        """ Check to see if course info gets cached """
        self.mock_access_token_response()
        if course_run:
            resource = "course_runs"
            course = CourseFactory(partner=self.partner)
            product = course.create_or_update_seat('verified', None, 100)
            key = CourseKey.from_string(product.attr.course_key)
            self.mock_course_run_detail_endpoint(
                course,
                discovery_api_url=self.site_configuration.discovery_api_url)
        else:
            resource = "courses"
            product = create_or_update_course_entitlement(
                'verified', 100, self.partner, 'foo-bar',
                'Foo Bar Entitlement')
            key = product.attr.UUID
            self.mock_course_detail_endpoint(
                discovery_api_url=self.site_configuration.discovery_api_url,
                course=product)

        cache_key = get_cache_key(site_domain=self.site.domain,
                                  resource="{}-{}".format(resource, key))
        course_cached_response = TieredCache.get_cached_response(cache_key)
        self.assertFalse(course_cached_response.is_found)

        response = get_course_info_from_catalog(self.request.site, product)

        if course_run:
            self.assertEqual(response['title'], course.name)
        else:
            self.assertEqual(response['title'], product.title)

        course_cached_response = TieredCache.get_cached_response(cache_key)
        self.assertEqual(course_cached_response.value, response)
Beispiel #3
0
def catalog_contains_course_runs(site,
                                 course_run_ids,
                                 enterprise_customer_uuid,
                                 enterprise_customer_catalog_uuid=None):
    """
    Determine if course runs are associated with the EnterpriseCustomer.
    """
    query_params = {'course_run_ids': course_run_ids}
    api_resource_name = 'enterprise-customer'
    api_resource_id = enterprise_customer_uuid
    if enterprise_customer_catalog_uuid:
        api_resource_name = 'enterprise_catalogs'
        api_resource_id = enterprise_customer_catalog_uuid

    cache_key = get_cache_key(
        site_domain=site.domain,
        resource='{resource}-{resource_id}-contains_content_items'.format(
            resource=api_resource_name,
            resource_id=api_resource_id,
        ),
        query_params=urlencode(query_params, True))

    contains_content_cached_response = TieredCache.get_cached_response(
        cache_key)
    if contains_content_cached_response.is_found:
        return contains_content_cached_response.value

    api = site.siteconfiguration.enterprise_api_client
    endpoint = getattr(api, api_resource_name)(api_resource_id)
    contains_content = endpoint.contains_content_items.get(
        **query_params)['contains_content_items']
    TieredCache.set_all_tiers(cache_key, contains_content,
                              settings.ENTERPRISE_API_CACHE_TIMEOUT)

    return contains_content
Beispiel #4
0
    def run_catalog_query(self, product):
        """
        Retrieve the results from running the query contained in catalog_query field.
        """
        request = get_current_request()
        partner_code = request.site.siteconfiguration.partner.short_code
        cache_key = get_cache_key(
            site_domain=request.site.domain,
            partner_code=partner_code,
            resource='course_runs.contains',
            course_id=product.course_id,
            query=self.catalog_query
        )
        response = cache.get(cache_key)
        if not response:  # pragma: no cover
            try:
                response = request.site.siteconfiguration.course_catalog_api_client.course_runs.contains.get(
                    query=self.catalog_query,
                    course_run_ids=product.course_id,
                    partner=partner_code
                )
                cache.set(cache_key, response, settings.COURSES_API_CACHE_TIMEOUT)
            except:  # pylint: disable=bare-except
                raise Exception('Could not contact Course Catalog Service.')

        return response
Beispiel #5
0
    def catalog_contains_product(self, product):
        """
        Retrieve the results from using the catalog contains endpoint for
        catalog service for the catalog id contained in field "course_catalog".
        """
        request = get_current_request()
        partner_code = request.site.siteconfiguration.partner.short_code
        cache_key = get_cache_key(site_domain=request.site.domain,
                                  partner_code=partner_code,
                                  resource='catalogs.contains',
                                  course_id=product.course_id,
                                  catalog_id=self.course_catalog)
        response = cache.get(cache_key)
        if not response:
            discovery_api_client = request.site.siteconfiguration.discovery_api_client
            try:
                # GET: /api/v1/catalogs/{catalog_id}/contains?course_run_id={course_run_ids}
                response = discovery_api_client.catalogs(
                    self.course_catalog).contains.get(
                        course_run_id=product.course_id)
                cache.set(cache_key, response,
                          settings.COURSES_API_CACHE_TIMEOUT)
            except (ConnectionError, SlumberBaseException, Timeout):
                raise Exception(
                    'Unable to connect to Discovery Service for catalog contains endpoint.'
                )

        return response
Beispiel #6
0
    def _identify_uncached_product_identifiers(self, lines, domain, partner_code, query):
        """
        Checks the cache to see if each line is in the catalog range specified by the given query
        and tracks identifiers for which discovery service data is still needed.
        """
        uncached_course_run_ids = []
        uncached_course_uuids = []

        applicable_lines = lines
        for line in applicable_lines:
            if line.product.is_seat_product:
                product_id = line.product.course.id
            else:  # All lines passed to this method should either have a seat or an entitlement product
                product_id = line.product.attr.UUID

            cache_key = get_cache_key(
                site_domain=domain,
                partner_code=partner_code,
                resource='catalog_query.contains',
                course_id=product_id,
                query=query
            )
            in_catalog_range_cached_response = TieredCache.get_cached_response(cache_key)

            if not in_catalog_range_cached_response.is_found:
                if line.product.is_seat_product:
                    uncached_course_run_ids.append({'id': product_id, 'cache_key': cache_key, 'line': line})
                else:
                    uncached_course_uuids.append({'id': product_id, 'cache_key': cache_key, 'line': line})
            elif not in_catalog_range_cached_response.value:
                applicable_lines.remove(line)

        return uncached_course_run_ids, uncached_course_uuids, applicable_lines
Beispiel #7
0
def fetch_journal_bundle(site, journal_bundle_uuid):
    """
    Retrieve journal bundle for given uuid.
    Retrieve it from cache if present, otherwise send GET request to journal bundle
        discovery api and store in cache.

    Args:
        site (Site): site for current request
        journal_bundle_uuid (str): uuid for desired journal bundle

    Returns:
        (dict): contains dict of journal_bundle attributes

    Raises:
        ConnectionError: raised if ecommerce is unable to connect to enterprise api server.
        SlumberBaseException: raised if API response contains http error status like 4xx, 5xx etc...
        Timeout: request is raised if API is taking too long to respond
    """

    api_resource = 'journal_bundle'
    cache_key = get_cache_key(site_domain=site.domain,
                              resource=api_resource,
                              journal_bundle_uuid=journal_bundle_uuid)

    journal_bundle_cached_response = TieredCache.get_cached_response(cache_key)
    if journal_bundle_cached_response.is_hit:
        return journal_bundle_cached_response.value

    client = site.siteconfiguration.journal_discovery_api_client
    journal_bundle = client.journal_bundles(journal_bundle_uuid).get()
    TieredCache.set_all_tiers(cache_key, journal_bundle,
                              JOURNAL_BUNDLE_CACHE_TIMEOUT)

    return journal_bundle
Beispiel #8
0
def get_with_access_to(site, user, jwt, enterprise_id):
    """
    Get the enterprises that this user has access to for the data api permission django group.
    """
    api_resource_name = 'enterprise-customer'
    api = EdxRestApiClient(site.siteconfiguration.enterprise_api_url, jwt=jwt)
    endpoint = getattr(api, api_resource_name)

    cache_key = get_cache_key(
        resource='{api_resource_name}-with_access_to_enterprises'.format(api_resource_name=api_resource_name),
        user=user.username,
        enterprise_customer=enterprise_id,
    )
    cached_response = TieredCache.get_cached_response(cache_key)
    if cached_response.is_found:
        return cached_response.value
    try:
        query_params = {
            'permissions': [settings.ENTERPRISE_DATA_API_GROUP],
            'enterprise_id': enterprise_id,
        }
        response = endpoint.with_access_to.get(**query_params)
    except (ConnectionError, SlumberHttpBaseException, Timeout) as exc:
        logger.warning('Unable to retrieve Enterprise Customer with_access_to details for user: %s: %r',
                       user.username, exc)
        return None
    if response.get('results', None) is None or response['count'] == 0:
        logger.warning('Unable to process Enterprise Customer with_access_to details for user: %s, enterprise: %s'
                       ' No Results Found', user.username, enterprise_id)
        return None
    if response['count'] > 1:
        logger.warning('Multiple Enterprise Customers found for user: %s, enterprise: %s', user.username, enterprise_id)
        return None
    TieredCache.set_all_tiers(cache_key, response['results'][0], settings.ENTERPRISE_API_CACHE_TIMEOUT)
    return response['results'][0]
Beispiel #9
0
    def test_run_catalog_query(self):
        """
        run_course_query() should return True for included course run ID's.
        """
        course, seat = self.create_course_and_seat()
        self.mock_access_token_response()
        self.mock_course_runs_contains_endpoint(
            query='key:*', course_run_ids=[course.id], discovery_api_url=self.site_configuration.discovery_api_url
        )
        request = RequestFactory()
        request.site = self.site
        self.range.catalog_query = 'key:*'

        partner_code = request.site.siteconfiguration.partner.short_code
        cache_key = get_cache_key(
            site_domain=request.site.domain,
            partner_code=partner_code,
            resource='course_runs.contains',
            course_id=seat.course_id,
            query=self.range.catalog_query
        )
        cached_response = cache.get(cache_key)
        self.assertIsNone(cached_response)

        with mock.patch('ecommerce.core.url_utils.get_current_request', mock.Mock(return_value=request)):
            response = self.range.run_catalog_query(seat)
            self.assertTrue(response['course_runs'][course.id])
            cached_response = cache.get(cache_key)
            self.assertEqual(response, cached_response)
Beispiel #10
0
    def catalog_contains_product(self, product):
        """
        Retrieve the results from using the catalog contains endpoint for
        catalog service for the catalog id contained in field "course_catalog".
        """
        request = get_current_request()
        partner_code = request.site.siteconfiguration.partner.short_code
        cache_key = get_cache_key(site_domain=request.site.domain,
                                  partner_code=partner_code,
                                  resource='catalogs.contains',
                                  course_id=product.course_id,
                                  catalog_id=self.course_catalog)
        cached_response = TieredCache.get_cached_response(cache_key)
        if cached_response.is_found:
            return cached_response.value

        discovery_api_client = request.site.siteconfiguration.discovery_api_client
        try:
            # GET: /api/v1/catalogs/{catalog_id}/contains?course_run_id={course_run_ids}
            response = discovery_api_client.catalogs(
                self.course_catalog).contains.get(
                    course_run_id=product.course_id)

            TieredCache.set_all_tiers(cache_key, response,
                                      settings.COURSES_API_CACHE_TIMEOUT)
            return response
        except (ReqConnectionError, SlumberBaseException, Timeout) as exc:
            logger.exception(
                '[Code Redemption Failure] Unable to connect to the Discovery Service '
                'for catalog contains endpoint. '
                'Product: %s, Message: %s, Range: %s', product.id, exc,
                self.id)
            raise Exception(
                'Unable to connect to Discovery Service for catalog contains endpoint.'
            ) from exc
Beispiel #11
0
def fetch_course_catalog(site, catalog_id):
    """
    Fetch course catalog for the given catalog id.

    This method will fetch catalog for given catalog id, if there is no catalog with the given
    catalog id, method will return `None`.

    Arguments:
        site (Site): Instance of the current site.
        catalog_id (int): An integer specifying the primary key value of catalog to fetch.

    Example:
        >>> fetch_course_catalog(site, catalog_id=1)
        {
            "id": 1,
            "name": "All Courses",
            "query": "*:*",
            ...
        }
    Returns:
        (dict): A dictionary containing key/value pairs corresponding to catalog attribute/values.

    Raises:
        ConnectionError: requests exception "ConnectionError", raised if if ecommerce is unable to connect
            to enterprise api server.
        SlumberBaseException: base slumber exception "SlumberBaseException", raised if API response contains
            http error status like 4xx, 5xx etc.
        Timeout: requests exception "Timeout", raised if enterprise API is taking too long for returning
            a response. This exception is raised for both connection timeout and read timeout.

    """
    api_resource = 'catalogs'

    cache_key = get_cache_key(
        site_domain=site.domain,
        resource=api_resource,
        catalog_id=catalog_id,
    )

    cached_response = TieredCache.get_cached_response(cache_key)
    if cached_response.is_found:
        return cached_response.value

    api = site.siteconfiguration.discovery_api_client
    endpoint = getattr(api, api_resource)

    try:
        response = endpoint(catalog_id).get()
    except HttpNotFoundError:
        logger.exception("Catalog '%s' not found.", catalog_id)
        raise

    TieredCache.set_all_tiers(cache_key, response,
                              settings.COURSES_API_CACHE_TIMEOUT)
    return response
Beispiel #12
0
def fetch_enterprise_learner_entitlements(site, learner_id):
    """
    Fetch enterprise learner entitlements along-with data sharing consent requirement.

    Arguments:
        site (Site): site instance.
        learner_id (int): Primary key identifier for the enterprise learner.

    Example:
        >>> from django.contrib.sites.shortcuts import get_current_site
        >>> site  = get_current_site()
        >>> fetch_enterprise_learner_entitlements(site, 1)
        [
            {
                "requires_consent": False,
                "entitlement_id": 1
            },
        ]

    Returns:
         (list): Containing dicts of the following structure
            {
                "requires_consent": True,
                "entitlement_id": 1
            }

    Raises:
        ConnectionError: requests exception "ConnectionError", raised if if ecommerce is unable to connect
            to enterprise api server.
        SlumberBaseException: base slumber exception "SlumberBaseException", raised if API response contains
            http error status like 4xx, 5xx etc.
        Timeout: requests exception "Timeout", raised if enterprise API is taking too long for returning
            a response. This exception is raised for both connection timeout and read timeout.
    """
    resource_url = 'enterprise-learner/{learner_id}/entitlements'.format(
        learner_id=learner_id)
    cache_key = get_cache_key(
        site_domain=site.domain,
        partner_code=site.siteconfiguration.partner.short_code,
        resource=resource_url,
        learner_id=learner_id)

    entitlements_cached_response = TieredCache.get_cached_response(cache_key)
    if entitlements_cached_response.is_found:
        return entitlements_cached_response.value

    api = site.siteconfiguration.enterprise_api_client
    entitlements = getattr(api, resource_url).get()

    TieredCache.set_all_tiers(cache_key, entitlements,
                              settings.ENTERPRISE_API_CACHE_TIMEOUT)
    return entitlements
Beispiel #13
0
def fetch_course_catalog(site, catalog_id):
    """
    Fetch course catalog for the given catalog id.

    This method will fetch catalog for given catalog id, if there is no catalog with the given
    catalog id, method will return `None`.

    Arguments:
        site (Site): Instance of the current site.
        catalog_id (int): An integer specifying the primary key value of catalog to fetch.

    Example:
        >>> fetch_course_catalog(site, catalog_id=1)
        {
            "id": 1,
            "name": "All Courses",
            "query": "*:*",
            ...
        }
    Returns:
        (dict): A dictionary containing key/value pairs corresponding to catalog attribute/values.

    Raises:
        HTTPError: requests exception "HTTPError".
    """
    api_resource = 'catalogs'

    cache_key = get_cache_key(
        site_domain=site.domain,
        resource=api_resource,
        catalog_id=catalog_id,
    )

    cached_response = TieredCache.get_cached_response(cache_key)
    if cached_response.is_found:
        return cached_response.value

    api_client = site.siteconfiguration.oauth_api_client
    api_url = urljoin(f"{site.siteconfiguration.discovery_api_url}/",
                      f"{api_resource}/{catalog_id}/")

    response = api_client.get(api_url)
    if response.status_code == 404:
        logger.exception("Catalog '%s' not found.", catalog_id)

    response.raise_for_status()

    result = response.json()

    TieredCache.set_all_tiers(cache_key, result,
                              settings.COURSES_API_CACHE_TIMEOUT)
    return result
Beispiel #14
0
def get_course_run_detail(site, course_run_key):
    """
    Return the course run information of given course_run_key from Discovery Service and cache.

    Arguments:
        site (Site): Site object containing Site Configuration data
        course_run_key (str): Course run key

    Returns:
        dict: CourseRun information received from Discovery API
    """
    resource = "course_runs"
    cache_key = get_cache_key(site_domain=site.domain,
                              resource="{}-{}".format(resource,
                                                      course_run_key))
    return _get_discovery_response(site, cache_key, resource, course_run_key)
Beispiel #15
0
def get_course_detail(site, course_resource_id):
    """
    Return the course information of given course's resource from Discovery Service and cache.

    Arguments:
        site (Site): Site object containing Site Configuration data
        course_resource_id (UUID or str): It can be course UUID or course key

    Returns:
        dict: Course information received from Discovery API
    """
    resource = "courses"
    cache_key = get_cache_key(site_domain=site.domain,
                              resource="{}-{}".format(resource,
                                                      course_resource_id))
    return _get_discovery_response(site, cache_key, resource,
                                   course_resource_id)
Beispiel #16
0
 def get_lms_resource(self, basket, resource_name, endpoint):
     cache_key = get_cache_key(
         site_domain=basket.site.domain,
         resource=resource_name,
         username=basket.owner.username,
     )
     data_list = cache.get(cache_key)
     if not data_list:
         user = basket.owner.username
         try:
             data_list = endpoint.get(user=user)
             cache.set(cache_key, data_list,
                       settings.ENROLLMENT_API_CACHE_TIMEOUT)
         except (ConnectionError, SlumberBaseException, Timeout) as exc:
             logger.error('Failed to retrieve %s : %s', resource_name,
                          str(exc))
     return data_list if data_list else []
Beispiel #17
0
def catalog_contains_course_runs(site,
                                 course_run_ids,
                                 enterprise_customer_uuid,
                                 enterprise_customer_catalog_uuid=None):
    """
    Determine if course runs are associated with the EnterpriseCustomer.
    """
    query_params = {'course_run_ids': course_run_ids}
    api_resource_name = 'enterprise-customer'
    api_resource_id = enterprise_customer_uuid
    if enterprise_customer_catalog_uuid:
        api_resource_name = 'enterprise_catalogs'
        api_resource_id = enterprise_customer_catalog_uuid

    cache_key = get_cache_key(
        site_domain=site.domain,
        resource='{resource}-{resource_id}-contains_content_items'.format(
            resource=api_resource_name,
            resource_id=api_resource_id,
        ),
        query_params=urlencode(query_params, True))

    contains_content_cached_response = TieredCache.get_cached_response(
        cache_key)
    if contains_content_cached_response.is_hit:
        return contains_content_cached_response.value

    api = site.siteconfiguration.enterprise_api_client
    endpoint = getattr(api, api_resource_name)(api_resource_id)
    try:
        contains_content = endpoint.contains_content_items.get(
            **query_params)['contains_content_items']

        TieredCache.set_all_tiers(cache_key, contains_content,
                                  settings.ENTERPRISE_API_CACHE_TIMEOUT)
    except (ConnectionError, KeyError, SlumberHttpBaseException, Timeout):
        logger.exception(
            'Failed to check if course_runs [%s] exist in '
            'EnterpriseCustomerCatalog [%s]'
            'for EnterpriseCustomer [%s].',
            course_run_ids,
            enterprise_customer_catalog_uuid,
            enterprise_customer_uuid,
        )
        contains_content = False
    return contains_content
Beispiel #18
0
    def _get_lms_resource_for_user(self, basket, resource_name, endpoint):
        cache_key = get_cache_key(
            site_domain=basket.site.domain,
            resource=resource_name,
            username=basket.owner.username,
        )
        data_list_cached_response = TieredCache.get_cached_response(cache_key)
        if data_list_cached_response.is_found:
            return data_list_cached_response.value

        user = basket.owner.username
        try:
            data_list = endpoint.get(user=user) or []
            TieredCache.set_all_tiers(cache_key, data_list, settings.LMS_API_CACHE_TIMEOUT)
        except (ConnectionError, SlumberBaseException, Timeout) as exc:
            logger.error('Failed to retrieve %s : %s', resource_name, str(exc))
            data_list = []
        return data_list
Beispiel #19
0
    def _get_lms_resource_for_user(self, basket, resource_name, client, endpoint):
        cache_key = get_cache_key(
            site_domain=basket.site.domain,
            resource=resource_name,
            username=basket.owner.username,
        )
        data_list_cached_response = TieredCache.get_cached_response(cache_key)
        if data_list_cached_response.is_found:
            return data_list_cached_response.value

        user = basket.owner.username
        try:
            response = client.get(endpoint, params={"user": user})
            response.raise_for_status()
            data_list = response.json() or []
            TieredCache.set_all_tiers(cache_key, data_list, settings.LMS_API_CACHE_TIMEOUT)
        except (ReqConnectionError, RequestException, Timeout) as exc:
            logger.error('Failed to retrieve %s : %s', resource_name, str(exc))
            data_list = []
        return data_list
Beispiel #20
0
    def _assert_get_course_catalogs(self, catalog_name_list):
        """
        Helper method to validate the response from the method
        "get_course_catalogs".
        """
        resource = "catalogs"
        cache_key = get_cache_key(site_domain=self.site.domain,
                                  resource=resource)
        course_catalogs_cached_response = TieredCache.get_cached_response(
            cache_key)
        self.assertFalse(course_catalogs_cached_response.is_found)

        response = get_course_catalogs(self.request.site)

        self.assertEqual(len(response), len(catalog_name_list))
        for catalog_index, catalog in enumerate(response):
            self.assertEqual(catalog['name'], catalog_name_list[catalog_index])

        course_cached_response = TieredCache.get_cached_response(cache_key)
        self.assertEqual(course_cached_response.value, response)
Beispiel #21
0
    def _assert_fetch_enterprise_learner_data(self):
        """
        Helper method to validate the response from the method
        "fetch_enterprise_learner_data".
        """
        cache_key = get_cache_key(
            site_domain=self.request.site.domain,
            partner_code=self.request.site.siteconfiguration.partner.short_code,
            resource='enterprise-learner',
            username=self.learner.username
        )

        enterprise_learner_cached_response = TieredCache.get_cached_response(cache_key)
        self.assertFalse(enterprise_learner_cached_response.is_found)

        response = enterprise_api.fetch_enterprise_learner_data(self.request.site, self.learner)
        self.assertEqual(len(response['results']), 1)

        course_cached_response = TieredCache.get_cached_response(cache_key)
        self.assertEqual(course_cached_response.value, response)
Beispiel #22
0
def get_course_catalogs(site, resource_id=None):
    """
    Get details related to course catalogs from Discovery Service.

    Arguments:
        site (Site): Site object containing Site Configuration data
        resource_id (int or str): Identifies a specific resource to be retrieved

    Returns:
        dict: Course catalogs received from Discovery API

    Raises:
        HTTPError: requests exception "HTTPError"
    """
    resource = "catalogs"
    cache_key = get_cache_key(
        site_domain=site.domain,
        resource=resource if resource_id is None else "{}-{}".format(resource, resource_id)
    )
    return _get_discovery_response(site, cache_key, 'catalogs', resource_id)
def is_course_in_enterprise_catalog(site, course_id, enterprise_catalog_id):
    """
    Verify that the provided course id exists in the site base list of course
    run keys from the provided enterprise course catalog.

    Arguments:
        course_id (str): The course ID.
        site: (django.contrib.sites.Site) site instance
        enterprise_catalog_id (Int): Course catalog id of enterprise

    Returns:
        Boolean

    """
    partner_code = site.siteconfiguration.partner.short_code
    cache_key = get_cache_key(site_domain=site.domain,
                              partner_code=partner_code,
                              resource='catalogs.contains',
                              course_id=course_id,
                              catalog_id=enterprise_catalog_id)
    cached_response = TieredCache.get_cached_response(cache_key)
    if cached_response.is_found:
        response = cached_response.value
    else:
        try:
            response = site.siteconfiguration.discovery_api_client.catalogs(
                enterprise_catalog_id).contains.get(course_run_id=course_id)
            TieredCache.set_all_tiers(cache_key, response,
                                      settings.COURSES_API_CACHE_TIMEOUT)
        except (ConnectionError, SlumberBaseException, Timeout):
            logger.exception(
                'Unable to connect to Discovery Service for catalog contains endpoint.'
            )
            return False

    try:
        return response['courses'][course_id]
    except KeyError:
        return False
Beispiel #24
0
    def catalog_contains_product(self, product):
        """
        Retrieve the results from using the catalog contains endpoint for
        catalog service for the catalog id contained in field "course_catalog".
        """
        request = get_current_request()
        partner_code = request.site.siteconfiguration.partner.short_code
        cache_key = get_cache_key(site_domain=request.site.domain,
                                  partner_code=partner_code,
                                  resource='catalogs.contains',
                                  course_id=product.course_id,
                                  catalog_id=self.course_catalog)
        cached_response = TieredCache.get_cached_response(cache_key)
        if cached_response.is_found:
            return cached_response.value

        api_client = request.site.siteconfiguration.oauth_api_client
        discovery_api_url = urljoin(
            f"{request.site.siteconfiguration.discovery_api_url}/",
            f"catalogs/{self.course_catalog}/contains/")
        try:
            response = api_client.get(
                discovery_api_url, params={"course_run_id": product.course_id})
            response.raise_for_status()
            response = response.json()

            TieredCache.set_all_tiers(cache_key, response,
                                      settings.COURSES_API_CACHE_TIMEOUT)
            return response
        except (ReqConnectionError, RequestException, Timeout) as exc:
            logger.exception(
                '[Code Redemption Failure] Unable to connect to the Discovery Service '
                'for catalog contains endpoint. '
                'Product: %s, Message: %s, Range: %s', product.id, exc,
                self.id)
            raise Exception(
                'Unable to connect to Discovery Service for catalog contains endpoint.'
            ) from exc
Beispiel #25
0
def catalog_contains_course_runs(site, course_run_ids, enterprise_customer_uuid, enterprise_customer_catalog_uuid=None):
    """
    Determine if course runs are associated with the EnterpriseCustomer.
    """
    query_params = {'course_run_ids': course_run_ids}
    api_client = site.siteconfiguration.oauth_api_client

    # Determine API resource to use
    api_resource_name = 'enterprise-customer'
    api_resource_id = enterprise_customer_uuid
    if enterprise_customer_catalog_uuid:
        api_resource_name = 'enterprise-catalogs'
        api_resource_id = enterprise_customer_catalog_uuid

    cache_key = get_cache_key(
        site_domain=site.domain,
        resource='{resource}-{resource_id}-contains_content_items'.format(
            resource=api_resource_name,
            resource_id=api_resource_id,
        ),
        query_params=urlencode(query_params, True)
    )

    contains_content_cached_response = TieredCache.get_cached_response(cache_key)
    if contains_content_cached_response.is_found:
        return contains_content_cached_response.value

    api_url = urljoin(
        f"{site.siteconfiguration.enterprise_catalog_api_url}/",
        f"{api_resource_name}/{api_resource_id}/contains_content_items/"
    )
    response = api_client.get(api_url, params=query_params)
    response.raise_for_status()
    contains_content = response.json()['contains_content_items']
    TieredCache.set_all_tiers(cache_key, contains_content, settings.ENTERPRISE_API_CACHE_TIMEOUT)

    return contains_content
Beispiel #26
0
    def get(self, request):
        """ Calculate basket totals given a list of sku's

        Create a temporary basket add the sku's and apply an optional voucher code.
        Then calculate the total price less discounts. If a voucher code is not
        provided apply a voucher in the Enterprise entitlements available
        to the user.

        Arguments:
            sku (string): A list of sku(s) to calculate
            code (string): Optional voucher code to apply to the basket.
            username (string): Optional username of a user for which to caclulate the basket.

        Returns:
            JSON: {
                    'total_incl_tax_excl_discounts': basket.total_incl_tax_excl_discounts,
                    'total_incl_tax': basket.total_incl_tax,
                    'currency': basket.currency
                }
        """
        partner = get_partner_for_site(request)
        skus = request.GET.getlist('sku')
        if not skus:
            return HttpResponseBadRequest(_('No SKUs provided.'))

        code = request.GET.get('code', None)
        try:
            voucher = Voucher.objects.get(code=code) if code else None
        except Voucher.DoesNotExist:
            voucher = None

        products = Product.objects.filter(stockrecords__partner=partner,
                                          stockrecords__partner_sku__in=skus)
        if not products:
            return HttpResponseBadRequest(
                _('Products with SKU(s) [{skus}] do not exist.').format(
                    skus=', '.join(skus)))

        # If there is only one product apply an Enterprise entitlement voucher
        if not voucher and len(products) == 1:
            voucher = get_entitlement_voucher(request, products[0])

        username = request.GET.get('username', default='')
        user = request.user

        # True if this request was made by the marketing user, and does not include a username
        # query param, and thus is calculating the non-logged in (anonymous) price.
        # Note: We need to verify separately that all calls without a username query param
        # can be treated in this same way.
        is_marketing_anonymous_request = False

        # If a username is passed in, validate that the user has staff access or is the same user.
        if username:
            if user.is_staff or (user.username.lower() == username.lower()):
                try:
                    user = User.objects.get(username=username)
                except User.DoesNotExist:
                    logger.debug('Request username: [%s] does not exist',
                                 username)
            else:
                return HttpResponseForbidden('Unauthorized user credentials')
        elif user.username == self.MARKETING_USER:
            is_marketing_anonymous_request = True

        cache_key = None
        # Since we know we can't have any enrollments or entitlements, we can directly get
        # the cached price.
        if is_marketing_anonymous_request:
            cache_key = get_cache_key(site_comain=request.site,
                                      resource_name='calculate',
                                      skus=skus)
            if waffle.flag_is_active(
                    request, "use_cached_basket_calculate_for_marketing_user"):
                basket_calculate_results = cache.get(cache_key)
                if basket_calculate_results:
                    return Response(basket_calculate_results)

        response = self._calculate_basket(user, request, products, voucher,
                                          skus, code)

        if response and is_marketing_anonymous_request:
            cache.set(cache_key, response,
                      settings.ANONYMOUS_BASKET_CALCULATE_CACHE_TIMEOUT)

        return Response(response)
Beispiel #27
0
    def test_fetch_journal_bundle(self):
        """ Test 'fetch_journal_bundle' properly calls API and uses cache """

        # The first time it is called the journal discovery api should get hit
        #     and store the journal bundle in the cache
        # The second time the api should not be called, the bundle should be retrieved from the cache

        self.mock_access_token_response()
        test_bundle = {
            "uuid":
            "4786e7be-2390-4332-a20e-e24895c38109",
            "title":
            "Transfiguration Bundle",
            "partner":
            "edX",
            "journals": [{
                "uuid": "a3db3f6e-f290-4eae-beea-873034c5a967",
                "partner": "edx",
                "organization": "edX",
                "title": "Intermediate Transfiguration",
                "price": "120.00",
                "currency": "USD",
                "sku": "88482D8",
                "card_image_url":
                "http://localhost:18606/media/original_images/transfiguration.jpg",
                "short_description": "Turning things into different things!",
                "full_description": "",
                "access_length": 365,
                "status": "active",
                "slug": "intermediate-transfiguration-about-page"
            }],
            "courses": [{
                "key":
                "HogwartsX+TR301",
                "uuid":
                "6d7c2805-ec9c-4961-8b0d-c8d608cc948e",
                "title":
                "Transfiguration 301",
                "course_runs": [{
                    "key": "course-v1:HogwartsX+TR301+TR301_2014",
                    "uuid": "ddaa84ce-e99c-4e3d-a3ca-7d5b4978b43b",
                    "title": "Transfiguration 301",
                    "image": 'fake_image_url',
                    "short_description": 'fake_description',
                    "marketing_url": 'fake_marketing_url',
                    "seats": [],
                    "start": "2030-01-01T00:00:00Z",
                    "end": "2040-01-01T00:00:00Z",
                    "enrollment_start": "2020-01-01T00:00:00Z",
                    "enrollment_end": "2040-01-01T00:00:00Z",
                    "pacing_type": "instructor_paced",
                    "type": "fake_course_type",
                    "status": "published"
                }],
                "entitlements": [],
                "owners": [{
                    "uuid": "becfbab0-c78d-42f1-b44e-c92abb99011a",
                    "key": "HogwartsX",
                    "name": ""
                }],
                "image":
                "fake_image_url",
                "short_description":
                "fake_description"
            }],
            "applicable_seat_types": ["verified"]
        }

        journal_bundle_uuid = test_bundle['uuid']
        test_url = urljoin(self.journal_discovery_url,
                           'journal_bundles/{}/'.format(journal_bundle_uuid))

        responses.add(responses.GET, test_url, json=test_bundle, status=200)

        # First call, should hit journal discovery api and store in cache
        journal_bundle_response = fetch_journal_bundle(
            site=self.site, journal_bundle_uuid=journal_bundle_uuid)

        # The first call (response.calls[0]) is to get post the access token
        # The second call (response.calls[1]) is the 'fetch_journal_bundle' call
        self.assertEqual(len(responses.calls), 2,
                         "Incorrect number of API calls")
        self.assertEqual(journal_bundle_response, test_bundle)

        # check that the journal bundle was stored in the cache
        cache_key = get_cache_key(site_domain=self.site.domain,
                                  resource='journal_bundle',
                                  journal_bundle_uuid=journal_bundle_uuid)
        journal_bundle_cached_response = TieredCache.get_cached_response(
            cache_key)
        self.assertTrue(journal_bundle_cached_response is not None)
        self.assertEqual(journal_bundle_cached_response.value, test_bundle)

        # Call 'fetch_journal_bundle' again, the api should not get hit again and response should be the same
        journal_bundle_response = fetch_journal_bundle(
            site=self.site, journal_bundle_uuid=journal_bundle_uuid)

        self.assertEqual(len(responses.calls), 2,
                         "Should have hit cache, not called API")
        self.assertEqual(journal_bundle_response, test_bundle)
Beispiel #28
0
    def get(self, request):  # pylint: disable=too-many-statements
        """ Calculate basket totals given a list of sku's

        Create a temporary basket add the sku's and apply an optional voucher code.
        Then calculate the total price less discounts. If a voucher code is not
        provided apply a voucher in the Enterprise entitlements available
        to the user.

        Query Params:
            sku (string): A list of sku(s) to calculate
            code (string): Optional voucher code to apply to the basket.
            username (string): Optional username of a user for which to calculate the basket.

        Returns:
            JSON: {
                    'total_incl_tax_excl_discounts': basket.total_incl_tax_excl_discounts,
                    'total_incl_tax': basket.total_incl_tax,
                    'currency': basket.currency
                }
        """
        RequestCache.set(TEMPORARY_BASKET_CACHE_KEY,
                         True)  # TODO: LEARNER 5463

        partner = get_partner_for_site(request)
        skus = request.GET.getlist('sku')
        if not skus:
            return HttpResponseBadRequest(_('No SKUs provided.'))
        skus.sort()

        code = request.GET.get('code', None)
        try:
            voucher = Voucher.objects.get(code=code) if code else None
        except Voucher.DoesNotExist:
            voucher = None

        products = Product.objects.filter(stockrecords__partner=partner,
                                          stockrecords__partner_sku__in=skus)
        if not products:
            return HttpResponseBadRequest(
                _('Products with SKU(s) [{skus}] do not exist.').format(
                    skus=', '.join(skus)))

        # If there is only one product apply an Enterprise entitlement voucher
        if not voucher and len(products) == 1:
            voucher = get_entitlement_voucher(request, products[0])

        basket_owner = request.user

        requested_username = request.GET.get('username', default='')
        is_anonymous = request.GET.get('is_anonymous',
                                       'false').lower() == 'true'

        use_default_basket = is_anonymous

        # validate query parameters
        if requested_username and is_anonymous:
            return HttpResponseBadRequest(
                _('Provide username or is_anonymous query param, but not both')
            )
        elif not requested_username and not is_anonymous:
            logger.warning(
                "Request to Basket Calculate must supply either username or is_anonymous query"
                " param. Requesting user=%s. Future versions of this API will treat this "
                "WARNING as an ERROR and raise an exception.",
                basket_owner.username)
            requested_username = request.user.username

        # If a username is passed in, validate that the user has staff access or is the same user.
        if requested_username:
            if basket_owner.username.lower() == requested_username.lower():
                pass
            elif basket_owner.is_staff:
                try:
                    basket_owner = User.objects.get(
                        username=requested_username)
                except User.DoesNotExist:
                    # This case represents a user who is logged in to marketing, but
                    # doesn't yet have an account in ecommerce. These users have
                    # never purchased before.
                    use_default_basket = True
            else:
                return HttpResponseForbidden('Unauthorized user credentials')

        if basket_owner.username == self.MARKETING_USER and not use_default_basket:
            # For legacy requests that predate is_anonymous parameter, we will calculate
            # an anonymous basket if the calculated user is the marketing user.
            # TODO: LEARNER-5057: Remove this special case for the marketing user
            # once logs show no more requests with no parameters (see above).
            use_default_basket = True

        if use_default_basket:
            basket_owner = None

        cache_key = None
        if use_default_basket:
            # For an anonymous user we can directly get the cached price, because
            # there can't be any enrollments or entitlements.
            cache_key = get_cache_key(site_comain=request.site,
                                      resource_name='calculate',
                                      skus=skus)
            cached_response = TieredCache.get_cached_response(cache_key)
            if cached_response.is_hit:
                return Response(cached_response.value)

        if waffle.flag_is_active(
                request,
                "disable_calculate_temporary_basket_atomic_transaction"):
            response = self._calculate_temporary_basket(
                basket_owner, request, products, voucher, skus, code)
        else:
            response = self._calculate_temporary_basket_atomic(
                basket_owner, request, products, voucher, skus, code)

        if response and use_default_basket:
            TieredCache.set_all_tiers(
                cache_key, response,
                settings.ANONYMOUS_BASKET_CALCULATE_CACHE_TIMEOUT)

        return Response(response)
Beispiel #29
0
def fetch_enterprise_learner_data(site, user):
    """
    Fetch information related to enterprise and its entitlements from the Enterprise
    Service.

    Example:
        fetch_enterprise_learner_data(site, user)

    Arguments:
        site: (Site) site instance
        user: (User) django auth user

    Returns:
        dict: {
            "enterprise_api_response_for_learner": {
                "count": 1,
                "num_pages": 1,
                "current_page": 1,
                "results": [
                    {
                        "enterprise_customer": {
                            "uuid": "cf246b88-d5f6-4908-a522-fc307e0b0c59",
                            "name": "BigEnterprise",
                            "catalog": 2,
                            "active": true,
                            "site": {
                                "domain": "example.com",
                                "name": "example.com"
                            },
                            "enable_data_sharing_consent": true,
                            "enforce_data_sharing_consent": "at_login",
                            "branding_configuration": {
                                "enterprise_customer": "cf246b88-d5f6-4908-a522-fc307e0b0c59",
                                "logo": "https://open.edx.org/sites/all/themes/edx_open/logo.png"
                            },
                            "enterprise_customer_entitlements": [
                                {
                                    "enterprise_customer": "cf246b88-d5f6-4908-a522-fc307e0b0c59",
                                    "entitlement_id": 69
                                }
                            ]
                        },
                        "user_id": 5,
                        "user": {
                            "username": "******",
                            "first_name": "",
                            "last_name": "",
                            "email": "*****@*****.**",
                            "is_staff": true,
                            "is_active": true,
                            "date_joined": "2016-09-01T19:18:26.026495Z"
                        },
                        "data_sharing_consent_records": [
                            {
                                "username": "******",
                                "enterprise_customer_uuid": "cf246b88-d5f6-4908-a522-fc307e0b0c59",
                                "exists": true,
                                "consent_provided": true,
                                "consent_required": false,
                                "course_id": "course-v1:edX DemoX Demo_Course",
                            }
                        ]
                    }
                ],
                "next": null,
                "start": 0,
                "previous": null
            }
        }

    Raises:
        ConnectionError: requests exception "ConnectionError", raised if if ecommerce is unable to connect
            to enterprise api server.
        SlumberBaseException: base slumber exception "SlumberBaseException", raised if API response contains
            http error status like 4xx, 5xx etc.
        Timeout: requests exception "Timeout", raised if enterprise API is taking too long for returning
            a response. This exception is raised for both connection timeout and read timeout.

    """
    api_resource_name = 'enterprise-learner'
    partner_code = site.siteconfiguration.partner.short_code
    cache_key = get_cache_key(
        site_domain=site.domain,
        partner_code=partner_code,
        resource=api_resource_name,
        username=user.username
    )

    cached_response = TieredCache.get_cached_response(cache_key)
    if cached_response.is_found:
        return cached_response.value

    api = site.siteconfiguration.enterprise_api_client
    endpoint = getattr(api, api_resource_name)
    querystring = {'username': user.username}
    response = endpoint().get(**querystring)

    TieredCache.set_all_tiers(cache_key, response, settings.ENTERPRISE_API_CACHE_TIMEOUT)
    return response
Beispiel #30
0
    def get(self, request):
        """ Calculate basket totals given a list of sku's

        Create a temporary basket add the sku's and apply an optional voucher code.
        Then calculate the total price less discounts. If a voucher code is not
        provided apply a voucher in the Enterprise entitlements available
        to the user.

        Query Params:
            sku (string): A list of sku(s) to calculate
            code (string): Optional voucher code to apply to the basket.
            username (string): Optional username of a user for which to calculate the basket.

        Returns:
            JSON: {
                    'total_incl_tax_excl_discounts': basket.total_incl_tax_excl_discounts,
                    'total_incl_tax': basket.total_incl_tax,
                    'currency': basket.currency
                }

         Side effects:
            If the basket owner does not have an LMS user id, tries to find it. If found, adds the id to the user and
            saves the user. If the id cannot be found, writes custom metrics to record this fact.
       """
        DEFAULT_REQUEST_CACHE.set(TEMPORARY_BASKET_CACHE_KEY, True)

        partner = get_partner_for_site(request)
        skus = request.GET.getlist('sku')
        if not skus:
            return HttpResponseBadRequest(_('No SKUs provided.'))
        skus.sort()

        code = request.GET.get('code', None)
        try:
            voucher = Voucher.objects.get(code=code) if code else None
        except Voucher.DoesNotExist:
            voucher = None

        products = Product.objects.filter(stockrecords__partner=partner,
                                          stockrecords__partner_sku__in=skus)
        if not products:
            return HttpResponseBadRequest(
                _('Products with SKU(s) [{skus}] do not exist.').format(
                    skus=', '.join(skus)))

        basket_owner = request.user

        requested_username = request.GET.get('username', default='')
        is_anonymous = request.GET.get('is_anonymous',
                                       'false').lower() == 'true'

        use_default_basket = is_anonymous

        # validate query parameters
        if requested_username and is_anonymous:
            return HttpResponseBadRequest(
                _('Provide username or is_anonymous query param, but not both')
            )
        if not requested_username and not is_anonymous:
            logger.warning(
                "Request to Basket Calculate must supply either username or is_anonymous query"
                " param. Requesting user=%s. Future versions of this API will treat this "
                "WARNING as an ERROR and raise an exception.",
                basket_owner.username)
            requested_username = request.user.username

        # If a username is passed in, validate that the user has staff access or is the same user.
        if requested_username:
            if basket_owner.username.lower() == requested_username.lower():
                pass
            elif basket_owner.is_staff:
                try:
                    basket_owner = User.objects.get(
                        username=requested_username)
                except User.DoesNotExist:
                    # This case represents a user who is logged in to marketing, but
                    # doesn't yet have an account in ecommerce. These users have
                    # never purchased before.
                    use_default_basket = True
            else:
                return HttpResponseForbidden('Unauthorized user credentials')

        if basket_owner.username == self.MARKETING_USER and not use_default_basket:
            # For legacy requests that predate is_anonymous parameter, we will calculate
            # an anonymous basket if the calculated user is the marketing user.
            # TODO: LEARNER-5057: Remove this special case for the marketing user
            # once logs show no more requests with no parameters (see above).
            use_default_basket = True

        if use_default_basket:
            basket_owner = None

        # If we have a basket owner, ensure they have an LMS user id
        try:
            if basket_owner:
                called_from = u'calculation of basket total'
                basket_owner.add_lms_user_id(
                    'ecommerce_missing_lms_user_id_calculate_basket_total',
                    called_from)
        except MissingLmsUserIdException:
            return self._report_bad_request(
                api_exceptions.LMS_USER_ID_NOT_FOUND_DEVELOPER_MESSAGE.format(
                    user_id=basket_owner.id),
                api_exceptions.LMS_USER_ID_NOT_FOUND_USER_MESSAGE)

        cache_key = None
        if use_default_basket:
            # For an anonymous user we can directly get the cached price, because
            # there can't be any enrollments or entitlements.
            cache_key = get_cache_key(site_comain=request.site,
                                      resource_name='calculate',
                                      skus=skus)
            cached_response = TieredCache.get_cached_response(cache_key)
            logger.info(
                'bundle debugging 1: Cache key [%s] site [%s] skus [%s] response [%s]',
                str(cache_key), str(request.site), str(skus),
                str(cached_response))
            if cached_response.is_found:
                return Response(cached_response.value)

        response = self._calculate_temporary_basket_atomic(
            basket_owner, request, products, voucher, skus, code)
        logger.info(
            'bundle debugging 2: Cache key [%s] response [%s] skus [%s] timeout [%s]',
            str(cache_key), str(response), str(skus),
            str(settings.ANONYMOUS_BASKET_CALCULATE_CACHE_TIMEOUT))
        if response and use_default_basket:
            logger.info(
                'bundle debugging 3: setting cache: Cache key [%s] response [%s] skus [%s]',
                str(cache_key), str(response), str(skus))
            TieredCache.set_all_tiers(
                cache_key, response,
                settings.ANONYMOUS_BASKET_CALCULATE_CACHE_TIMEOUT)

        return Response(response)