diff --git a/common/djangoapps/third_party_auth/tests/specs/base.py b/common/djangoapps/third_party_auth/tests/specs/base.py index 84ab7e07e6..ee879390b4 100644 --- a/common/djangoapps/third_party_auth/tests/specs/base.py +++ b/common/djangoapps/third_party_auth/tests/specs/base.py @@ -13,10 +13,12 @@ from django.contrib.sessions.backends import cache from django.core.urlresolvers import reverse from django.test import utils as django_utils from django.conf import settings as django_settings -from edxmako.tests import mako_middleware_process_request from social import actions, exceptions from social.apps.django_app import utils as social_utils from social.apps.django_app import views as social_views + +from edxmako.tests import mako_middleware_process_request +from lms.djangoapps.commerce.tests import TEST_API_URL, TEST_API_SIGNING_KEY from student import models as student_models from student import views as student_views from student.tests.factories import UserFactory @@ -898,7 +900,9 @@ class IntegrationTest(testutil.TestCase, test.TestCase): strategy.request.backend.auth_complete = mock.MagicMock(return_value=self.fake_auth_complete(strategy)) -class Oauth2IntegrationTest(IntegrationTest): # pylint: disable=abstract-method +# pylint: disable=test-inherits-tests, abstract-method +@django_utils.override_settings(ECOMMERCE_API_URL=TEST_API_URL, ECOMMERCE_API_SIGNING_KEY=TEST_API_SIGNING_KEY) +class Oauth2IntegrationTest(IntegrationTest): """Base test case for integration tests of Oauth2 providers.""" # Dict of string -> object. Information about the token granted to the diff --git a/lms/djangoapps/commerce/migrations/0004_auto_20160531_0950.py b/lms/djangoapps/commerce/migrations/0004_auto_20160531_0950.py new file mode 100644 index 0000000000..96e076d5b1 --- /dev/null +++ b/lms/djangoapps/commerce/migrations/0004_auto_20160531_0950.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('commerce', '0003_auto_20160329_0709'), + ] + + operations = [ + migrations.AddField( + model_name='commerceconfiguration', + name='cache_ttl', + field=models.PositiveIntegerField(default=0, help_text='Specified in seconds. Enable caching by setting this to a value greater than 0.', verbose_name='Cache Time To Live'), + ), + migrations.AddField( + model_name='commerceconfiguration', + name='receipt_page', + field=models.CharField(default=b'/commerce/checkout/receipt/?orderNum=', help_text='Path to order receipt page.', max_length=255), + ), + ] diff --git a/lms/djangoapps/commerce/models.py b/lms/djangoapps/commerce/models.py index 287ea91964..586a69ade6 100644 --- a/lms/djangoapps/commerce/models.py +++ b/lms/djangoapps/commerce/models.py @@ -13,6 +13,9 @@ class CommerceConfiguration(ConfigurationModel): class Meta(object): app_label = "commerce" + API_NAME = 'commerce' + CACHE_KEY = 'commerce.api.data' + checkout_on_ecommerce_service = models.BooleanField( default=False, help_text=_('Use the checkout page hosted by the E-Commerce service.') @@ -23,6 +26,23 @@ class CommerceConfiguration(ConfigurationModel): default='/basket/single-item/', help_text=_('Path to single course checkout page hosted by the E-Commerce service.') ) + cache_ttl = models.PositiveIntegerField( + verbose_name=_('Cache Time To Live'), + default=0, + help_text=_( + 'Specified in seconds. Enable caching by setting this to a value greater than 0.' + ) + ) + receipt_page = models.CharField( + max_length=255, + default='/commerce/checkout/receipt/?orderNum=', + help_text=_('Path to order receipt page.') + ) def __unicode__(self): return "Commerce configuration" + + @property + def is_cache_enabled(self): + """Whether responses from the Ecommerce API will be cached.""" + return self.cache_ttl > 0 diff --git a/lms/djangoapps/commerce/tests/factories.py b/lms/djangoapps/commerce/tests/factories.py new file mode 100644 index 0000000000..8a124b3b5c --- /dev/null +++ b/lms/djangoapps/commerce/tests/factories.py @@ -0,0 +1,55 @@ +""" Factories for generating fake commerce-related data. """ +import factory +from factory.fuzzy import FuzzyText + + +class OrderFactory(factory.Factory): + """ Factory for stubbing orders resources from Ecommerce (v2). """ + class Meta(object): + model = dict + + number = factory.Sequence(lambda n: 'edx-%d' % n) + date_placed = '2016-01-01T10:00:00Z' + status = 'Complete' + currency = 'USD' + total_excl_tax = '100.00' + lines = [] + + +class OrderLineFactory(factory.Factory): + """ Factory for stubbing order lines resources from Ecommerce (v2). """ + class Meta(object): + model = dict + + title = FuzzyText(prefix='Seat in ') + quantity = 1 + description = FuzzyText() + status = 'Complete' + line_price_excl_tax = '100.00' + unit_price_excl_tax = '100.00' + product = {} + + +class ProductFactory(factory.Factory): + """ Factory for stubbing Product resources from Ecommerce (v2). """ + class Meta(object): + model = dict + + id = factory.Sequence(lambda n: n) # pylint: disable=invalid-name + url = 'http://test/api/v2/products/' + str(id) + product_class = 'Seat' + title = FuzzyText(prefix='Seat in ') + price = '100.00' + attribute_values = [] + + +class ProductAttributeFactory(factory.Factory): + """ Factory for stubbing product attribute resources from + Ecommerce (v2). + """ + class Meta(object): + model = dict + + name = FuzzyText() + code = FuzzyText() + value = FuzzyText() diff --git a/lms/djangoapps/commerce/tests/mocks.py b/lms/djangoapps/commerce/tests/mocks.py index 4d6fcfdd35..933e4bbb01 100644 --- a/lms/djangoapps/commerce/tests/mocks.py +++ b/lms/djangoapps/commerce/tests/mocks.py @@ -3,7 +3,7 @@ import json import httpretty -from commerce.tests import TEST_API_URL +from commerce.tests import TEST_API_URL, factories class mock_ecommerce_api_endpoint(object): # pylint: disable=invalid-name @@ -117,3 +117,26 @@ class mock_order_endpoint(mock_ecommerce_api_endpoint): # pylint: disable=inval def get_uri(self): return TEST_API_URL + '/orders/{}/'.format(self.order_number) + + +class mock_get_orders(mock_ecommerce_api_endpoint): # pylint: disable=invalid-name + """ Mocks calls to E-Commerce API client order get method. """ + + default_response = { + 'results': [ + factories.OrderFactory( + lines=[ + factories.OrderLineFactory( + product=factories.ProductFactory(attribute_values=[factories.ProductAttributeFactory( + name='certificate_type', + value='verified' + )]) + ) + ] + ) + ] + } + method = httpretty.GET + + def get_uri(self): + return TEST_API_URL + '/orders/' diff --git a/lms/djangoapps/commerce/views.py b/lms/djangoapps/commerce/views.py index 291bff1bc6..f24df08ba8 100644 --- a/lms/djangoapps/commerce/views.py +++ b/lms/djangoapps/commerce/views.py @@ -3,14 +3,16 @@ import logging from django.conf import settings from django.contrib.auth.decorators import login_required +from django.core.cache import cache +from django.utils.translation import ugettext as _ from django.views.decorators.csrf import csrf_exempt +from commerce.models import CommerceConfiguration from edxmako.shortcuts import render_to_response from microsite_configuration import microsite from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification -from shoppingcart.processors.CyberSource2 import is_user_payment_error -from django.utils.translation import ugettext as _ from openedx.core.djangoapps.theming.helpers import is_request_in_themed_site +from shoppingcart.processors.CyberSource2 import is_user_payment_error log = logging.getLogger(__name__) @@ -65,6 +67,13 @@ def checkout_receipt(request): "If your course does not appear on your dashboard, contact {payment_support_link}." ).format(payment_support_link=payment_support_link) + commerce_configuration = CommerceConfiguration.current() + # user order cache should be cleared when a new order is placed + # so user can see new order in their order history. + if is_payment_complete and commerce_configuration.enabled and commerce_configuration.is_cache_enabled: + cache_key = commerce_configuration.CACHE_KEY + '.' + str(request.user.id) + cache.delete(cache_key) + context = { 'page_title': page_title, 'is_payment_complete': is_payment_complete, diff --git a/lms/djangoapps/student_account/test/test_views.py b/lms/djangoapps/student_account/test/test_views.py index 4482d54341..7cf2f0cec3 100644 --- a/lms/djangoapps/student_account/test/test_views.py +++ b/lms/djangoapps/student_account/test/test_views.py @@ -17,14 +17,19 @@ from django.contrib.messages.middleware import MessageMiddleware from django.test import TestCase from django.test.utils import override_settings from django.http import HttpRequest +from edx_rest_api_client import exceptions from course_modes.models import CourseMode +from commerce.models import CommerceConfiguration +from commerce.tests import TEST_API_URL, TEST_API_SIGNING_KEY, factories +from commerce.tests.mocks import mock_get_orders +from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin from openedx.core.djangoapps.user_api.accounts.api import activate_account, create_account from openedx.core.djangoapps.user_api.accounts import EMAIL_MAX_LENGTH from openedx.core.djangolib.js_utils import dump_js_escaped_json from openedx.core.djangolib.testing.utils import CacheIsolationTestCase from student.tests.factories import UserFactory -from student_account.views import account_settings_context +from student_account.views import account_settings_context, get_user_orders from third_party_auth.tests.testutil import simulate_running_pipeline, ThirdPartyAuthTestMixin from util.testing import UrlResetMixin from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase @@ -442,7 +447,8 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi }) -class AccountSettingsViewTest(ThirdPartyAuthTestMixin, TestCase): +@override_settings(ECOMMERCE_API_URL=TEST_API_URL, ECOMMERCE_API_SIGNING_KEY=TEST_API_SIGNING_KEY) +class AccountSettingsViewTest(ThirdPartyAuthTestMixin, TestCase, ProgramsApiConfigMixin): """ Tests for the account settings view. """ USERNAME = 'student' @@ -461,6 +467,7 @@ class AccountSettingsViewTest(ThirdPartyAuthTestMixin, TestCase): def setUp(self): super(AccountSettingsViewTest, self).setUp() self.user = UserFactory.create(username=self.USERNAME, password=self.PASSWORD) + CommerceConfiguration.objects.create(cache_ttl=10, enabled=True) self.client.login(username=self.USERNAME, password=self.PASSWORD) self.request = HttpRequest() @@ -508,6 +515,86 @@ class AccountSettingsViewTest(ThirdPartyAuthTestMixin, TestCase): for attribute in self.FIELDS: self.assertIn(attribute, response.content) + def test_header_with_programs_listing_enabled(self): + """ + Verify that tabs header will be shown while program listing is enabled. + """ + self.create_programs_config(program_listing_enabled=True) + view_path = reverse('account_settings') + response = self.client.get(path=view_path) + + self.assertContains(response, '
  • ') + + def test_header_with_programs_listing_disabled(self): + """ + Verify that nav header will be shown while program listing is disabled. + """ + self.create_programs_config(program_listing_enabled=False) + view_path = reverse('account_settings') + response = self.client.get(path=view_path) + + self.assertContains(response, '