Commit 2383f489 by Robert Raposa Committed by Jason Myatt

Reuse TieredCache.

LEARNER-5182
parent da832628
...@@ -7,7 +7,6 @@ from dateutil.parser import parse ...@@ -7,7 +7,6 @@ from dateutil.parser import parse
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.core.cache import cache
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.utils.functional import cached_property from django.utils.functional import cached_property
...@@ -19,6 +18,7 @@ from requests.exceptions import ConnectionError, Timeout ...@@ -19,6 +18,7 @@ from requests.exceptions import ConnectionError, Timeout
from slumber.exceptions import HttpNotFoundError, SlumberBaseException from slumber.exceptions import HttpNotFoundError, SlumberBaseException
from analytics import Client as SegmentClient from analytics import Client as SegmentClient
from ecommerce.cache_utils.utils import TieredCache
from ecommerce.core.url_utils import get_lms_url from ecommerce.core.url_utils import get_lms_url
from ecommerce.core.utils import log_message_and_raise_validation_error from ecommerce.core.utils import log_message_and_raise_validation_error
from ecommerce.extensions.payment.exceptions import ProcessorNotFoundError from ecommerce.extensions.payment.exceptions import ProcessorNotFoundError
...@@ -365,21 +365,20 @@ class SiteConfiguration(models.Model): ...@@ -365,21 +365,20 @@ class SiteConfiguration(models.Model):
str: JWT access token str: JWT access token
""" """
key = 'siteconfiguration_access_token_{}'.format(self.id) key = 'siteconfiguration_access_token_{}'.format(self.id)
access_token = cache.get(key) access_token_cached_response = TieredCache.get_cached_response(key)
if access_token_cached_response.is_hit:
# pylint: disable=unsubscriptable-object return access_token_cached_response.value
if not access_token:
url = '{root}/access_token'.format(root=self.oauth2_provider_url) url = '{root}/access_token'.format(root=self.oauth2_provider_url)
access_token, expiration_datetime = EdxRestApiClient.get_oauth_access_token( access_token, expiration_datetime = EdxRestApiClient.get_oauth_access_token(
url, url,
self.oauth_settings['SOCIAL_AUTH_EDX_OIDC_KEY'], self.oauth_settings['SOCIAL_AUTH_EDX_OIDC_KEY'], # pylint: disable=unsubscriptable-object
self.oauth_settings['SOCIAL_AUTH_EDX_OIDC_SECRET'], self.oauth_settings['SOCIAL_AUTH_EDX_OIDC_SECRET'], # pylint: disable=unsubscriptable-object
token_type='jwt' token_type='jwt'
) )
expires = (expiration_datetime - datetime.datetime.utcnow()).seconds expires = (expiration_datetime - datetime.datetime.utcnow()).seconds
cache.set(key, access_token, expires) TieredCache.set_all_tiers(key, access_token, expires)
return access_token return access_token
@cached_property @cached_property
...@@ -542,18 +541,20 @@ class User(AbstractUser): ...@@ -542,18 +541,20 @@ class User(AbstractUser):
try: try:
cache_key = 'verification_status_{username}'.format(username=self.username) cache_key = 'verification_status_{username}'.format(username=self.username)
cache_key = hashlib.md5(cache_key).hexdigest() cache_key = hashlib.md5(cache_key).hexdigest()
verification = cache.get(cache_key) verification_cached_response = TieredCache.get_cached_response(cache_key)
if not verification: if verification_cached_response.is_hit:
api = EdxRestApiClient( return verification_cached_response.value
site.siteconfiguration.build_lms_url('api/user/v1/'),
oauth_access_token=self.access_token api = EdxRestApiClient(
) site.siteconfiguration.build_lms_url('api/user/v1/'),
response = api.accounts(self.username).verification_status().get() oauth_access_token=self.access_token
)
response = api.accounts(self.username).verification_status().get()
verification = response.get('is_verified', False) verification = response.get('is_verified', False)
if verification: if verification:
cache_timeout = int((parse(response.get('expiration_datetime')) - now()).total_seconds()) cache_timeout = int((parse(response.get('expiration_datetime')) - now()).total_seconds())
cache.set(cache_key, verification, cache_timeout) TieredCache.set_all_tiers(cache_key, verification, cache_timeout)
return verification return verification
except HttpNotFoundError: except HttpNotFoundError:
log.debug('No verification data found for [%s]', self.username) log.debug('No verification data found for [%s]', self.username)
...@@ -564,7 +565,7 @@ class User(AbstractUser): ...@@ -564,7 +565,7 @@ class User(AbstractUser):
return False return False
def deactivate_account(self, site_configuration): def deactivate_account(self, site_configuration):
"""Deactive the user's account. """Deactivate the user's account.
Args: Args:
site_configuration (SiteConfiguration): The site configuration site_configuration (SiteConfiguration): The site configuration
......
...@@ -2,11 +2,11 @@ import datetime ...@@ -2,11 +2,11 @@ import datetime
import json import json
import httpretty import httpretty
from django.core.cache import cache
from django.test import RequestFactory from django.test import RequestFactory
from oscar.core.utils import slugify from oscar.core.utils import slugify
from oscar.test import factories from oscar.test import factories
from ecommerce.cache_utils.utils import TieredCache
from ecommerce.core.constants import COUPON_PRODUCT_CLASS_NAME from ecommerce.core.constants import COUPON_PRODUCT_CLASS_NAME
from ecommerce.core.models import BusinessClient from ecommerce.core.models import BusinessClient
from ecommerce.extensions.api.v2.views.coupons import CouponViewSet from ecommerce.extensions.api.v2.views.coupons import CouponViewSet
...@@ -20,7 +20,7 @@ class DiscoveryMockMixin(object): ...@@ -20,7 +20,7 @@ class DiscoveryMockMixin(object):
""" Mocks for the Discovery service response. """ """ Mocks for the Discovery service response. """
def setUp(self): def setUp(self):
super(DiscoveryMockMixin, self).setUp() super(DiscoveryMockMixin, self).setUp()
cache.clear() TieredCache.clear_all_tiers()
@staticmethod @staticmethod
def build_discovery_catalogs_url(discovery_api_url, catalog_id=''): def build_discovery_catalogs_url(discovery_api_url, catalog_id=''):
......
import ddt import ddt
import httpretty
from mock import patch
from oscar.test.factories import ProductFactory, RangeFactory, VoucherFactory from oscar.test.factories import ProductFactory, RangeFactory, VoucherFactory
from ecommerce.coupons.utils import is_voucher_applied, prepare_course_seat_types from ecommerce.cache_utils.utils import TieredCache
from ecommerce.coupons.tests.mixins import DiscoveryMockMixin
from ecommerce.coupons.utils import fetch_course_catalog, is_voucher_applied, prepare_course_seat_types
from ecommerce.extensions.basket.utils import prepare_basket from ecommerce.extensions.basket.utils import prepare_basket
from ecommerce.extensions.test.factories import prepare_voucher from ecommerce.extensions.test.factories import prepare_voucher
from ecommerce.tests.testcases import TestCase from ecommerce.tests.testcases import TestCase
@ddt.ddt @ddt.ddt
class CouponAppViewTests(TestCase): @httpretty.activate
class CouponUtilsTests(TestCase, DiscoveryMockMixin):
def setUp(self): def setUp(self):
""" """
Setup variables for test cases. Setup variables for test cases.
""" """
super(CouponAppViewTests, self).setUp() super(CouponUtilsTests, self).setUp()
self.user = self.create_user(email='test@tester.fake') self.user = self.create_user(email='test@tester.fake')
self.request.user = self.user self.request.user = self.user
...@@ -45,3 +50,23 @@ class CouponAppViewTests(TestCase): ...@@ -45,3 +50,23 @@ class CouponAppViewTests(TestCase):
# Verify is_voucher_applied returns False when voucher can not be applied to the basket. # Verify is_voucher_applied returns False when voucher can not be applied to the basket.
self.assertFalse(is_voucher_applied(basket, VoucherFactory())) self.assertFalse(is_voucher_applied(basket, VoucherFactory()))
def test_fetch_course_catalog(self):
"""
Verify that fetch_course_catalog is cached
We expect 2 calls to set_all_tiers due to:
- the site_configuration api setup
- the result being cached
"""
self.mock_access_token_response()
self.mock_catalog_detail_endpoint(self.site_configuration.discovery_api_url)
with patch.object(TieredCache, 'set_all_tiers', wraps=TieredCache.set_all_tiers) as mocked_set_all_tiers:
mocked_set_all_tiers.assert_not_called()
_ = fetch_course_catalog(self.site, 1)
self.assertEqual(mocked_set_all_tiers.call_count, 2)
_ = fetch_course_catalog(self.site, 1)
self.assertEqual(mocked_set_all_tiers.call_count, 2)
...@@ -3,10 +3,10 @@ import hashlib ...@@ -3,10 +3,10 @@ import hashlib
import logging import logging
from django.conf import settings from django.conf import settings
from django.core.cache import cache
from oscar.core.loading import get_model from oscar.core.loading import get_model
from slumber.exceptions import HttpNotFoundError from slumber.exceptions import HttpNotFoundError
from ecommerce.cache_utils.utils import TieredCache
from ecommerce.core.utils import get_cache_key from ecommerce.core.utils import get_cache_key
Product = get_model('catalogue', 'Product') Product = get_model('catalogue', 'Product')
...@@ -66,18 +66,20 @@ def get_catalog_course_runs(site, query, limit=None, offset=None): ...@@ -66,18 +66,20 @@ def get_catalog_course_runs(site, query, limit=None, offset=None):
) )
cache_key = hashlib.md5(cache_key).hexdigest() cache_key = hashlib.md5(cache_key).hexdigest()
response = cache.get(cache_key) cached_response = TieredCache.get_cached_response(cache_key)
if not response: if cached_response.is_hit:
api = site.siteconfiguration.discovery_api_client return cached_response.value
endpoint = getattr(api, api_resource_name)
response = endpoint().get(
partner=partner_code,
q=query,
limit=limit,
offset=offset
)
cache.set(cache_key, response, settings.COURSES_API_CACHE_TIMEOUT)
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 return response
...@@ -135,18 +137,20 @@ def fetch_course_catalog(site, catalog_id): ...@@ -135,18 +137,20 @@ def fetch_course_catalog(site, catalog_id):
catalog_id=catalog_id, catalog_id=catalog_id,
) )
response = cache.get(cache_key) cached_response = TieredCache.get_cached_response(cache_key)
if not response: if cached_response.is_hit:
api = site.siteconfiguration.discovery_api_client return cached_response.value
endpoint = getattr(api, api_resource)
api = site.siteconfiguration.discovery_api_client
endpoint = getattr(api, api_resource)
try: try:
response = endpoint(catalog_id).get() response = endpoint(catalog_id).get()
except HttpNotFoundError: except HttpNotFoundError:
logger.exception("Catalog '%s' not found.", catalog_id) logger.exception("Catalog '%s' not found.", catalog_id)
raise raise
cache.set(cache_key, response, settings.COURSES_API_CACHE_TIMEOUT) TieredCache.set_all_tiers(cache_key, response, settings.COURSES_API_CACHE_TIMEOUT)
return response return response
......
...@@ -2,10 +2,11 @@ import hashlib ...@@ -2,10 +2,11 @@ import hashlib
import ddt import ddt
import httpretty import httpretty
from django.core.cache import cache from mock import patch
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from requests.exceptions import ConnectionError from requests.exceptions import ConnectionError
from ecommerce.cache_utils.utils import TieredCache
from ecommerce.coupons.tests.mixins import DiscoveryMockMixin from ecommerce.coupons.tests.mixins import DiscoveryMockMixin
from ecommerce.courses.tests.factories import CourseFactory from ecommerce.courses.tests.factories import CourseFactory
from ecommerce.courses.utils import ( from ecommerce.courses.utils import (
...@@ -63,8 +64,8 @@ class UtilsTests(DiscoveryTestMixin, DiscoveryMockMixin, TestCase): ...@@ -63,8 +64,8 @@ class UtilsTests(DiscoveryTestMixin, DiscoveryMockMixin, TestCase):
cache_key = 'courses_api_detail_{}{}'.format(key, self.site.siteconfiguration.partner.short_code) cache_key = 'courses_api_detail_{}{}'.format(key, self.site.siteconfiguration.partner.short_code)
cache_key = hashlib.md5(cache_key).hexdigest() cache_key = hashlib.md5(cache_key).hexdigest()
cached_course = cache.get(cache_key) course_cached_response = TieredCache.get_cached_response(cache_key)
self.assertIsNone(cached_course) self.assertTrue(course_cached_response.is_miss)
response = get_course_info_from_catalog(self.request.site, product) response = get_course_info_from_catalog(self.request.site, product)
...@@ -73,8 +74,31 @@ class UtilsTests(DiscoveryTestMixin, DiscoveryMockMixin, TestCase): ...@@ -73,8 +74,31 @@ class UtilsTests(DiscoveryTestMixin, DiscoveryMockMixin, TestCase):
else: else:
self.assertEqual(response['title'], product.title) self.assertEqual(response['title'], product.title)
cached_course = cache.get(cache_key) course_cached_response = TieredCache.get_cached_response(cache_key)
self.assertEqual(cached_course, response) self.assertEqual(course_cached_response.value, response)
def test_get_course_info_from_catalog_cached(self):
"""
Verify that get_course_info_from_catalog is cached
We expect 2 calls to set_all_tiers in the get_course_info_from_catalog
method due to:
- the site_configuration api setup
- the result being cached
"""
self.mock_access_token_response()
product = create_or_update_course_entitlement(
'verified', 100, self.partner, 'foo-bar', 'Foo Bar Entitlement')
self.mock_course_detail_endpoint(product, discovery_api_url=self.site_configuration.discovery_api_url)
with patch.object(TieredCache, 'set_all_tiers', wraps=TieredCache.set_all_tiers) as mocked_set_all_tiers:
mocked_set_all_tiers.assert_not_called()
_ = get_course_info_from_catalog(self.request.site, product)
self.assertEqual(mocked_set_all_tiers.call_count, 2)
_ = get_course_info_from_catalog(self.request.site, product)
self.assertEqual(mocked_set_all_tiers.call_count, 2)
@ddt.data( @ddt.data(
('honor', 'Honor'), ('honor', 'Honor'),
...@@ -113,8 +137,8 @@ class GetCourseCatalogUtilTests(DiscoveryMockMixin, TestCase): ...@@ -113,8 +137,8 @@ class GetCourseCatalogUtilTests(DiscoveryMockMixin, TestCase):
""" """
cache_key = '{}.catalog.api.data'.format(self.request.site.domain) cache_key = '{}.catalog.api.data'.format(self.request.site.domain)
cache_key = hashlib.md5(cache_key).hexdigest() cache_key = hashlib.md5(cache_key).hexdigest()
cached_course_catalogs = cache.get(cache_key) course_catalogs_cached_response = TieredCache.get_cached_response(cache_key)
self.assertIsNone(cached_course_catalogs) self.assertTrue(course_catalogs_cached_response.is_miss)
response = get_course_catalogs(self.request.site) response = get_course_catalogs(self.request.site)
...@@ -122,8 +146,8 @@ class GetCourseCatalogUtilTests(DiscoveryMockMixin, TestCase): ...@@ -122,8 +146,8 @@ class GetCourseCatalogUtilTests(DiscoveryMockMixin, TestCase):
for catalog_index, catalog in enumerate(response): for catalog_index, catalog in enumerate(response):
self.assertEqual(catalog['name'], catalog_name_list[catalog_index]) self.assertEqual(catalog['name'], catalog_name_list[catalog_index])
cached_course = cache.get(cache_key) course_cached_response = TieredCache.get_cached_response(cache_key)
self.assertEqual(cached_course, response) self.assertEqual(course_cached_response.value, response)
def test_get_course_catalogs_for_single_catalog_with_id(self): def test_get_course_catalogs_for_single_catalog_with_id(self):
""" """
...@@ -136,14 +160,14 @@ class GetCourseCatalogUtilTests(DiscoveryMockMixin, TestCase): ...@@ -136,14 +160,14 @@ class GetCourseCatalogUtilTests(DiscoveryMockMixin, TestCase):
catalog_id = 1 catalog_id = 1
cache_key = '{}.catalog.api.data.{}'.format(self.request.site.domain, catalog_id) cache_key = '{}.catalog.api.data.{}'.format(self.request.site.domain, catalog_id)
cache_key = hashlib.md5(cache_key).hexdigest() cache_key = hashlib.md5(cache_key).hexdigest()
cached_course_catalog = cache.get(cache_key) course_catalogs_cached_response = TieredCache.get_cached_response(cache_key)
self.assertIsNone(cached_course_catalog) self.assertTrue(course_catalogs_cached_response.is_miss)
response = get_course_catalogs(self.request.site, catalog_id) response = get_course_catalogs(self.request.site, catalog_id)
self.assertEqual(response['name'], 'All Courses') self.assertEqual(response['name'], 'All Courses')
cached_course = cache.get(cache_key) course_cached_response = TieredCache.get_cached_response(cache_key)
self.assertEqual(cached_course, response) self.assertEqual(course_cached_response.value, response)
# Verify the API was actually hit (not the cache) # Verify the API was actually hit (not the cache)
self._assert_num_requests(2) self._assert_num_requests(2)
......
...@@ -4,8 +4,10 @@ import ddt ...@@ -4,8 +4,10 @@ import ddt
import httpretty import httpretty
from django.conf import settings from django.conf import settings
from django.urls import reverse from django.urls import reverse
from mock import patch
from testfixtures import LogCapture from testfixtures import LogCapture
from ecommerce.cache_utils.utils import TieredCache
from ecommerce.core.url_utils import get_lms_url from ecommerce.core.url_utils import get_lms_url
from ecommerce.tests.testcases import TestCase from ecommerce.tests.testcases import TestCase
...@@ -129,6 +131,26 @@ class CourseAppViewTests(TestCase): ...@@ -129,6 +131,26 @@ class CourseAppViewTests(TestCase):
self.assertEqual(response.context['credit_providers'], provider_json) self.assertEqual(response.context['credit_providers'], provider_json)
@httpretty.activate @httpretty.activate
def test_credit_providers_in_context_cached(self):
""" Verify the cached context data includes a list of credit providers. """
self._create_and_login_staff_user()
__, provider_json = self.mock_credit_api_providers()
with patch.object(TieredCache, 'set_all_tiers', wraps=TieredCache.set_all_tiers) as mocked_set_all_tiers:
mocked_set_all_tiers.assert_not_called()
response = self.client.get(self.path)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context['credit_providers'], provider_json)
self.assertEqual(mocked_set_all_tiers.call_count, 1)
response = self.client.get(self.path)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context['credit_providers'], provider_json)
self.assertEqual(mocked_set_all_tiers.call_count, 1)
@httpretty.activate
def test_credit_api_failure(self): def test_credit_api_failure(self):
""" Verify the view logs an error if it fails to retrieve credit providers. """ """ Verify the view logs an error if it fails to retrieve credit providers. """
# Setup staff user with an OAuth 2 access token # Setup staff user with an OAuth 2 access token
......
import hashlib import hashlib
from django.conf import settings from django.conf import settings
from django.core.cache import cache
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from ecommerce.cache_utils.utils import TieredCache
from ecommerce.core.utils import deprecated_traverse_pagination from ecommerce.core.utils import deprecated_traverse_pagination
...@@ -32,15 +32,19 @@ def get_course_info_from_catalog(site, product): ...@@ -32,15 +32,19 @@ def get_course_info_from_catalog(site, product):
api = site.siteconfiguration.discovery_api_client api = site.siteconfiguration.discovery_api_client
partner_short_code = site.siteconfiguration.partner.short_code partner_short_code = site.siteconfiguration.partner.short_code
cache_key = 'courses_api_detail_{}{}'.format(key, partner_short_code) cache_key = 'courses_api_detail_{}{}'.format(key, partner_short_code)
cache_key = hashlib.md5(cache_key).hexdigest() cache_key = hashlib.md5(cache_key).hexdigest()
course = cache.get(cache_key) course_cached_response = TieredCache.get_cached_response(cache_key)
if not course: # pragma: no cover if course_cached_response.is_hit:
if product.is_course_entitlement_product: return course_cached_response.value
course = api.courses(key).get()
else: if product.is_course_entitlement_product:
course = api.course_runs(key).get(partner=partner_short_code) course = api.courses(key).get()
cache.set(cache_key, course, settings.COURSES_API_CACHE_TIMEOUT) 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 return course
...@@ -66,9 +70,10 @@ def get_course_catalogs(site, resource_id=None): ...@@ -66,9 +70,10 @@ def get_course_catalogs(site, resource_id=None):
cache_key = '{}.{}'.format(base_cache_key, resource_id) if resource_id else base_cache_key cache_key = '{}.{}'.format(base_cache_key, resource_id) if resource_id else base_cache_key
cache_key = hashlib.md5(cache_key).hexdigest() cache_key = hashlib.md5(cache_key).hexdigest()
cached = cache.get(cache_key)
if cached: cached_response = TieredCache.get_cached_response(cache_key)
return cached if cached_response.is_hit:
return cached_response.value
api = site.siteconfiguration.discovery_api_client api = site.siteconfiguration.discovery_api_client
endpoint = getattr(api, resource) endpoint = getattr(api, resource)
...@@ -79,7 +84,7 @@ def get_course_catalogs(site, resource_id=None): ...@@ -79,7 +84,7 @@ def get_course_catalogs(site, resource_id=None):
else: else:
results = deprecated_traverse_pagination(response, endpoint) results = deprecated_traverse_pagination(response, endpoint)
cache.set(cache_key, results, settings.COURSES_API_CACHE_TIMEOUT) TieredCache.set_all_tiers(cache_key, results, settings.COURSES_API_CACHE_TIMEOUT)
return results return results
......
...@@ -4,7 +4,6 @@ import os ...@@ -4,7 +4,6 @@ import os
from io import StringIO from io import StringIO
from django.conf import settings from django.conf import settings
from django.core.cache import cache
from django.core.management import call_command from django.core.management import call_command
from django.http import Http404, HttpResponse from django.http import Http404, HttpResponse
from django.views.generic import TemplateView, View from django.views.generic import TemplateView, View
...@@ -12,6 +11,7 @@ from edx_rest_api_client.client import EdxRestApiClient ...@@ -12,6 +11,7 @@ from edx_rest_api_client.client import EdxRestApiClient
from requests import Timeout from requests import Timeout
from slumber.exceptions import SlumberBaseException from slumber.exceptions import SlumberBaseException
from ecommerce.cache_utils.utils import TieredCache
from ecommerce.core.url_utils import get_lms_url from ecommerce.core.url_utils import get_lms_url
from ecommerce.core.views import StaffOnlyMixin from ecommerce.core.views import StaffOnlyMixin
from ecommerce.extensions.partner.shortcuts import get_partner_for_site from ecommerce.extensions.partner.shortcuts import get_partner_for_site
...@@ -42,22 +42,23 @@ class CourseAppView(StaffOnlyMixin, TemplateView): ...@@ -42,22 +42,23 @@ class CourseAppView(StaffOnlyMixin, TemplateView):
Results will be sorted alphabetically by display name. Results will be sorted alphabetically by display name.
""" """
key = 'credit_providers' key = 'credit_providers'
credit_providers = cache.get(key, []) credit_providers_cache_response = TieredCache.get_cached_response(key)
if credit_providers_cache_response.is_hit:
if not credit_providers: return credit_providers_cache_response.value
try:
credit_api = EdxRestApiClient(
get_lms_url('/api/credit/v1/'),
oauth_access_token=self.request.user.access_token
)
credit_providers = credit_api.providers.get()
credit_providers.sort(key=lambda provider: provider['display_name'])
# Update the cache
cache.set(key, credit_providers, settings.CREDIT_PROVIDER_CACHE_TIMEOUT)
except (SlumberBaseException, Timeout):
logger.exception('Failed to retrieve credit providers!')
try:
credit_api = EdxRestApiClient(
get_lms_url('/api/credit/v1/'),
oauth_access_token=self.request.user.access_token
)
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 return credit_providers
......
...@@ -5,10 +5,10 @@ import logging ...@@ -5,10 +5,10 @@ import logging
from urllib import urlencode from urllib import urlencode
from django.conf import settings from django.conf import settings
from django.core.cache import cache
from requests.exceptions import ConnectionError, Timeout from requests.exceptions import ConnectionError, Timeout
from slumber.exceptions import SlumberHttpBaseException from slumber.exceptions import SlumberHttpBaseException
from ecommerce.cache_utils.utils import TieredCache
from ecommerce.core.utils import get_cache_key from ecommerce.core.utils import get_cache_key
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -56,12 +56,14 @@ def fetch_enterprise_learner_entitlements(site, learner_id): ...@@ -56,12 +56,14 @@ def fetch_enterprise_learner_entitlements(site, learner_id):
learner_id=learner_id learner_id=learner_id
) )
entitlements = cache.get(cache_key) entitlements_cached_response = TieredCache.get_cached_response(cache_key)
if not entitlements: if entitlements_cached_response.is_hit:
api = site.siteconfiguration.enterprise_api_client return entitlements_cached_response.value
entitlements = getattr(api, resource_url).get()
cache.set(cache_key, entitlements, settings.ENTERPRISE_API_CACHE_TIMEOUT)
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 return entitlements
...@@ -153,14 +155,16 @@ def fetch_enterprise_learner_data(site, user): ...@@ -153,14 +155,16 @@ def fetch_enterprise_learner_data(site, user):
username=user.username username=user.username
) )
response = cache.get(cache_key) cached_response = TieredCache.get_cached_response(cache_key)
if not response: if cached_response.is_hit:
api = site.siteconfiguration.enterprise_api_client return cached_response.value
endpoint = getattr(api, api_resource_name)
querystring = {'username': user.username} api = site.siteconfiguration.enterprise_api_client
response = endpoint().get(**querystring) endpoint = getattr(api, api_resource_name)
cache.set(cache_key, response, settings.ENTERPRISE_API_CACHE_TIMEOUT) querystring = {'username': user.username}
response = endpoint().get(**querystring)
TieredCache.set_all_tiers(cache_key, response, settings.ENTERPRISE_API_CACHE_TIMEOUT)
return response return response
...@@ -184,22 +188,24 @@ def catalog_contains_course_runs(site, course_run_ids, enterprise_customer_uuid, ...@@ -184,22 +188,24 @@ def catalog_contains_course_runs(site, course_run_ids, enterprise_customer_uuid,
query_params=urlencode(query_params, True) query_params=urlencode(query_params, True)
) )
contains_content = cache.get(cache_key) contains_content_cached_response = TieredCache.get_cached_response(cache_key)
if contains_content is None: if contains_content_cached_response.is_hit:
api = site.siteconfiguration.enterprise_api_client return contains_content_cached_response.value
endpoint = getattr(api, api_resource_name)(api_resource_id)
try: api = site.siteconfiguration.enterprise_api_client
contains_content = endpoint.contains_content_items.get(**query_params)['contains_content_items'] endpoint = getattr(api, api_resource_name)(api_resource_id)
cache.set(cache_key, contains_content, settings.ENTERPRISE_API_CACHE_TIMEOUT) try:
except (ConnectionError, KeyError, SlumberHttpBaseException, Timeout): contains_content = endpoint.contains_content_items.get(**query_params)['contains_content_items']
logger.exception(
'Failed to check if course_runs [%s] exist in ' TieredCache.set_all_tiers(cache_key, contains_content, settings.ENTERPRISE_API_CACHE_TIMEOUT)
'EnterpriseCustomerCatalog [%s]' except (ConnectionError, KeyError, SlumberHttpBaseException, Timeout):
'for EnterpriseCustomer [%s].', logger.exception(
course_run_ids, 'Failed to check if course_runs [%s] exist in '
enterprise_customer_catalog_uuid, 'EnterpriseCustomerCatalog [%s]'
enterprise_customer_uuid, 'for EnterpriseCustomer [%s].',
) course_run_ids,
contains_content = False enterprise_customer_catalog_uuid,
enterprise_customer_uuid,
)
contains_content = False
return contains_content return contains_content
...@@ -12,13 +12,13 @@ from collections import OrderedDict ...@@ -12,13 +12,13 @@ from collections import OrderedDict
from urllib import urlencode from urllib import urlencode
from django.conf import settings from django.conf import settings
from django.core.cache import cache
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.urls import reverse from django.urls import reverse
from oscar.core.loading import get_model from oscar.core.loading import get_model
from requests.exceptions import ConnectionError, Timeout from requests.exceptions import ConnectionError, Timeout
from slumber.exceptions import SlumberBaseException from slumber.exceptions import SlumberBaseException
from ecommerce.cache_utils.utils import TieredCache
from ecommerce.core.constants import COUPON_PRODUCT_CLASS_NAME from ecommerce.core.constants import COUPON_PRODUCT_CLASS_NAME
from ecommerce.core.utils import get_cache_key from ecommerce.core.utils import get_cache_key
from ecommerce.coupons.views import voucher_is_valid from ecommerce.coupons.views import voucher_is_valid
...@@ -168,14 +168,15 @@ def is_course_in_enterprise_catalog(site, course_id, enterprise_catalog_id): ...@@ -168,14 +168,15 @@ def is_course_in_enterprise_catalog(site, course_id, enterprise_catalog_id):
course_id=course_id, course_id=course_id,
catalog_id=enterprise_catalog_id catalog_id=enterprise_catalog_id
) )
response = cache.get(cache_key) cached_response = TieredCache.get_cached_response(cache_key)
if not response: if cached_response.is_hit:
response = cached_response.value
else:
try: try:
# GET: /api/v1/catalogs/{catalog_id}/contains?course_run_id={course_run_ids}
response = site.siteconfiguration.discovery_api_client.catalogs(enterprise_catalog_id).contains.get( response = site.siteconfiguration.discovery_api_client.catalogs(enterprise_catalog_id).contains.get(
course_run_id=course_id course_run_id=course_id
) )
cache.set(cache_key, response, settings.COURSES_API_CACHE_TIMEOUT) TieredCache.set_all_tiers(cache_key, response, settings.COURSES_API_CACHE_TIMEOUT)
except (ConnectionError, SlumberBaseException, Timeout): except (ConnectionError, SlumberBaseException, Timeout):
logger.exception('Unable to connect to Discovery Service for catalog contains endpoint.') logger.exception('Unable to connect to Discovery Service for catalog contains endpoint.')
return False return False
......
import ddt import ddt
import httpretty import httpretty
from django.conf import settings from django.conf import settings
from django.core.cache import cache from mock import patch
from oscar.core.loading import get_model from oscar.core.loading import get_model
from ecommerce.cache_utils.utils import TieredCache
from ecommerce.core.tests import toggle_switch from ecommerce.core.tests import toggle_switch
from ecommerce.core.utils import get_cache_key from ecommerce.core.utils import get_cache_key
from ecommerce.courses.tests.factories import CourseFactory from ecommerce.courses.tests.factories import CourseFactory
from ecommerce.enterprise import api as enterprise_api from ecommerce.enterprise import api as enterprise_api
from ecommerce.enterprise.tests.mixins import EnterpriseServiceMockMixin from ecommerce.enterprise.tests.mixins import EnterpriseServiceMockMixin
from ecommerce.extensions.catalogue.tests.mixins import DiscoveryTestMixin
from ecommerce.extensions.partner.strategy import DefaultStrategy from ecommerce.extensions.partner.strategy import DefaultStrategy
from ecommerce.tests.testcases import TestCase from ecommerce.tests.testcases import TestCase
...@@ -18,7 +20,7 @@ StockRecord = get_model('partner', 'StockRecord') ...@@ -18,7 +20,7 @@ StockRecord = get_model('partner', 'StockRecord')
@ddt.ddt @ddt.ddt
@httpretty.activate @httpretty.activate
class EnterpriseAPITests(EnterpriseServiceMockMixin, TestCase): class EnterpriseAPITests(EnterpriseServiceMockMixin, DiscoveryTestMixin, TestCase):
def setUp(self): def setUp(self):
super(EnterpriseAPITests, self).setUp() super(EnterpriseAPITests, self).setUp()
self.course_run = CourseFactory() self.course_run = CourseFactory()
...@@ -54,14 +56,14 @@ class EnterpriseAPITests(EnterpriseServiceMockMixin, TestCase): ...@@ -54,14 +56,14 @@ class EnterpriseAPITests(EnterpriseServiceMockMixin, TestCase):
username=self.learner.username username=self.learner.username
) )
cached_enterprise_learner_response = cache.get(cache_key) enterprise_learner_cached_response = TieredCache.get_cached_response(cache_key)
self.assertIsNone(cached_enterprise_learner_response) self.assertTrue(enterprise_learner_cached_response.is_miss)
response = enterprise_api.fetch_enterprise_learner_data(self.request.site, self.learner) response = enterprise_api.fetch_enterprise_learner_data(self.request.site, self.learner)
self.assertEqual(len(response['results']), 1) self.assertEqual(len(response['results']), 1)
cached_course = cache.get(cache_key) course_cached_response = TieredCache.get_cached_response(cache_key)
self.assertEqual(cached_course, response) self.assertEqual(course_cached_response.value, response)
def _assert_contains_course_runs(self, expected, course_run_ids, enterprise_customer_uuid, def _assert_contains_course_runs(self, expected, course_run_ids, enterprise_customer_uuid,
enterprise_customer_catalog_uuid): enterprise_customer_catalog_uuid):
...@@ -145,6 +147,26 @@ class EnterpriseAPITests(EnterpriseServiceMockMixin, TestCase): ...@@ -145,6 +147,26 @@ class EnterpriseAPITests(EnterpriseServiceMockMixin, TestCase):
self._assert_contains_course_runs(expected, [self.course_run.id], 'fake-uuid', enterprise_customer_catalog_uuid) self._assert_contains_course_runs(expected, [self.course_run.id], 'fake-uuid', enterprise_customer_catalog_uuid)
def test_catalog_contains_course_runs_cache_hit(self):
"""
Verify `catalog_contains_course_runs` returns a cached response
"""
self.mock_catalog_contains_course_runs(
[self.course_run.id],
'fake-uuid',
enterprise_customer_catalog_uuid=None,
contains_content=True,
)
with patch.object(TieredCache, 'set_all_tiers', wraps=TieredCache.set_all_tiers) as mocked_set_all_tiers:
mocked_set_all_tiers.assert_not_called()
self._assert_contains_course_runs(True, [self.course_run.id], 'fake-uuid', None)
self.assertEqual(mocked_set_all_tiers.call_count, 2)
self._assert_contains_course_runs(True, [self.course_run.id], 'fake-uuid', None)
self.assertEqual(mocked_set_all_tiers.call_count, 2)
def test_catalog_contains_course_runs_with_api_exception(self): def test_catalog_contains_course_runs_with_api_exception(self):
""" """
Verify that method `catalog_contains_course_runs` returns the appropriate response Verify that method `catalog_contains_course_runs` returns the appropriate response
......
...@@ -2,11 +2,13 @@ ...@@ -2,11 +2,13 @@
import ddt import ddt
import httpretty import httpretty
from django.conf import settings from django.conf import settings
from mock import patch
from oscar.core.loading import get_model from oscar.core.loading import get_model
from requests.exceptions import ConnectionError, Timeout from requests.exceptions import ConnectionError, Timeout
from slumber.exceptions import SlumberBaseException from slumber.exceptions import SlumberBaseException
from testfixtures import LogCapture from testfixtures import LogCapture
from ecommerce.cache_utils.utils import TieredCache
from ecommerce.core.tests import toggle_switch from ecommerce.core.tests import toggle_switch
from ecommerce.coupons.tests.mixins import CouponMixin, DiscoveryMockMixin from ecommerce.coupons.tests.mixins import CouponMixin, DiscoveryMockMixin
from ecommerce.courses.tests.factories import CourseFactory from ecommerce.courses.tests.factories import CourseFactory
...@@ -371,6 +373,36 @@ class EntitlementsTests(EnterpriseServiceMockMixin, DiscoveryTestMixin, Discover ...@@ -371,6 +373,36 @@ class EntitlementsTests(EnterpriseServiceMockMixin, DiscoveryTestMixin, Discover
self._assert_num_requests(2) self._assert_num_requests(2)
self.assertTrue(is_course_available) self.assertTrue(is_course_available)
def test_is_course_in_enterprise_catalog_for_available_course_cached(self):
"""
Verify that the response from the discovery API call made in method
"is_course_in_enterprise_catalog" is cached for cases where the
course is available in the enterprise course catalog.
We expect 2 calls to set_all_tiers in the
is_course_in_enterprise_catalog method due to:
- the site_configuration api setup
- the result being cached
"""
enterprise_catalog_id = 1
self.mock_access_token_response()
self.mock_catalog_contains_endpoint(
discovery_api_url=self.site_configuration.discovery_api_url, catalog_id=enterprise_catalog_id,
course_run_ids=[self.course.id]
)
with patch.object(TieredCache, 'set_all_tiers', wraps=TieredCache.set_all_tiers) as mocked_set_all_tiers:
mocked_set_all_tiers.assert_not_called()
is_course_available = is_course_in_enterprise_catalog(
self.request.site, self.course.id, enterprise_catalog_id)
self.assertEqual(mocked_set_all_tiers.call_count, 2)
self.assertTrue(is_course_available)
is_course_available = is_course_in_enterprise_catalog(
self.request.site, self.course.id, enterprise_catalog_id)
self.assertEqual(mocked_set_all_tiers.call_count, 2)
self.assertTrue(is_course_available)
def test_is_course_in_enterprise_catalog_for_unavailable_course(self): def test_is_course_in_enterprise_catalog_for_unavailable_course(self):
""" """
Verify that method "is_course_in_enterprise_catalog" returns False if Verify that method "is_course_in_enterprise_catalog" returns False if
...@@ -391,6 +423,38 @@ class EntitlementsTests(EnterpriseServiceMockMixin, DiscoveryTestMixin, Discover ...@@ -391,6 +423,38 @@ class EntitlementsTests(EnterpriseServiceMockMixin, DiscoveryTestMixin, Discover
self._assert_num_requests(2) self._assert_num_requests(2)
self.assertFalse(is_course_available) self.assertFalse(is_course_available)
def test_is_course_in_enterprise_catalog_for_unavailable_course_cached(self):
"""
Verify that the response from the discovery API call made in method
"is_course_in_enterprise_catalog" is cached for cases where the
course is not available in the enterprise course catalog.
We expect 2 calls to set_all_tiers due to:
- the site_configuration api setup
- the result being cached
"""
enterprise_catalog_id = 1
self.mock_access_token_response()
self.mock_catalog_contains_endpoint(
discovery_api_url=self.site_configuration.discovery_api_url, catalog_id=enterprise_catalog_id,
course_run_ids=[self.course.id]
)
test_course = CourseFactory(id='edx/Non_Enterprise_Course/DemoX')
with patch.object(TieredCache, 'set_all_tiers', wraps=TieredCache.set_all_tiers) as mocked_set_all_tiers:
mocked_set_all_tiers.assert_not_called()
is_course_available = is_course_in_enterprise_catalog(
self.request.site, test_course.id, enterprise_catalog_id)
self.assertEqual(mocked_set_all_tiers.call_count, 2)
self.assertFalse(is_course_available)
is_course_available = is_course_in_enterprise_catalog(
self.request.site, test_course.id, enterprise_catalog_id)
self.assertEqual(mocked_set_all_tiers.call_count, 2)
self.assertFalse(is_course_available)
@ddt.data(ConnectionError, SlumberBaseException, Timeout) @ddt.data(ConnectionError, SlumberBaseException, Timeout)
def test_is_course_in_enterprise_catalog_for_error_in_get_course_catalogs(self, error): def test_is_course_in_enterprise_catalog_for_error_in_get_course_catalogs(self, error):
""" """
......
import json import json
from django.conf import settings from django.conf import settings
from django.core.cache import cache
from django.test import override_settings from django.test import override_settings
from django.urls import reverse from django.urls import reverse
from waffle.models import Switch from waffle.models import Switch
from ecommerce.cache_utils.utils import TieredCache
from ecommerce.core.models import SiteConfiguration from ecommerce.core.models import SiteConfiguration
from ecommerce.extensions.payment.tests.processors import AnotherDummyProcessor, DummyProcessor from ecommerce.extensions.payment.tests.processors import AnotherDummyProcessor, DummyProcessor
from ecommerce.tests.testcases import TestCase from ecommerce.tests.testcases import TestCase
...@@ -34,7 +34,7 @@ class PaymentProcessorListViewTests(TestCase): ...@@ -34,7 +34,7 @@ class PaymentProcessorListViewTests(TestCase):
self.addCleanup(reset_site_config) self.addCleanup(reset_site_config)
# Clear the view cache # Clear the view cache
cache.clear() TieredCache.clear_all_tiers()
def toggle_payment_processor(self, processor, active): def toggle_payment_processor(self, processor, active):
"""Set the given payment processor's Waffle switch.""" """Set the given payment processor's Waffle switch."""
......
...@@ -7,7 +7,6 @@ import warnings ...@@ -7,7 +7,6 @@ import warnings
import waffle import waffle
from django.conf import settings from django.conf import settings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.core.cache import cache
from django.db import transaction from django.db import transaction
from django.http import HttpResponseBadRequest, HttpResponseForbidden from django.http import HttpResponseBadRequest, HttpResponseForbidden
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
...@@ -18,6 +17,7 @@ from rest_framework import generics, status ...@@ -18,6 +17,7 @@ from rest_framework import generics, status
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response from rest_framework.response import Response
from ecommerce.cache_utils.utils import TieredCache
from ecommerce.core.utils import get_cache_key from ecommerce.core.utils import get_cache_key
from ecommerce.enterprise.entitlements import get_entitlement_voucher from ecommerce.enterprise.entitlements import get_entitlement_voucher
from ecommerce.extensions.analytics.utils import audit_log from ecommerce.extensions.analytics.utils import audit_log
...@@ -514,9 +514,9 @@ class BasketCalculateView(generics.GenericAPIView): ...@@ -514,9 +514,9 @@ class BasketCalculateView(generics.GenericAPIView):
resource_name='calculate', resource_name='calculate',
skus=skus skus=skus
) )
basket_calculate_results = cache.get(cache_key) cached_response = TieredCache.get_cached_response(cache_key)
if basket_calculate_results: if cached_response.is_hit:
return Response(basket_calculate_results) return Response(cached_response.value)
if waffle.flag_is_active(request, "disable_calculate_temporary_basket_atomic_transaction"): if waffle.flag_is_active(request, "disable_calculate_temporary_basket_atomic_transaction"):
response = self._calculate_temporary_basket(basket_owner, request, products, voucher, skus, code) response = self._calculate_temporary_basket(basket_owner, request, products, voucher, skus, code)
...@@ -524,6 +524,6 @@ class BasketCalculateView(generics.GenericAPIView): ...@@ -524,6 +524,6 @@ class BasketCalculateView(generics.GenericAPIView):
response = self._calculate_temporary_basket_atomic(basket_owner, request, products, voucher, skus, code) response = self._calculate_temporary_basket_atomic(basket_owner, request, products, voucher, skus, code)
if response and use_default_basket: if response and use_default_basket:
cache.set(cache_key, response, settings.ANONYMOUS_BASKET_CALCULATE_CACHE_TIMEOUT) TieredCache.set_all_tiers(cache_key, response, settings.ANONYMOUS_BASKET_CALCULATE_CACHE_TIMEOUT)
return Response(response) return Response(response)
...@@ -9,7 +9,6 @@ import mock ...@@ -9,7 +9,6 @@ import mock
import pytz import pytz
from django.conf import settings from django.conf import settings
from django.contrib.messages import get_messages from django.contrib.messages import get_messages
from django.core.cache import cache
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.test import override_settings from django.test import override_settings
from django.urls import reverse from django.urls import reverse
...@@ -23,6 +22,7 @@ from slumber.exceptions import SlumberBaseException ...@@ -23,6 +22,7 @@ from slumber.exceptions import SlumberBaseException
from testfixtures import LogCapture from testfixtures import LogCapture
from waffle.testutils import override_flag from waffle.testutils import override_flag
from ecommerce.cache_utils.utils import TieredCache
from ecommerce.core.constants import ENROLLMENT_CODE_PRODUCT_CLASS_NAME from ecommerce.core.constants import ENROLLMENT_CODE_PRODUCT_CLASS_NAME
from ecommerce.core.exceptions import SiteConfigurationError from ecommerce.core.exceptions import SiteConfigurationError
from ecommerce.core.tests import toggle_switch from ecommerce.core.tests import toggle_switch
...@@ -465,13 +465,13 @@ class BasketSummaryViewTests(EnterpriseServiceMockMixin, DiscoveryTestMixin, Dis ...@@ -465,13 +465,13 @@ class BasketSummaryViewTests(EnterpriseServiceMockMixin, DiscoveryTestMixin, Dis
cache_key = 'courses_api_detail_{}{}'.format(self.course.id, self.site.siteconfiguration.partner.short_code) cache_key = 'courses_api_detail_{}{}'.format(self.course.id, self.site.siteconfiguration.partner.short_code)
cache_key = hashlib.md5(cache_key).hexdigest() cache_key = hashlib.md5(cache_key).hexdigest()
cached_course_before = cache.get(cache_key) course_before_cached_response = TieredCache.get_cached_response(cache_key)
self.assertIsNone(cached_course_before) self.assertTrue(course_before_cached_response.is_miss)
response = self.client.get(self.path) response = self.client.get(self.path)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
cached_course_after = cache.get(cache_key) course_after_cached_response = TieredCache.get_cached_response(cache_key)
self.assertEqual(cached_course_after['title'], self.course.name) self.assertEqual(course_after_cached_response.value['title'], self.course.name)
@ddt.data({ @ddt.data({
'course': 'edX+DemoX', 'course': 'edX+DemoX',
......
...@@ -4,7 +4,6 @@ import logging ...@@ -4,7 +4,6 @@ import logging
import re import re
from django.conf import settings from django.conf import settings
from django.core.cache import cache
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from oscar.apps.offer.abstract_models import ( from oscar.apps.offer.abstract_models import (
...@@ -18,6 +17,7 @@ from requests.exceptions import ConnectionError, Timeout ...@@ -18,6 +17,7 @@ from requests.exceptions import ConnectionError, Timeout
from slumber.exceptions import SlumberBaseException from slumber.exceptions import SlumberBaseException
from threadlocals.threadlocals import get_current_request from threadlocals.threadlocals import get_current_request
from ecommerce.cache_utils.utils import TieredCache
from ecommerce.core.utils import get_cache_key, log_message_and_raise_validation_error from ecommerce.core.utils import get_cache_key, log_message_and_raise_validation_error
OFFER_PRIORITY_ENTERPRISE = 10 OFFER_PRIORITY_ENTERPRISE = 10
...@@ -78,14 +78,14 @@ class Benefit(AbstractBenefit): ...@@ -78,14 +78,14 @@ class Benefit(AbstractBenefit):
course_id=product_id, course_id=product_id,
query=query query=query
) )
response = cache.get(cache_key) cached_response = TieredCache.get_cached_response(cache_key)
if response is False: if cached_response.is_miss:
applicable_lines.remove(line)
elif response is None:
if line.product.is_seat_product: if line.product.is_seat_product:
course_run_ids.append({'id': product_id, 'cache_key': cache_key, 'line': line}) course_run_ids.append({'id': product_id, 'cache_key': cache_key, 'line': line})
else: else:
course_uuids.append({'id': product_id, 'cache_key': cache_key, 'line': line}) course_uuids.append({'id': product_id, 'cache_key': cache_key, 'line': line})
elif cached_response.value is False:
applicable_lines.remove(line)
return course_run_ids, course_uuids, applicable_lines return course_run_ids, course_uuids, applicable_lines
def get_applicable_lines(self, offer, basket, range=None): # pylint: disable=redefined-builtin def get_applicable_lines(self, offer, basket, range=None): # pylint: disable=redefined-builtin
...@@ -116,7 +116,7 @@ class Benefit(AbstractBenefit): ...@@ -116,7 +116,7 @@ class Benefit(AbstractBenefit):
# Cache range-state individually for each course or run identifier and remove lines not in the range. # Cache range-state individually for each course or run identifier and remove lines not in the range.
for metadata in course_run_ids + course_uuids: for metadata in course_run_ids + course_uuids:
in_range = response[str(metadata['id'])] in_range = response[str(metadata['id'])]
cache.set(metadata['cache_key'], in_range, settings.COURSES_API_CACHE_TIMEOUT) TieredCache.set_all_tiers(metadata['cache_key'], in_range, settings.COURSES_API_CACHE_TIMEOUT)
if not in_range: if not in_range:
applicable_lines.remove(metadata['line']) applicable_lines.remove(metadata['line'])
return [(line.product.stockrecords.first().price_excl_tax, line) for line in applicable_lines] return [(line.product.stockrecords.first().price_excl_tax, line) for line in applicable_lines]
...@@ -334,19 +334,21 @@ class Range(AbstractRange): ...@@ -334,19 +334,21 @@ class Range(AbstractRange):
course_id=product.course_id, course_id=product.course_id,
catalog_id=self.course_catalog catalog_id=self.course_catalog
) )
response = cache.get(cache_key) cached_response = TieredCache.get_cached_response(cache_key)
if not response: if cached_response.is_hit:
discovery_api_client = request.site.siteconfiguration.discovery_api_client return cached_response.value
try:
# GET: /api/v1/catalogs/{catalog_id}/contains?course_run_id={course_run_ids} discovery_api_client = request.site.siteconfiguration.discovery_api_client
response = discovery_api_client.catalogs(self.course_catalog).contains.get( try:
course_run_id=product.course_id # GET: /api/v1/catalogs/{catalog_id}/contains?course_run_id={course_run_ids}
) response = discovery_api_client.catalogs(self.course_catalog).contains.get(
cache.set(cache_key, response, settings.COURSES_API_CACHE_TIMEOUT) course_run_id=product.course_id
except (ConnectionError, SlumberBaseException, Timeout): )
raise Exception('Unable to connect to Discovery Service for catalog contains endpoint.')
return response TieredCache.set_all_tiers(cache_key, response, settings.COURSES_API_CACHE_TIMEOUT)
return response
except (ConnectionError, SlumberBaseException, Timeout):
raise Exception('Unable to connect to Discovery Service for catalog contains endpoint.')
def contains_product(self, product): def contains_product(self, product):
""" """
......
...@@ -4,11 +4,13 @@ from __future__ import unicode_literals ...@@ -4,11 +4,13 @@ from __future__ import unicode_literals
import ddt import ddt
import httpretty import httpretty
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from mock import patch
from oscar.core.loading import get_model from oscar.core.loading import get_model
from oscar.test import factories from oscar.test import factories
from requests.exceptions import ConnectionError, Timeout from requests.exceptions import ConnectionError, Timeout
from slumber.exceptions import SlumberBaseException from slumber.exceptions import SlumberBaseException
from ecommerce.cache_utils.utils import TieredCache
from ecommerce.coupons.tests.mixins import CouponMixin, DiscoveryMockMixin from ecommerce.coupons.tests.mixins import CouponMixin, DiscoveryMockMixin
from ecommerce.extensions.catalogue.tests.mixins import DiscoveryTestMixin from ecommerce.extensions.catalogue.tests.mixins import DiscoveryTestMixin
from ecommerce.tests.testcases import TestCase from ecommerce.tests.testcases import TestCase
...@@ -294,6 +296,39 @@ class RangeTests(CouponMixin, DiscoveryTestMixin, DiscoveryMockMixin, TestCase): ...@@ -294,6 +296,39 @@ class RangeTests(CouponMixin, DiscoveryTestMixin, DiscoveryMockMixin, TestCase):
_range = Range.objects.create(**data) _range = Range.objects.create(**data)
self.assertEqual(_range.course_seat_types, course_seat_types) self.assertEqual(_range.course_seat_types, course_seat_types)
def test_catalog_contains_product(self):
"""
Verify that catalog_contains_product is cached
We expect 2 calls to set_all_tiers due to:
- the site_configuration api setup
- the result being cached
"""
self.mock_access_token_response()
course, __ = self.create_course_and_seat()
course_catalog_id = 1
self.range.catalog_query = None
self.range.course_seat_types = 'verified'
self.range.course_catalog = course_catalog_id
self.range.save()
self.product.course = course
self.product.save()
self.mock_catalog_contains_endpoint(
discovery_api_url=self.site_configuration.discovery_api_url, catalog_id=course_catalog_id,
course_run_ids=[course.id]
)
with patch.object(TieredCache, 'set_all_tiers', wraps=TieredCache.set_all_tiers) as mocked_set_all_tiers:
mocked_set_all_tiers.assert_not_called()
_ = self.range.catalog_contains_product(self.product)
self.assertEqual(mocked_set_all_tiers.call_count, 2)
_ = self.range.catalog_contains_product(self.product)
self.assertEqual(mocked_set_all_tiers.call_count, 2)
@ddt.ddt @ddt.ddt
@httpretty.activate @httpretty.activate
......
"""Test Order Utility classes """ """Test Order Utility classes """
import datetime
import json import json
import logging import logging
import ddt import ddt
import httpretty import httpretty
import mock import mock
import pytz
from django.test.client import RequestFactory from django.test.client import RequestFactory
from oscar.core.loading import get_class, get_model from oscar.core.loading import get_class, get_model
from oscar.test.factories import BasketFactory from oscar.test.factories import BasketFactory
from requests import Timeout from requests import Timeout
from testfixtures import LogCapture from testfixtures import LogCapture
from ecommerce.cache_utils.utils import TieredCache
from ecommerce.core.url_utils import get_lms_entitlement_api_url from ecommerce.core.url_utils import get_lms_entitlement_api_url
from ecommerce.extensions.fulfillment.status import ORDER from ecommerce.extensions.fulfillment.status import ORDER
from ecommerce.extensions.order.utils import UserAlreadyPlacedOrder from ecommerce.extensions.order.utils import UserAlreadyPlacedOrder
...@@ -22,6 +25,7 @@ from ecommerce.tests.factories import PartnerFactory, SiteConfigurationFactory ...@@ -22,6 +25,7 @@ from ecommerce.tests.factories import PartnerFactory, SiteConfigurationFactory
from ecommerce.tests.testcases import TestCase from ecommerce.tests.testcases import TestCase
LOGGER_NAME = 'ecommerce.extensions.order.utils' LOGGER_NAME = 'ecommerce.extensions.order.utils'
EXPIRED_DATE = datetime.datetime(year=1985, month=10, day=26, hour=1, minute=20, tzinfo=pytz.utc)
Country = get_class('address.models', 'Country') Country = get_class('address.models', 'Country')
NoShippingRequired = get_class('shipping.methods', 'NoShippingRequired') NoShippingRequired = get_class('shipping.methods', 'NoShippingRequired')
...@@ -310,3 +314,29 @@ class UserAlreadyPlacedOrderTests(RefundTestMixin, TestCase): ...@@ -310,3 +314,29 @@ class UserAlreadyPlacedOrderTests(RefundTestMixin, TestCase):
refund_line.status = refund_line_status refund_line.status = refund_line_status
refund_line.save() refund_line.save()
self.assertEqual(UserAlreadyPlacedOrder.is_order_line_refunded(refund_line.order_line), is_refunded) self.assertEqual(UserAlreadyPlacedOrder.is_order_line_refunded(refund_line.order_line), is_refunded)
@httpretty.activate
def test_is_entitlement_expired_cached(self):
"""
Test that entitlement's expired status gets cached
We expect 2 calls to set_all_tiers in the is_entitlement_expired
method due to:
- the site_configuration api setup
- the result being cached
"""
self.mock_access_token_response()
self.course_entitlement.expires = EXPIRED_DATE
httpretty.register_uri(httpretty.GET, get_lms_entitlement_api_url() +
'entitlements/' + self.course_entitlement_uuid + '/',
status=200, body=json.dumps({}), content_type='application/json')
with mock.patch.object(TieredCache, 'set_all_tiers', wraps=TieredCache.set_all_tiers) as mocked_set_all_tiers:
mocked_set_all_tiers.assert_not_called()
_ = UserAlreadyPlacedOrder.is_entitlement_expired(self.course_entitlement_uuid, site=self.site)
self.assertEqual(mocked_set_all_tiers.call_count, 2)
_ = UserAlreadyPlacedOrder.is_entitlement_expired(self.course_entitlement_uuid, site=self.site)
self.assertEqual(mocked_set_all_tiers.call_count, 2)
...@@ -5,7 +5,6 @@ import logging ...@@ -5,7 +5,6 @@ import logging
import waffle import waffle
from django.conf import settings from django.conf import settings
from django.core.cache import cache
from edx_rest_api_client.client import EdxRestApiClient from edx_rest_api_client.client import EdxRestApiClient
from edx_rest_api_client.exceptions import HttpNotFoundError from edx_rest_api_client.exceptions import HttpNotFoundError
from oscar.apps.order.utils import OrderCreator as OscarOrderCreator from oscar.apps.order.utils import OrderCreator as OscarOrderCreator
...@@ -13,6 +12,7 @@ from oscar.core.loading import get_model ...@@ -13,6 +12,7 @@ from oscar.core.loading import get_model
from requests.exceptions import ConnectionError, ConnectTimeout # pylint: disable=ungrouped-imports from requests.exceptions import ConnectionError, ConnectTimeout # pylint: disable=ungrouped-imports
from threadlocals.threadlocals import get_current_request from threadlocals.threadlocals import get_current_request
from ecommerce.cache_utils.utils import TieredCache
from ecommerce.core.url_utils import get_lms_entitlement_api_url from ecommerce.core.url_utils import get_lms_entitlement_api_url
from ecommerce.extensions.order.constants import DISABLE_REPEAT_ORDER_CHECK_SWITCH_NAME from ecommerce.extensions.order.constants import DISABLE_REPEAT_ORDER_CHECK_SWITCH_NAME
from ecommerce.extensions.refund.status import REFUND_LINE from ecommerce.extensions.refund.status import REFUND_LINE
...@@ -149,15 +149,15 @@ class UserAlreadyPlacedOrder(object): ...@@ -149,15 +149,15 @@ class UserAlreadyPlacedOrder(object):
jwt=site.siteconfiguration.access_token) jwt=site.siteconfiguration.access_token)
partner_short_code = site.siteconfiguration.partner.short_code partner_short_code = site.siteconfiguration.partner.short_code
key = 'course_entitlement_detail_{}{}'.format(entitlement_uuid, partner_short_code) key = 'course_entitlement_detail_{}{}'.format(entitlement_uuid, partner_short_code)
entitlement = cache.get(key) entitlement_cached_response = TieredCache.get_cached_response(key)
if entitlement_cached_response.is_hit:
if not entitlement: entitlement = entitlement_cached_response.value
else:
logger.debug('Trying to get entitlement {%s}', entitlement_uuid) logger.debug('Trying to get entitlement {%s}', entitlement_uuid)
entitlement = entitlement_api_client.entitlements(entitlement_uuid).get() entitlement = entitlement_api_client.entitlements(entitlement_uuid).get()
cache.set(key, entitlement, settings.COURSES_API_CACHE_TIMEOUT) TieredCache.set_all_tiers(key, entitlement, settings.COURSES_API_CACHE_TIMEOUT)
expired = entitlement.get('expired_at') expired = entitlement.get('expired_at')
logger.debug('Entitlement {%s} expired = {%s}', entitlement_uuid, expired) logger.debug('Entitlement {%s} expired = {%s}', entitlement_uuid, expired)
return expired return expired
......
import logging import logging
from django.conf import settings from django.conf import settings
from django.core.cache import cache
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
from waffle.models import Switch from waffle.models import Switch
from ecommerce.cache_utils.utils import TieredCache
from ecommerce.extensions.api.v2.views.payments import PAYMENT_PROCESSOR_CACHE_KEY from ecommerce.extensions.api.v2.views.payments import PAYMENT_PROCESSOR_CACHE_KEY
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -22,5 +22,5 @@ def invalidate_processor_cache(*_args, **kwargs): ...@@ -22,5 +22,5 @@ def invalidate_processor_cache(*_args, **kwargs):
if len(parts) == 2: if len(parts) == 2:
processor = parts[1] processor = parts[1]
logger.info('Switched payment processor [%s] %s.', processor, 'on' if switch.active else 'off') logger.info('Switched payment processor [%s] %s.', processor, 'on' if switch.active else 'off')
cache.delete(PAYMENT_PROCESSOR_CACHE_KEY) TieredCache.delete_all_tiers(PAYMENT_PROCESSOR_CACHE_KEY)
logger.info('Invalidated payment processor cache after toggling [%s].', switch.name) logger.info('Invalidated payment processor cache after toggling [%s].', switch.name)
from django.conf import settings from django.conf import settings
from django.core.cache import cache
from django.urls import reverse from django.urls import reverse
from waffle.models import Switch from waffle.models import Switch
from ecommerce.cache_utils.utils import TieredCache
from ecommerce.extensions.api.v2.views.payments import PAYMENT_PROCESSOR_CACHE_KEY from ecommerce.extensions.api.v2.views.payments import PAYMENT_PROCESSOR_CACHE_KEY
from ecommerce.tests.testcases import TestCase from ecommerce.tests.testcases import TestCase
...@@ -16,8 +16,8 @@ class SignalTests(TestCase): ...@@ -16,8 +16,8 @@ class SignalTests(TestCase):
# Make a call that triggers cache creation # Make a call that triggers cache creation
response = self.client.get(reverse('api:v2:payment:list_processors')) response = self.client.get(reverse('api:v2:payment:list_processors'))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertIsNotNone(cache.get(PAYMENT_PROCESSOR_CACHE_KEY)) self.assertTrue(TieredCache.get_cached_response(PAYMENT_PROCESSOR_CACHE_KEY).is_hit)
# Toggle a switch to trigger cache deletion # Toggle a switch to trigger cache deletion
Switch.objects.get_or_create(name=settings.PAYMENT_PROCESSOR_SWITCH_PREFIX + 'dummy') Switch.objects.get_or_create(name=settings.PAYMENT_PROCESSOR_SWITCH_PREFIX + 'dummy')
self.assertIsNone(cache.get(PAYMENT_PROCESSOR_CACHE_KEY)) self.assertTrue(TieredCache.get_cached_response(PAYMENT_PROCESSOR_CACHE_KEY).is_miss)
...@@ -11,13 +11,13 @@ from decimal import Decimal, DecimalException ...@@ -11,13 +11,13 @@ from decimal import Decimal, DecimalException
import dateutil.parser import dateutil.parser
import pytz import pytz
from django.conf import settings from django.conf import settings
from django.core.cache import cache
from django.urls import reverse from django.urls import reverse
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from oscar.core.loading import get_model from oscar.core.loading import get_model
from oscar.templatetags.currency_filters import currency from oscar.templatetags.currency_filters import currency
from ecommerce.cache_utils.utils import TieredCache
from ecommerce.core.url_utils import get_ecommerce_url from ecommerce.core.url_utils import get_ecommerce_url
from ecommerce.core.utils import log_message_and_raise_validation_error from ecommerce.core.utils import log_message_and_raise_validation_error
from ecommerce.extensions.api import exceptions from ecommerce.extensions.api import exceptions
...@@ -674,12 +674,15 @@ def get_cached_voucher(code): ...@@ -674,12 +674,15 @@ def get_cached_voucher(code):
Raises: Raises:
Voucher.DoesNotExist: When no vouchers with provided code exist. Voucher.DoesNotExist: When no vouchers with provided code exist.
""" """
cache_key = 'voucher_{code}'.format(code=code) voucher_code = 'voucher_{code}'.format(code=code)
cache_key = hashlib.md5(cache_key).hexdigest() # pylint: disable=redefined-variable-type cache_key = hashlib.md5(voucher_code).hexdigest()
voucher = cache.get(cache_key) voucher_cached_response = TieredCache.get_cached_response(cache_key)
if not voucher: if voucher_cached_response.is_hit:
voucher = Voucher.objects.get(code=code) return voucher_cached_response.value
cache.set(cache_key, voucher, settings.VOUCHER_CACHE_TIMEOUT)
voucher = Voucher.objects.get(code=code)
TieredCache.set_all_tiers(cache_key, voucher, settings.VOUCHER_CACHE_TIMEOUT)
return voucher return voucher
......
import logging import logging
from django.conf import settings from django.conf import settings
from django.core.cache import cache
from ecommerce.cache_utils.utils import TieredCache
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -31,14 +32,15 @@ class ProgramsApiClient(object): ...@@ -31,14 +32,15 @@ class ProgramsApiClient(object):
program_uuid = str(uuid) program_uuid = str(uuid)
cache_key = '{site_domain}-program-{uuid}'.format(site_domain=self.site_domain, uuid=program_uuid) cache_key = '{site_domain}-program-{uuid}'.format(site_domain=self.site_domain, uuid=program_uuid)
program = cache.get(cache_key) program_cached_response = TieredCache.get_cached_response(cache_key)
if program: # pragma: no cover if program_cached_response.is_hit: # pragma: no cover
logger.debug('Program [%s] was found in the cache.', program_uuid) logger.debug('Program [%s] was found in the cache.', program_uuid)
else: return program_cached_response.value
logging.info('Retrieving details of of program [%s]...', program_uuid)
program = self.client.programs(program_uuid).get() logging.info('Retrieving details of of program [%s]...', program_uuid)
cache.set(cache_key, program, self.cache_ttl) program = self.client.programs(program_uuid).get()
logging.info('Program [%s] was successfully retrieved and cached.', program_uuid)
TieredCache.set_all_tiers(cache_key, program, self.cache_ttl)
logging.info('Program [%s] was successfully retrieved and cached.', program_uuid)
return program return program
...@@ -10,7 +10,6 @@ import jwt ...@@ -10,7 +10,6 @@ import jwt
from django.conf import settings from django.conf import settings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.core.cache import cache
from django.urls import reverse from django.urls import reverse
from django.utils.timezone import now from django.utils.timezone import now
from mock import patch from mock import patch
...@@ -20,7 +19,9 @@ from oscar.test.utils import RequestFactory ...@@ -20,7 +19,9 @@ from oscar.test.utils import RequestFactory
from social_django.models import UserSocialAuth from social_django.models import UserSocialAuth
from threadlocals.threadlocals import set_thread_variable from threadlocals.threadlocals import set_thread_variable
from ecommerce.cache_utils.utils import TieredCache
from ecommerce.core.url_utils import get_lms_url from ecommerce.core.url_utils import get_lms_url
from ecommerce.courses.models import Course
from ecommerce.courses.utils import mode_for_product from ecommerce.courses.utils import mode_for_product
from ecommerce.extensions.fulfillment.signals import SHIPPING_EVENT_NAME from ecommerce.extensions.fulfillment.signals import SHIPPING_EVENT_NAME
from ecommerce.tests.factories import SiteConfigurationFactory from ecommerce.tests.factories import SiteConfigurationFactory
...@@ -80,7 +81,7 @@ class ThrottlingMixin(object): ...@@ -80,7 +81,7 @@ class ThrottlingMixin(object):
super(ThrottlingMixin, self).setUp() super(ThrottlingMixin, self).setUp()
# Throttling for tests relies on the cache. To get around throttling, simply clear the cache. # Throttling for tests relies on the cache. To get around throttling, simply clear the cache.
self.addCleanup(cache.clear) self.addCleanup(TieredCache.clear_all_tiers)
class JwtMixin(object): class JwtMixin(object):
...@@ -255,6 +256,7 @@ class SiteMixin(object): ...@@ -255,6 +256,7 @@ class SiteMixin(object):
domain = 'testserver.fake' domain = 'testserver.fake'
self.client = self.client_class(SERVER_NAME=domain) self.client = self.client_class(SERVER_NAME=domain)
Course.objects.all().delete()
Partner.objects.all().delete() Partner.objects.all().delete()
Site.objects.all().delete() Site.objects.all().delete()
self.site_configuration = SiteConfigurationFactory( self.site_configuration = SiteConfigurationFactory(
......
from django.conf import settings from django.conf import settings
from django.core.cache import cache
from django.test import LiveServerTestCase as DjangoLiveServerTestCase from django.test import LiveServerTestCase as DjangoLiveServerTestCase
from django.test import TestCase as DjangoTestCase from django.test import TestCase as DjangoTestCase
from django.test import TransactionTestCase as DjangoTransactionTestCase from django.test import TransactionTestCase as DjangoTransactionTestCase
from ecommerce.cache_utils.utils import TieredCache
from ecommerce.tests.mixins import SiteMixin, TestServerUrlMixin, UserMixin from ecommerce.tests.mixins import SiteMixin, TestServerUrlMixin, UserMixin
class CacheMixin(object): class TieredCacheMixin(object):
def setUp(self): def setUp(self):
cache.clear() TieredCache.clear_all_tiers()
super(CacheMixin, self).setUp() super(TieredCacheMixin, self).setUp()
def tearDown(self): def tearDown(self):
cache.clear() TieredCache.clear_all_tiers()
super(CacheMixin, self).tearDown() super(TieredCacheMixin, self).tearDown()
class ViewTestMixin(CacheMixin): class ViewTestMixin(TieredCacheMixin):
path = None path = None
def setUp(self): def setUp(self):
...@@ -50,7 +50,7 @@ class ViewTestMixin(CacheMixin): ...@@ -50,7 +50,7 @@ class ViewTestMixin(CacheMixin):
self.assert_get_response_status(200) self.assert_get_response_status(200)
class TestCase(TestServerUrlMixin, UserMixin, SiteMixin, CacheMixin, DjangoTestCase): class TestCase(TestServerUrlMixin, UserMixin, SiteMixin, TieredCacheMixin, DjangoTestCase):
""" """
Base test case for ecommerce tests. Base test case for ecommerce tests.
...@@ -58,7 +58,7 @@ class TestCase(TestServerUrlMixin, UserMixin, SiteMixin, CacheMixin, DjangoTestC ...@@ -58,7 +58,7 @@ class TestCase(TestServerUrlMixin, UserMixin, SiteMixin, CacheMixin, DjangoTestC
""" """
class LiveServerTestCase(TestServerUrlMixin, UserMixin, SiteMixin, CacheMixin, DjangoLiveServerTestCase): class LiveServerTestCase(TestServerUrlMixin, UserMixin, SiteMixin, TieredCacheMixin, DjangoLiveServerTestCase):
""" """
Base test case for ecommerce tests. Base test case for ecommerce tests.
...@@ -67,7 +67,7 @@ class LiveServerTestCase(TestServerUrlMixin, UserMixin, SiteMixin, CacheMixin, D ...@@ -67,7 +67,7 @@ class LiveServerTestCase(TestServerUrlMixin, UserMixin, SiteMixin, CacheMixin, D
pass pass
class TransactionTestCase(TestServerUrlMixin, UserMixin, SiteMixin, CacheMixin, DjangoTransactionTestCase): class TransactionTestCase(TestServerUrlMixin, UserMixin, SiteMixin, TieredCacheMixin, DjangoTransactionTestCase):
""" """
Base test case for ecommerce tests. Base test case for ecommerce tests.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment