Commit d31b7cbd by Ahsan Ulhaq Committed by Waheed Ahmed

Complete Order History area for students

ECOM-2361
parent b2f21ef4
......@@ -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
......
# -*- 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),
),
]
......@@ -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
""" 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()
......@@ -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/'
......@@ -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,
......
......@@ -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, '<li class="tab-nav-item">')
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, '<li class="item nav-global-01">')
def test_commerce_order_detail(self):
with mock_get_orders():
order_detail = get_user_orders(self.user)
user_order = mock_get_orders.default_response['results'][0]
expected = [
{
'number': user_order['number'],
'price': user_order['total_excl_tax'],
'title': user_order['lines'][0]['title'],
'order_date': 'Jan 01, 2016',
'receipt_url': '/commerce/checkout/receipt/?orderNum=' + user_order['number']
}
]
self.assertEqual(order_detail, expected)
def test_commerce_order_detail_exception(self):
with mock_get_orders(exception=exceptions.HttpNotFoundError):
order_detail = get_user_orders(self.user)
self.assertEqual(order_detail, [])
def test_incomplete_order_detail(self):
response = {
'results': [
factories.OrderFactory(
status='Incomplete',
lines=[
factories.OrderLineFactory(
product=factories.ProductFactory(attribute_values=[factories.ProductAttributeFactory()])
)
]
)
]
}
with mock_get_orders(response=response):
order_detail = get_user_orders(self.user)
self.assertEqual(order_detail, [])
def test_honor_course_order_detail(self):
response = {
'results': [
factories.OrderFactory(
lines=[
factories.OrderLineFactory(
product=factories.ProductFactory(attribute_values=[factories.ProductAttributeFactory(
name='certificate_type',
value='honor'
)])
)
]
)
]
}
with mock_get_orders(response=response):
order_detail = get_user_orders(self.user)
self.assertEqual(order_detail, [])
@override_settings(SITE_NAME=settings.MICROSITE_LOGISTRATION_HOSTNAME)
class MicrositeLogistrationTests(TestCase):
......
......@@ -3,28 +3,35 @@
import logging
import json
import urlparse
from datetime import datetime
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.core.urlresolvers import reverse, resolve
from django.http import (
HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, HttpRequest
)
from django.shortcuts import redirect
from django.http import HttpRequest
from django_countries import countries
from django.core.urlresolvers import reverse, resolve
from django.utils.translation import ugettext as _
from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.decorators.http import require_http_methods
from lang_pref.api import released_languages, all_languages
from django_countries import countries
from edxmako.shortcuts import render_to_response
import pytz
from commerce.models import CommerceConfiguration
from external_auth.login_and_register import (
login as external_auth_login,
register as external_auth_register
)
from lang_pref.api import released_languages, all_languages
from openedx.core.djangoapps.commerce.utils import ecommerce_api_client
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.djangoapps.theming.helpers import is_request_in_themed_site, get_value as get_themed_value
from openedx.core.djangoapps.user_api.accounts.api import request_password_change
from openedx.core.djangoapps.user_api.errors import UserNotFound
from openedx.core.lib.edx_api_utils import get_edx_api_data
from student.models import UserProfile
from student.views import (
signin_user as old_login_view,
......@@ -35,13 +42,14 @@ import third_party_auth
from third_party_auth import pipeline
from third_party_auth.decorators import xframe_allow_whitelisted
from util.bad_request_rate_limiter import BadRequestRateLimiter
from openedx.core.djangoapps.theming.helpers import is_request_in_themed_site, get_value as get_themed_value
from openedx.core.djangoapps.user_api.accounts.api import request_password_change
from openedx.core.djangoapps.user_api.errors import UserNotFound
from util.date_utils import strftime_localized
AUDIT_LOG = logging.getLogger("audit")
log = logging.getLogger(__name__)
@require_http_methods(['GET'])
......@@ -301,6 +309,50 @@ def _external_auth_intercept(request, mode):
return external_auth_register(request)
def get_user_orders(user):
"""Given a user, get the detail of all the orders from the Ecommerce service.
Arguments:
user (User): The user to authenticate as when requesting ecommerce.
Returns:
list of dict, representing orders returned by the Ecommerce service.
"""
no_data = []
user_orders = []
allowed_course_modes = ['professional', 'verified', 'credit']
commerce_configuration = CommerceConfiguration.current()
user_query = {'username': user.username}
use_cache = commerce_configuration.is_cache_enabled
cache_key = commerce_configuration.CACHE_KEY + '.' + str(user.id) if use_cache else None
api = ecommerce_api_client(user)
commerce_user_orders = get_edx_api_data(
commerce_configuration, user, 'orders', api=api, querystring=user_query, cache_key=cache_key
)
for order in commerce_user_orders:
if order['status'].lower() == 'complete':
for line in order['lines']:
for attribute in line['product']['attribute_values']:
if attribute['name'] == 'certificate_type' and attribute['value'] in allowed_course_modes:
try:
date_placed = datetime.strptime(order['date_placed'], "%Y-%m-%dT%H:%M:%SZ")
order_data = {
'number': order['number'],
'price': order['total_excl_tax'],
'title': order['lines'][0]['title'],
'order_date': strftime_localized(date_placed.replace(tzinfo=pytz.UTC), 'SHORT_DATE'),
'receipt_url': commerce_configuration.receipt_page + order['number']
}
user_orders.append(order_data)
except KeyError:
log.exception('Invalid order structure: %r', order)
return no_data
return user_orders
@login_required
@require_http_methods(['GET'])
def account_settings(request):
......@@ -394,6 +446,8 @@ def account_settings_context(request):
'user_accounts_api_url': reverse("accounts_api", kwargs={'username': user.username}),
'user_preferences_api_url': reverse('preferences_api', kwargs={'username': user.username}),
'disable_courseware_js': True,
'show_program_listing': ProgramsApiConfig.current().show_program_listing,
'order_history': get_user_orders(user)
}
if third_party_auth.is_enabled():
......
......@@ -11,7 +11,8 @@ from openedx.core.lib.token_utils import get_id_token
log = logging.getLogger(__name__)
def get_edx_api_data(api_config, user, resource, resource_id=None, querystring=None, cache_key=None):
def get_edx_api_data(api_config, user, resource,
api=None, resource_id=None, querystring=None, cache_key=None):
"""GET data from an edX REST API.
DRY utility for handling caching and pagination.
......@@ -22,6 +23,7 @@ def get_edx_api_data(api_config, user, resource, resource_id=None, querystring=N
resource (str): Name of the API resource being requested.
Keyword Arguments:
api (APIClient): API client to use for requesting data.
resource_id (int or str): Identifies a specific resource to be retrieved.
querystring (dict): Optional query string parameters.
cache_key (str): Where to cache retrieved data. The cache will be ignored if this is omitted
......@@ -45,8 +47,9 @@ def get_edx_api_data(api_config, user, resource, resource_id=None, querystring=N
return cached
try:
jwt = get_id_token(user, api_config.OAUTH2_CLIENT_NAME)
api = EdxRestApiClient(api_config.internal_api_url, jwt=jwt)
if not api:
jwt = get_id_token(user, api_config.OAUTH2_CLIENT_NAME)
api = EdxRestApiClient(api_config.internal_api_url, jwt=jwt)
except: # pylint: disable=bare-except
log.exception('Failed to initialize the %s API client.', api_config.API_NAME)
return no_data
......
......@@ -3,12 +3,14 @@ import json
import unittest
from django.core.cache import cache
from django.test.utils import override_settings
import httpretty
import mock
from nose.plugins.attrib import attr
from edx_oauth2_provider.tests.factories import ClientFactory
from provider.constants import CONFIDENTIAL
from openedx.core.djangoapps.commerce.utils import ecommerce_api_client
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase
......@@ -17,6 +19,8 @@ from student.tests.factories import UserFactory
UTILITY_MODULE = 'openedx.core.lib.edx_api_utils'
TEST_API_URL = 'http://www-internal.example.com/api'
TEST_API_SIGNING_KEY = 'edx'
@attr('shard_2')
......@@ -195,3 +199,15 @@ class TestGetEdxApiData(ProgramsApiConfigMixin, CacheIsolationTestCase):
self.assertTrue(mock_exception.called)
self.assertEqual(actual, [])
@override_settings(JWT_AUTH={'JWT_ISSUER': 'http://example.com/oauth', 'JWT_EXPIRATION': 30},
ECOMMERCE_API_SIGNING_KEY=TEST_API_SIGNING_KEY, ECOMMERCE_API_URL=TEST_API_URL)
def test_client_passed(self):
""" Verify that when API client is passed edx_rest_api_client is not
used.
"""
program_config = self.create_programs_config()
api = ecommerce_api_client(self.user)
with mock.patch('openedx.core.lib.edx_api_utils.EdxRestApiClient.__init__') as mock_init:
get_edx_api_data(program_config, self.user, 'orders', api=api)
self.assertFalse(mock_init.called)
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