Exemple #1
0
def consent_needed_for_course(request, user, course_id, enrollment_exists=False):
    """
    Wrap the enterprise app check to determine if the user needs to grant
    data sharing permissions before accessing a course.
    """
    consent_cache_key = get_data_consent_share_cache_key(user.id, course_id)
    data_sharing_consent_needed_cache = TieredCache.get_cached_response(consent_cache_key)
    if data_sharing_consent_needed_cache.is_found and data_sharing_consent_needed_cache.value is 0:
        return False

    enterprise_learner_details = get_enterprise_learner_data(user)
    if not enterprise_learner_details:
        consent_needed = False
    else:
        client = ConsentApiClient(user=request.user)
        consent_needed = any(
            client.consent_required(
                username=user.username,
                course_id=course_id,
                enterprise_customer_uuid=learner['enterprise_customer']['uuid'],
                enrollment_exists=enrollment_exists,
            )
            for learner in enterprise_learner_details
        )
    if not consent_needed:
        # Set an ephemeral item in the cache to prevent us from needing
        # to make a Consent API request every time this function is called.
        TieredCache.set_all_tiers(consent_cache_key, 0, settings.DATA_CONSENT_SHARE_CACHE_TIMEOUT)

    return consent_needed
Exemple #2
0
def get_enterprise_catalog(site, enterprise_catalog, limit, page):
    """
    Get the EnterpriseCustomerCatalog for a given catalog uuid.

    Args:
        site (Site): The site which is handling the current request
        enterprise_catalog (str): The uuid of the Enterprise Catalog
        limit (int): The number of results to return per page.
        page (int): The page number to fetch.

    Returns:
        dict: The result set containing the content objects associated with the Enterprise Catalog.
        NoneType: Return None if no catalog with that uuid is found.
    """
    resource = 'enterprise_catalogs'
    partner_code = site.siteconfiguration.partner.short_code
    cache_key = '{site_domain}_{partner_code}_{resource}_{catalog}_{limit}_{page}'.format(
        site_domain=site.domain,
        partner_code=partner_code,
        resource=resource,
        catalog=enterprise_catalog,
        limit=limit,
        page=page
    )
    cache_key = hashlib.md5(cache_key).hexdigest()

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

    client = get_enterprise_api_client(site)
    path = [resource, str(enterprise_catalog)]
    client = reduce(getattr, path, client)

    response = client.get(
        limit=limit,
        page=page,
    )
    TieredCache.set_all_tiers(cache_key, response, settings.CATALOG_RESULTS_CACHE_TIMEOUT)

    return response
Exemple #3
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]
Exemple #4
0
def get_enterprise_customer(site, uuid):
    """
    Return a single enterprise customer
    """
    resource = 'enterprise-customer'
    cache_key = u'{site_domain}_{partner_code}_{resource}_{enterprise_uuid}'.format(
        site_domain=site.domain,
        partner_code=site.siteconfiguration.partner.short_code,
        resource=resource,
        enterprise_uuid=uuid,
    )
    cache_key = hashlib.md5(cache_key.encode('utf-8')).hexdigest()
    cached_response = TieredCache.get_cached_response(cache_key)
    if cached_response.is_found:
        return cached_response.value

    client = get_enterprise_api_client(site)
    path = [resource, str(uuid)]
    client = reduce(getattr, path, client)

    try:
        response = client.get()
    except (ReqConnectionError, SlumberHttpBaseException, Timeout):
        return None

    enterprise_customer_response = {
        'name': response['name'],
        'id': response['uuid'],
        'enable_data_sharing_consent': response['enable_data_sharing_consent'],
        'enforce_data_sharing_consent':
        response['enforce_data_sharing_consent'],
        'contact_email': response.get('contact_email', ''),
        'slug': response.get('slug'),
        'sender_alias': response.get('sender_alias', ''),
    }

    TieredCache.set_all_tiers(
        cache_key, enterprise_customer_response,
        settings.ENTERPRISE_CUSTOMER_RESULTS_CACHE_TIMEOUT)

    return enterprise_customer_response
Exemple #5
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
Exemple #6
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

    api = site.siteconfiguration.enterprise_api_client
    # Temporarily gate enterprise catalog api usage behind waffle flag
    if can_use_enterprise_catalog(enterprise_customer_uuid):
        api = site.siteconfiguration.enterprise_catalog_api_client
        if enterprise_customer_catalog_uuid:
            api_resource_name = 'enterprise-catalogs'

    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

    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
Exemple #7
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)
    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
Exemple #8
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:
        ConnectionError: requests exception "ConnectionError"
        SlumberBaseException: slumber exception "SlumberBaseException"
        Timeout: requests exception "Timeout"

    """
    resource = 'catalogs'
    base_cache_key = '{}.catalog.api.data'.format(site.domain)

    cache_key = '{}.{}'.format(base_cache_key,
                               resource_id) if resource_id else base_cache_key
    cache_key = hashlib.md5(cache_key).hexdigest()

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

    api = site.siteconfiguration.discovery_api_client
    endpoint = getattr(api, resource)
    response = endpoint(resource_id).get()

    if resource_id:
        results = response
    else:
        results = deprecated_traverse_pagination(response, endpoint)

    TieredCache.set_all_tiers(cache_key, results,
                              settings.COURSES_API_CACHE_TIMEOUT)
    return results
    def is_verified(self, site):
        """
        Check if a user has verified his/her identity.
        Calls the LMS verification status API endpoint and returns the verification status information.
        The status information is stored in cache, if the user is verified, until the verification expires.

        Args:
            site (Site): The site object from which the LMS account API endpoint is created.

        Returns:
            True if the user is verified, false otherwise.
        """
        try:
            cache_key = 'verification_status_{username}'.format(
                username=self.username)
            cache_key = hashlib.md5(cache_key.encode('utf-8')).hexdigest()
            verification_cached_response = TieredCache.get_cached_response(
                cache_key)
            if verification_cached_response.is_found:
                return verification_cached_response.value

            api = site.siteconfiguration.user_api_client
            response = api.accounts(self.username).verification_status().get()

            verification = response.get('is_verified', False)
            if verification:
                cache_timeout = int(
                    (parse(response.get('expiration_datetime')) -
                     now()).total_seconds())
                TieredCache.set_all_tiers(cache_key, verification,
                                          cache_timeout)
            return verification
        except HttpNotFoundError:
            log.debug('No verification data found for [%s]', self.username)
            return False
        except (ReqConnectionError, SlumberBaseException, Timeout):
            msg = 'Failed to retrieve verification status details for [{username}]'.format(
                username=self.username)
            log.warning(msg)
            return False
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
Exemple #11
0
    def set_last_seen_courseware_timezone(self, user):
        """
        The timezone in the user's account is frequently not set.
        This method sets a user's recent timezone that can be used as a fallback
        """
        if not user.id:
            return

        cache_key = 'browser_timezone_{}'.format(str(user.id))
        browser_timezone = self.request.query_params.get(
            'browser_timezone', None)
        cached_value = TieredCache.get_cached_response(cache_key)
        if not cached_value.is_found:
            if browser_timezone:
                TieredCache.set_all_tiers(cache_key, str(browser_timezone),
                                          86400)  # Refresh the cache daily
                LastSeenCoursewareTimezone.objects.update_or_create(
                    user=user,
                    defaults={
                        'last_seen_courseware_timezone': browser_timezone
                    },
                )
Exemple #12
0
def _get_discovery_response(site, cache_key, resource, resource_id):
    """
    Return the discovery endpoint result of given resource or cached response if its already been cached.

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

    Returns:
        dict: resource's information for given resource_id received from Discovery API
    """
    course_cached_response = TieredCache.get_cached_response(cache_key)
    if course_cached_response.is_found:
        return course_cached_response.value

    params = {}

    if resource == 'course_runs':
        params['partner'] = site.siteconfiguration.partner.short_code

    api_client = site.siteconfiguration.oauth_api_client
    resource_path = f"{resource_id}/" if resource_id else ""
    discovery_api_url = urljoin(
        f"{site.siteconfiguration.discovery_api_url}/",
        f"{resource}/{resource_path}"
    )

    response = api_client.get(discovery_api_url, params=params)
    response.raise_for_status()

    result = response.json()

    if resource_id is None:
        result = deprecated_traverse_pagination(result, api_client, discovery_api_url)

    TieredCache.set_all_tiers(cache_key, result, settings.COURSES_API_CACHE_TIMEOUT)
    return result
Exemple #13
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
Exemple #14
0
def get_course_info_from_catalog(site, product):
    """ Get course or course_run information from Discovery Service and cache """
    if product.is_course_entitlement_product:
        key = product.attr.UUID
    else:
        key = CourseKey.from_string(product.attr.course_key)

    api = site.siteconfiguration.discovery_api_client
    partner_short_code = site.siteconfiguration.partner.short_code

    cache_key = 'courses_api_detail_{}{}'.format(key, partner_short_code)
    cache_key = hashlib.md5(cache_key).hexdigest()
    course_cached_response = TieredCache.get_cached_response(cache_key)
    if course_cached_response.is_found:
        return course_cached_response.value

    if product.is_course_entitlement_product:
        course = api.courses(key).get()
    else:
        course = api.course_runs(key).get(partner=partner_short_code)

    TieredCache.set_all_tiers(cache_key, course, settings.COURSES_API_CACHE_TIMEOUT)
    return course
Exemple #15
0
def consent_needed_for_course(request,
                              user,
                              course_id,
                              enrollment_exists=False):
    """
    Wrap the enterprise app check to determine if the user needs to grant
    data sharing permissions before accessing a course.
    """
    consent_cache_key = get_data_consent_share_cache_key(user.id, course_id)
    data_sharing_consent_needed_cache = TieredCache.get_cached_response(
        consent_cache_key)
    if data_sharing_consent_needed_cache.is_found and data_sharing_consent_needed_cache.value == 0:
        return False

    enterprise_learner_details = get_enterprise_learner_data(user)
    if not enterprise_learner_details:
        consent_needed = False
    else:
        client = ConsentApiClient(user=request.user)
        current_enterprise_uuid = enterprise_customer_uuid_for_request(request)
        consent_needed = any(
            current_enterprise_uuid == learner['enterprise_customer']['uuid']
            and Site.objects.get(
                domain=learner['enterprise_customer']['site']
                ['domain']) == request.site and client.consent_required(
                    username=user.username,
                    course_id=course_id,
                    enterprise_customer_uuid=current_enterprise_uuid,
                    enrollment_exists=enrollment_exists,
                ) for learner in enterprise_learner_details)
    if not consent_needed:
        # Set an ephemeral item in the cache to prevent us from needing
        # to make a Consent API request every time this function is called.
        TieredCache.set_all_tiers(consent_cache_key, 0,
                                  settings.DATA_CONSENT_SHARE_CACHE_TIMEOUT)

    return consent_needed
Exemple #16
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
Exemple #17
0
def get_cached_voucher(code):
    """
    Returns a voucher from cache if one is stored to cache, if not the voucher
    is retrieved from database and stored to cache.

    Arguments:
        code (str): The code of a coupon voucher.

    Returns:
        voucher (Voucher): The Voucher for the passed code.

    Raises:
        Voucher.DoesNotExist: When no vouchers with provided code exist.
    """
    voucher_code = 'voucher_{code}'.format(code=code)
    cache_key = hashlib.md5(voucher_code.encode('utf-8')).hexdigest()
    voucher_cached_response = TieredCache.get_cached_response(cache_key)
    if voucher_cached_response.is_found:
        return voucher_cached_response.value

    voucher = Voucher.objects.get(code=code)

    TieredCache.set_all_tiers(cache_key, voucher, settings.VOUCHER_CACHE_TIMEOUT)
    return voucher
Exemple #18
0
    def get_credit_providers(self):
        """
        Retrieve all credit providers from LMS.

        Results will be sorted alphabetically by display name.
        """
        key = 'credit_providers'
        credit_providers_cache_response = TieredCache.get_cached_response(key)
        if credit_providers_cache_response.is_found:
            return credit_providers_cache_response.value

        try:
            credit_api = self.request.site.siteconfiguration.credit_api_client
            credit_providers = credit_api.providers.get()
            credit_providers.sort(
                key=lambda provider: provider['display_name'])

            # Update the cache
            TieredCache.set_all_tiers(key, credit_providers,
                                      settings.CREDIT_PROVIDER_CACHE_TIMEOUT)
        except (SlumberBaseException, Timeout):
            logger.exception('Failed to retrieve credit providers!')
            credit_providers = []
        return credit_providers
Exemple #19
0
def get_enterprise_customer_catalogs(site, endpoint_request_url,
                                     enterprise_customer_uuid, page):
    """
    Get catalogs associated with an Enterprise Customer.

    Args:
        site (Site): The site which is handling the current request
        enterprise_customer_uuid (str): The uuid of the Enterprise Customer

    Returns:
        dict: Information associated with the Enterprise Catalog.

    Response will look like

        {
            'count': 2,
            'num_pages': 1,
            'current_page': 1,
            'results': [
                {
                    'enterprise_customer': '6ae013d4-c5c4-474d-8da9-0e559b2448e2',
                    'uuid': '869d26dd-2c44-487b-9b6a-24eee973f9a4',
                    'title': 'batman_catalog'
                },
                {
                    'enterprise_customer': '6ae013d4-c5c4-474d-8da9-0e559b2448e2',
                    'uuid': '1a61de70-f8e8-4e8c-a76e-01783a930ae6',
                    'title': 'new catalog'
                }
            ],
            'next': None,
            'start': 0,
            'previous': None
        }
    """
    resource = 'enterprise_catalogs'
    partner_code = site.siteconfiguration.partner.short_code
    cache_key = u'{site_domain}_{partner_code}_{resource}_{uuid}_{page}'.format(
        site_domain=site.domain,
        partner_code=partner_code,
        resource=resource,
        uuid=enterprise_customer_uuid,
        page=page,
    )
    cache_key = hashlib.md5(cache_key.encode('utf-8')).hexdigest()

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

    client = get_enterprise_api_client(site)
    endpoint = getattr(client, resource)

    try:
        response = endpoint.get(enterprise_customer=enterprise_customer_uuid,
                                page=page)
        response = update_paginated_response(endpoint_request_url, response)
    except (ConnectionError, SlumberHttpBaseException, Timeout) as exc:
        logging.exception(
            'Unable to retrieve catalogs for enterprise customer! customer: %s, Exception: %s',
            enterprise_customer_uuid, exc)
        return CUSTOMER_CATALOGS_DEFAULT_RESPONSE

    TieredCache.set_all_tiers(cache_key, response,
                              settings.ENTERPRISE_API_CACHE_TIMEOUT)

    return response
def get_course_outline(course_key: CourseKey) -> CourseOutlineData:
    """
    Get the outline of a course run.

    There is no user-specific data or permissions applied in this function.

    See the definition of CourseOutlineData for details about the data returned.
    """
    # Record the course separately from the course_id usually done in views,
    # to make sure we get useful Span information if we're invoked by things
    # like management commands, where it may iterate through many courses.
    set_custom_attribute('learning_sequences.api.course_id', str(course_key))
    course_context = _get_course_context_for_outline(course_key)

    # Check to see if it's in the cache.
    cache_key = "learning_sequences.api.get_course_outline.v1.{}.{}".format(
        course_context.learning_context.context_key,
        course_context.learning_context.published_version)
    outline_cache_result = TieredCache.get_cached_response(cache_key)
    if outline_cache_result.is_found:
        return outline_cache_result.value

    # Fetch model data, and remember that empty Sections should still be
    # represented (so query CourseSection explicitly instead of relying only on
    # select_related from CourseSectionSequence).
    section_models = CourseSection.objects \
        .filter(course_context=course_context) \
        .order_by('ordering')
    section_sequence_models = CourseSectionSequence.objects \
        .filter(course_context=course_context) \
        .order_by('ordering') \
        .select_related('sequence', 'exam')

    # Build mapping of section.id keys to sequence lists.
    sec_ids_to_sequence_list = defaultdict(list)

    for sec_seq_model in section_sequence_models:
        sequence_model = sec_seq_model.sequence

        try:
            exam_data = ExamData(
                is_practice_exam=sec_seq_model.exam.is_practice_exam,
                is_proctored_enabled=sec_seq_model.exam.is_proctored_enabled,
                is_time_limited=sec_seq_model.exam.is_time_limited)
        except CourseSequenceExam.DoesNotExist:
            exam_data = ExamData()

        sequence_data = CourseLearningSequenceData(
            usage_key=sequence_model.usage_key,
            title=sequence_model.title,
            inaccessible_after_due=sec_seq_model.inaccessible_after_due,
            visibility=VisibilityData(
                hide_from_toc=sec_seq_model.hide_from_toc,
                visible_to_staff_only=sec_seq_model.visible_to_staff_only,
            ),
            exam=exam_data)
        sec_ids_to_sequence_list[sec_seq_model.section_id].append(
            sequence_data)

    sections_data = [
        CourseSectionData(
            usage_key=section_model.usage_key,
            title=section_model.title,
            sequences=sec_ids_to_sequence_list[section_model.id],
            visibility=VisibilityData(
                hide_from_toc=section_model.hide_from_toc,
                visible_to_staff_only=section_model.visible_to_staff_only,
            )) for section_model in section_models
    ]

    outline_data = CourseOutlineData(
        course_key=course_context.learning_context.context_key,
        title=course_context.learning_context.title,
        published_at=course_context.learning_context.published_at,
        published_version=course_context.learning_context.published_version,
        days_early_for_beta=course_context.days_early_for_beta,
        entrance_exam_id=course_context.entrance_exam_id,
        sections=sections_data,
        self_paced=course_context.self_paced,
        course_visibility=CourseVisibility(course_context.course_visibility),
    )
    TieredCache.set_all_tiers(cache_key, outline_data, 300)

    return outline_data
Exemple #21
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
Exemple #22
0
 def _is_dsc_cache_found(user_id, course_id):
     consent_cache_key = get_data_consent_share_cache_key(user_id, course_id)
     data_sharing_consent_needed_cache = TieredCache.get_cached_response(consent_cache_key)
     return data_sharing_consent_needed_cache.is_found
Exemple #23
0
 def _is_dsc_cache_found(user_id, course_id):
     consent_cache_key = get_data_consent_share_cache_key(
         user_id, course_id)
     data_sharing_consent_needed_cache = TieredCache.get_cached_response(
         consent_cache_key)
     return data_sharing_consent_needed_cache.is_found
Exemple #24
0
def get_course_outline(course_key: CourseKey) -> CourseOutlineData:
    """
    Get the outline of a course run.

    There is no user-specific data or permissions applied in this function.

    See the definition of CourseOutlineData for details about the data returned.
    """
    learning_context = _get_learning_context_for_outline(course_key)

    # Check to see if it's in the cache.
    cache_key = "learning_sequences.api.get_course_outline.v1.{}.{}".format(
        learning_context.context_key, learning_context.published_version)
    outline_cache_result = TieredCache.get_cached_response(cache_key)
    if outline_cache_result.is_found:
        return outline_cache_result.value

    # Fetch model data, and remember that empty Sections should still be
    # represented (so query CourseSection explicitly instead of relying only on
    # select_related from CourseSectionSequence).
    section_models = CourseSection.objects \
        .filter(learning_context=learning_context) \
        .order_by('ordering')
    section_sequence_models = CourseSectionSequence.objects \
        .filter(learning_context=learning_context) \
        .order_by('ordering') \
        .select_related('sequence')

    # Build mapping of section.id keys to sequence lists.
    sec_ids_to_sequence_list = defaultdict(list)

    for sec_seq_model in section_sequence_models:
        sequence_model = sec_seq_model.sequence
        sequence_data = CourseLearningSequenceData(
            usage_key=sequence_model.usage_key,
            title=sequence_model.title,
            inaccessible_after_due=sec_seq_model.inaccessible_after_due,
            visibility=VisibilityData(
                hide_from_toc=sec_seq_model.hide_from_toc,
                visible_to_staff_only=sec_seq_model.visible_to_staff_only,
            ))
        sec_ids_to_sequence_list[sec_seq_model.section_id].append(
            sequence_data)

    sections_data = [
        CourseSectionData(
            usage_key=section_model.usage_key,
            title=section_model.title,
            sequences=sec_ids_to_sequence_list[section_model.id],
            visibility=VisibilityData(
                hide_from_toc=section_model.hide_from_toc,
                visible_to_staff_only=section_model.visible_to_staff_only,
            )) for section_model in section_models
    ]

    outline_data = CourseOutlineData(
        course_key=learning_context.context_key,
        title=learning_context.title,
        published_at=learning_context.published_at,
        published_version=learning_context.published_version,
        sections=sections_data,
    )
    TieredCache.set_all_tiers(cache_key, outline_data, 300)

    return outline_data
Exemple #25
0
def consent_needed_for_course(request,
                              user,
                              course_id,
                              enrollment_exists=False):
    """
    Wrap the enterprise app check to determine if the user needs to grant
    data sharing permissions before accessing a course.
    """
    LOGGER.info(
        u"Determining if user [{username}] must consent to data sharing for course [{course_id}]"
        .format(username=user.username, course_id=course_id))

    consent_cache_key = get_data_consent_share_cache_key(user.id, course_id)
    data_sharing_consent_needed_cache = TieredCache.get_cached_response(
        consent_cache_key)
    if data_sharing_consent_needed_cache.is_found and data_sharing_consent_needed_cache.value == 0:
        LOGGER.info(
            u"Consent from user [{username}] is not needed for course [{course_id}]. The DSC cache was checked,"
            u" and the value was 0.".format(username=user.username,
                                            course_id=course_id))
        return False

    consent_needed = False
    enterprise_learner_details = get_enterprise_learner_data_from_db(user)
    if not enterprise_learner_details:
        LOGGER.info(
            u"Consent from user [{username}] is not needed for course [{course_id}]. The user is not linked to an"
            u" enterprise.".format(username=user.username,
                                   course_id=course_id))
    else:
        client = ConsentApiClient(user=request.user)
        current_enterprise_uuid = enterprise_customer_uuid_for_request(request)
        consent_needed = any(
            current_enterprise_uuid == learner['enterprise_customer']['uuid']
            and Site.objects.get(
                domain=learner['enterprise_customer']['site']
                ['domain']) == request.site and client.consent_required(
                    username=user.username,
                    course_id=course_id,
                    enterprise_customer_uuid=current_enterprise_uuid,
                    enrollment_exists=enrollment_exists,
                ) for learner in enterprise_learner_details)

        if consent_needed:
            LOGGER.info(
                u"Consent from user [{username}] is needed for course [{course_id}]. The user's current enterprise"
                u" required data sharing consent, and it has not been given.".
                format(username=user.username, course_id=course_id))
        else:
            LOGGER.info(
                u"Consent from user [{username}] is not needed for course [{course_id}]. The user's current enterprise"
                u"does not require data sharing consent.".format(
                    username=user.username, course_id=course_id))

    if not consent_needed:
        # Set an ephemeral item in the cache to prevent us from needing
        # to make a Consent API request every time this function is called.
        TieredCache.set_all_tiers(consent_cache_key, 0,
                                  settings.DATA_CONSENT_SHARE_CACHE_TIMEOUT)

    return consent_needed
Exemple #26
0
    def record_user_activity(cls,
                             user,
                             course_key,
                             request=None,
                             only_if_mobile_app=False):
        '''
        Update the user activity table with a record for this activity.

        Since we store one activity per date, we don't need to query the database
        for every activity on a given date.
        To avoid unnecessary queries, we store a record in a cache once we have an activity for the date,
        which times out at the end of that date (in the user's timezone).

        The request argument is only used to check if the request is coming from a mobile app.
        Once the only_if_mobile_app argument is removed the request argument can be removed as well.

        The return value is the id of the object that was created, or retrieved.
        A return value of None signifies that a user activity record was not stored or retrieved
        '''
        if not ENABLE_COURSE_GOALS.is_enabled(course_key):
            return None

        if not (user and user.id) or not course_key:
            return None

        if only_if_mobile_app and request and not is_request_from_mobile_app(
                request):
            return None

        if is_masquerading(user, course_key):
            return None

        timezone = get_user_timezone_or_last_seen_timezone_or_utc(user)
        now = datetime.now(timezone)
        date = now.date()

        cache_key = 'goals_user_activity_{}_{}_{}'.format(
            str(user.id), str(course_key), str(date))

        cached_value = TieredCache.get_cached_response(cache_key)
        if cached_value.is_found:
            return cached_value.value, False

        activity_object, __ = cls.objects.get_or_create(user=user,
                                                        course_key=course_key,
                                                        date=date)

        # Cache result until the end of the day to avoid unnecessary database requests
        tomorrow = now + timedelta(days=1)
        midnight = datetime(year=tomorrow.year,
                            month=tomorrow.month,
                            day=tomorrow.day,
                            hour=0,
                            minute=0,
                            second=0,
                            tzinfo=timezone)
        seconds_until_midnight = (midnight - now).seconds

        TieredCache.set_all_tiers(cache_key, activity_object.id,
                                  seconds_until_midnight)
        # Temporary debugging log for testing mobile app connection
        if request:
            log.info(
                'Set cached value with request {} for user and course combination {} {}'
                .format(str(request.build_absolute_uri()), str(user.id),
                        str(course_key)))
        return activity_object.id
Exemple #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)
Exemple #28
0
def consent_needed_for_course(request,
                              user,
                              course_id,
                              enrollment_exists=False):
    """
    Wrap the enterprise app check to determine if the user needs to grant
    data sharing permissions before accessing a course.
    """
    LOGGER.info(
        u"Determining if user [{username}] must consent to data sharing for course [{course_id}]"
        .format(username=user.username, course_id=course_id))

    consent_cache_key = get_data_consent_share_cache_key(user.id, course_id)
    data_sharing_consent_needed_cache = TieredCache.get_cached_response(
        consent_cache_key)
    if data_sharing_consent_needed_cache.is_found and data_sharing_consent_needed_cache.value == 0:
        LOGGER.info(
            u"Consent from user [{username}] is not needed for course [{course_id}]. The DSC cache was checked,"
            u" and the value was 0.".format(username=user.username,
                                            course_id=course_id))
        return False

    consent_needed = False
    enterprise_learner_details = get_enterprise_learner_data_from_db(user)
    if not enterprise_learner_details:
        LOGGER.info(
            u"Consent from user [{username}] is not needed for course [{course_id}]. The user is not linked to an"
            u" enterprise.".format(username=user.username,
                                   course_id=course_id))
    else:
        client = ConsentApiClient(user=request.user)
        current_enterprise_uuid = enterprise_customer_uuid_for_request(request)
        consent_needed = any(
            str(current_enterprise_uuid) == str(
                learner['enterprise_customer']['uuid']) and Site.objects.get(
                    domain=learner['enterprise_customer']['site']['domain']) ==
            request.site and client.consent_required(
                username=user.username,
                course_id=course_id,
                enterprise_customer_uuid=current_enterprise_uuid,
                enrollment_exists=enrollment_exists,
            ) for learner in enterprise_learner_details)

        if not consent_needed:
            # TODO: https://openedx.atlassian.net/browse/ENT-3724
            # this whole code branch seems to do nothing other than log some misleading info:
            # the consent requirement doesn't actually fail.  If there's an enterprise or site mismatch,
            # we'll still end up in the else branch of "if consent_needed:" below, where
            # we'll log that consent is not needed, and ultimately, return False.
            # Are we supposed to raise some exceptions in here?
            enterprises = [
                str(learner['enterprise_customer']['uuid'])
                for learner in enterprise_learner_details
            ]

            if str(current_enterprise_uuid) not in enterprises:
                LOGGER.info(  # pragma: no cover
                    '[ENTERPRISE DSC] Enterprise mismatch. USER: [%s], CurrentEnterprise: [%s], UserEnterprises: [%s]',
                    user.username, current_enterprise_uuid, enterprises)
            else:
                domains = [
                    learner['enterprise_customer']['site']['domain']
                    for learner in enterprise_learner_details
                ]
                if not Site.objects.filter(domain__in=domains).filter(
                        id=request.site.id).exists():
                    LOGGER.info(  # pragma: no cover
                        '[ENTERPRISE DSC] Site mismatch. USER: [%s], RequestSite: [%s], LearnerEnterpriseDomains: [%s]',
                        user.username, request.site, domains)

        if consent_needed:
            LOGGER.info(
                u"Consent from user [{username}] is needed for course [{course_id}]. The user's current enterprise"
                u" required data sharing consent, and it has not been given.".
                format(username=user.username, course_id=course_id))
        else:
            LOGGER.info(
                u"Consent from user [{username}] is not needed for course [{course_id}]. The user's current enterprise "
                u"does not require data sharing consent.".format(
                    username=user.username, course_id=course_id))

    if not consent_needed:
        # Set an ephemeral item in the cache to prevent us from needing
        # to make a Consent API request every time this function is called.
        TieredCache.set_all_tiers(consent_cache_key, 0,
                                  settings.DATA_CONSENT_SHARE_CACHE_TIMEOUT)

    return consent_needed
Exemple #29
0
def get_catalog_course_runs(site, query, limit=None, offset=None):
    """
    Get course runs for a site on the basis of provided query from the Course
    Catalog API.

    This method will get all course runs by recursively retrieving API
    next urls in the API response if no limit is provided.

    Arguments:
        limit (int): Number of results per page
        offset (int): Page offset
        query (str): ElasticSearch Query
        site (Site): Site object containing Site Configuration data

    Example:
        >>> get_catalog_course_runs(site, query, limit=1)
        {
            "count": 1,
            "next": "None",
            "previous": "None",
            "results": [{
                "key": "course-v1:edX+DemoX+Demo_Course",
                "title": edX Demonstration Course,
                "start": "2016-05-01T00:00:00Z",
                "image": {
                    "src": "path/to/the/course/image"
                },
                "enrollment_end": None
            }],
        }
    Returns:
        dict: Query search results for course runs received from Course
            Catalog API

    Raises:
        HTTPError: requests HTTPError.
    """
    api_resource_name = 'course_runs'
    partner_code = site.siteconfiguration.partner.short_code
    cache_key = u'{site_domain}_{partner_code}_{resource}_{query}_{limit}_{offset}'.format(
        site_domain=site.domain,
        partner_code=partner_code,
        resource=api_resource_name,
        query=query,
        limit=limit,
        offset=offset)
    cache_key = hashlib.md5(cache_key.encode('utf-8')).hexdigest()

    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_name}/")
    response = api_client.get(api_url,
                              params={
                                  "partner": partner_code,
                                  "q": query,
                                  "limit": limit,
                                  "offset": offset
                              })
    response.raise_for_status()

    results = response.json()

    TieredCache.set_all_tiers(cache_key, results,
                              settings.COURSES_API_CACHE_TIMEOUT)
    return results
    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
                }
        """
        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)))

        # 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_found:
                return Response(cached_response.value)

        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)
Exemple #31
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
                }

         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
        use_default_basket_case = 0

        # 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
                    use_default_basket_case = 1
            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
            use_default_basket_case = 2

        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 2: request [%s] referrer [%s] url [%s] Cache key [%s] response [%s]'
                'skus [%s] case [%s] basket_owner [%s]',
                str(request),
                str(request.META.get('HTTP_REFERER')),
                str(request._request),
                str(cache_key),  # pylint: disable=protected-access
                str(cached_response),
                str(skus),
                str(use_default_basket_case),
                str(basket_owner))
            if cached_response.is_found:
                return Response(cached_response.value)

        response = self._calculate_temporary_basket_atomic(
            basket_owner, request, products, voucher, skus, code)
        if response and use_default_basket:
            logger.info(
                'bundle debugging 3: request [%s] referrer [%s] url [%s] Cache key [%s] response [%s]'
                'skus [%s] case [%s] basket_owner [%s]',
                str(request),
                str(request.META.get('HTTP_REFERER')),
                str(request._request),
                str(cache_key),  # pylint: disable=protected-access
                str(response),
                str(skus),
                str(use_default_basket_case),
                str(basket_owner))
            TieredCache.set_all_tiers(
                cache_key, response,
                settings.ANONYMOUS_BASKET_CALCULATE_CACHE_TIMEOUT)

        return Response(response)
def get_catalog_course_runs(site, query, limit=None, offset=None):
    """
    Get course runs for a site on the basis of provided query from the Course
    Catalog API.

    This method will get all course runs by recursively retrieving API
    next urls in the API response if no limit is provided.

    Arguments:
        limit (int): Number of results per page
        offset (int): Page offset
        query (str): ElasticSearch Query
        site (Site): Site object containing Site Configuration data

    Example:
        >>> get_catalog_course_runs(site, query, limit=1)
        {
            "count": 1,
            "next": "None",
            "previous": "None",
            "results": [{
                "key": "course-v1:edX+DemoX+Demo_Course",
                "title": edX Demonstration Course,
                "start": "2016-05-01T00:00:00Z",
                "image": {
                    "src": "path/to/the/course/image"
                },
                "enrollment_end": None
            }],
        }
    Returns:
        dict: Query search results for course runs received from Course
            Catalog API

    Raises:
        ConnectionError: requests exception "ConnectionError"
        SlumberBaseException: slumber exception "SlumberBaseException"
        Timeout: requests exception "Timeout"

    """
    api_resource_name = 'course_runs'
    partner_code = site.siteconfiguration.partner.short_code
    cache_key = '{site_domain}_{partner_code}_{resource}_{query}_{limit}_{offset}'.format(
        site_domain=site.domain,
        partner_code=partner_code,
        resource=api_resource_name,
        query=query,
        limit=limit,
        offset=offset
    )
    cache_key = hashlib.md5(cache_key).hexdigest()

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

    response = endpoint().get(
        partner=partner_code,
        q=query,
        limit=limit,
        offset=offset
    )
    TieredCache.set_all_tiers(cache_key, response, settings.COURSES_API_CACHE_TIMEOUT)
    return response