Commit 817a1585 by Waheed Ahmed

Merge pull request #12543 from edx/waheed/ecom-2361-complete-order-history-for-students

Complete Order History tab for students on account settings page
parents 6589139e d31b7cbd
"""
Stub implementation of ecommerce service for acceptance tests
"""
import re
import urlparse
from .http import StubHttpRequestHandler, StubHttpService
class StubEcommerceServiceHandler(StubHttpRequestHandler): # pylint: disable=missing-docstring
def do_GET(self): # pylint: disable=invalid-name, missing-docstring
pattern_handlers = {
'/api/v2/orders/$': self.get_orders_list,
}
if self.match_pattern(pattern_handlers):
return
self.send_response(404, content='404 Not Found')
def match_pattern(self, pattern_handlers):
"""
Find the correct handler method given the path info from the HTTP request.
"""
path = urlparse.urlparse(self.path).path
for pattern in pattern_handlers:
match = re.match(pattern, path)
if match:
pattern_handlers[pattern](**match.groupdict())
return True
return None
def get_orders_list(self):
"""
Stubs the orders list endpoint.
"""
orders = {
'results': [
{
'status': 'Complete',
'number': 'Edx-123',
'total_excl_tax': '100.0',
'date_placed': '2016-04-21T23:14:23Z',
'lines': [
{
'title': 'Test Course',
'product': {
'attribute_values': [
{
'name': 'certificate_type',
'value': 'verified'
}
]
}
}
],
}
]
}
orders = self.server.config.get('orders', orders)
self.send_json_response(orders)
class StubEcommerceService(StubHttpService): # pylint: disable=missing-docstring
HANDLER_CLASS = StubEcommerceServiceHandler
......@@ -4,7 +4,9 @@ Command-line utility to start a stub service.
import sys
import time
import logging
from .comments import StubCommentsService
from .ecommerce import StubEcommerceService
from .xqueue import StubXQueueService
from .youtube import StubYouTubeService
from .lti import StubLtiService
......@@ -23,6 +25,7 @@ SERVICES = {
'video': VideoSourceHttpService,
'edxnotes': StubEdxNotesService,
'programs': StubProgramsService,
'ecommerce': StubEcommerceService,
}
# Log to stdout, including debug messages
......
......@@ -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
......
......@@ -63,3 +63,19 @@ class AccountSettingsPage(FieldsMixin, PageObject):
Switch between the different account settings tabs.
"""
self.q(css='#{}'.format(tab_id)).click()
@property
def is_order_history_tab_visible(self):
""" Check if tab with the name "Order History" is visible."""
return self.q(css='.u-field-orderHistory').visible
def get_value_of_order_history_row_item(self, field_id, field_name):
""" Return the text value of the provided order field name."""
query = self.q(css='.u-field-{} .u-field-order-{}'.format(field_id, field_name))
return query.text[0] if query.present else None
def order_button_is_visible(self, field_id):
""" Check that if hovering over the order history row shows the
order detail link or not.
"""
return self.q(css='.u-field-{} .u-field-{}'.format(field_id, 'link')).visible
......@@ -444,6 +444,28 @@ class AccountSettingsPageTest(AccountSettingsTestMixin, WebAppTest):
self.assertEqual(self.account_settings_page.title_for_field(field_id), title)
self.assertEqual(self.account_settings_page.link_title_for_link_field(field_id), link_title)
def test_order_history(self):
"""
Test that we can see orders on Order History tab.
"""
# switch to "Order History" tab
self.account_settings_page.switch_account_settings_tabs('orders-tab')
# verify that we are on correct tab
self.assertTrue(self.account_settings_page.is_order_history_tab_visible)
expected_order_data = {
'title': 'Test Course',
'date': 'Date Placed:\nApr 21, 2016',
'price': 'Cost:\n$100.0',
'number': 'Order Number:\nEdx-123'
}
for field_name, value in expected_order_data.iteritems():
self.assertEqual(
self.account_settings_page.get_value_of_order_history_row_item('order-Edx-123', field_name), value
)
self.assertTrue(self.account_settings_page.order_button_is_visible('order-Edx-123'))
@attr('a11y')
class AccountSettingsA11yTest(AccountSettingsTestMixin, WebAppTest):
......
[
{
"pk": 2,
"model": "commerce.commerceconfiguration",
"fields": {
"enabled": 1,
"change_date": "2016-04-21 10:19:32.034856"
}
}
]
# -*- 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():
......
......@@ -189,6 +189,10 @@ INSTALLED_APPS += ('coursewarehistoryextended',)
BADGING_BACKEND = 'lms.djangoapps.badges.backends.tests.dummy_backend.DummyBackend'
# Configure the LMS to use our stub eCommerce implementation
ECOMMERCE_API_URL = 'http://localhost:8043/api/v2/'
ECOMMERCE_API_SIGNING_KEY = 'ecommerce-key'
#####################################################################
# Lastly, see if the developer has any local overrides.
try:
......
......@@ -52,7 +52,7 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers
var createAccountSettingsPage = function() {
var context = AccountSettingsPage(
FIELDS_DATA, AUTH_DATA, Helpers.USER_ACCOUNTS_API_URL, Helpers.USER_PREFERENCES_API_URL, 'edX'
FIELDS_DATA, [], AUTH_DATA, Helpers.USER_ACCOUNTS_API_URL, Helpers.USER_PREFERENCES_API_URL, 'edX'
);
return context.accountSettingsView;
};
......
......@@ -10,9 +10,18 @@
], function (gettext, $, _, Backbone, Logger, UserAccountModel, UserPreferencesModel,
AccountSettingsFieldViews, AccountSettingsView, StringUtils) {
return function (fieldsData, authData, userAccountsApiUrl, userPreferencesApiUrl, accountUserId, platformName) {
return function (
fieldsData,
ordersHistoryData,
authData,
userAccountsApiUrl,
userPreferencesApiUrl,
accountUserId,
platformName
) {
var accountSettingsElement, userAccountModel, userPreferencesModel, aboutSectionsData,
accountsSectionData, accountSettingsView, showAccountSettingsPage, showLoadingError;
accountsSectionData, ordersSectionData, accountSettingsView, showAccountSettingsPage,
showLoadingError, orderNumber;
accountSettingsElement = $('.wrapper-account-settings');
......@@ -170,13 +179,49 @@
}
];
ordersHistoryData.unshift(
{
'title': gettext('ORDER NAME'),
'order_date': gettext('ORDER PLACED'),
'price': gettext('TOTAL'),
'number': gettext('ORDER NUMBER')
}
);
ordersSectionData = [
{
title: gettext('My Orders'),
subtitle: StringUtils.interpolate(
gettext('This page contains information about orders that you have placed with {platform_name}.'), /* jshint ignore:line */
{platform_name: platformName}
),
fields: _.map(ordersHistoryData, function(order) {
orderNumber = order.number;
if (orderNumber === 'ORDER NUMBER') {
orderNumber = 'orderId';
}
return {
'view': new AccountSettingsFieldViews.OrderHistoryFieldView({
title: order.title,
totalPrice: order.price,
orderId: order.number,
orderDate: order.order_date,
receiptUrl: order.receipt_url,
valueAttribute: 'order-' + orderNumber
})
};
})
}
];
accountSettingsView = new AccountSettingsView({
model: userAccountModel,
accountUserId: accountUserId,
el: accountSettingsElement,
tabSections: {
aboutTabSections: aboutSectionsData,
accountsTabSections: accountsSectionData
accountsTabSections: accountsSectionData,
ordersTabSections: ordersSectionData
},
userPreferencesModel: userPreferencesModel
});
......
......@@ -11,7 +11,9 @@
'text!templates/fields/field_link_account.underscore',
'text!templates/fields/field_dropdown_account.underscore',
'text!templates/fields/field_social_link_account.underscore',
'edx-ui-toolkit/js/utils/string-utils'
'text!templates/fields/field_order_history.underscore',
'edx-ui-toolkit/js/utils/string-utils',
'edx-ui-toolkit/js/utils/html-utils'
], function (
gettext, $, _, Backbone,
FieldViews,
......@@ -20,7 +22,9 @@
field_link_account_template,
field_dropdown_account_template,
field_social_link_template,
StringUtils
field_order_history_template,
StringUtils,
HtmlUtils
)
{
......@@ -224,6 +228,30 @@
return this.indicators.success + gettext('Successfully unlinked.');
}
}),
OrderHistoryFieldView: FieldViews.ReadonlyFieldView.extend({
fieldType: 'orderHistory',
fieldTemplate: field_order_history_template,
initialize: function (options) {
this.options = options;
this._super(options);
this.template = HtmlUtils.template(this.fieldTemplate);
},
render: function () {
HtmlUtils.setHtml(this.$el, this.template({
title: this.options.title,
totalPrice: this.options.totalPrice,
orderId: this.options.orderId,
orderDate: this.options.orderDate,
receiptUrl: this.options.receiptUrl,
valueAttribute: this.options.valueAttribute
}));
this.delegateEvents();
return this;
}
})
};
return AccountSettingsFieldViews;
......
......@@ -15,7 +15,8 @@
activeTab: 'aboutTabSections',
accountSettingsTabs: [
{name: 'aboutTabSections', id: 'about-tab', label: gettext('Account Information'), class: 'active'},
{name: 'accountsTabSections', id: 'accounts-tab', label: gettext('Linked Accounts')}
{name: 'accountsTabSections', id: 'accounts-tab', label: gettext('Linked Accounts')},
{name: 'ordersTabSections', id: 'orders-tab', label: gettext('Order History')}
],
events: {
'click .account-nav-link': 'changeTab'
......
......@@ -213,6 +213,7 @@ $dark-gray1: rgb(74,74,74);
$light-gray1: rgb(242,242,242);
$light-gray2: rgb(171,171,171);
$light-gray3: rgb(249,249,249);
$light-gray4: rgb(252,252,252);
$dark-gray2: rgb(151,151,151);
$blue1: rgb(74,144,226);
$blue2: rgb(0,161,229);
......
......@@ -193,6 +193,54 @@
}
}
.u-field-order {
display: flex;
align-items: center;
font-size: em(16);
color: $gray;
width: 100%;
padding-top: $baseline;
padding-bottom: $baseline;
line-height: normal;
span {
padding: $baseline;
}
.u-field-order-title {
@include float(left);
width: 26%;
font-size: em(20);
padding-left: ($baseline*2);
}
.u-field-order-value {
@include float(left);
width: 12%;
}
.u-field-order-date {
@include float(left);
width: 16%;
}
.u-field-order-link {
width: 15%;
padding: 0;
.u-field-link {
@extend %ui-clear-button;
@extend %btn-pl-default-base;
@include font-size(14);
border: 1px solid $blue;
color: $blue;
line-height: normal;
padding: 10px;
width: 110px;
}
}
}
.social-field-linked {
background: $m-gray-l4;
box-shadow: 0 1px 2px 1px $shadow-l2;
......@@ -267,6 +315,42 @@
}
}
.u-field-orderHistory {
border-bottom: none;
border: 1px solid $m-gray-l4;
margin-bottom: $baseline;
padding: 0;
&:last-child {
border-bottom: 1px solid $m-gray-l4;
}
&:hover, &:focus {
background-color: $light-gray4;
}
}
.u-field-order-orderId {
border: none;
margin-top: $baseline;
margin-bottom: 0;
padding-bottom: 0;
&:hover, &:focus {
background-color: transparent;
}
.u-field-order {
font-weight: $font-semibold;
padding-top: 0;
padding-bottom: 0;
.u-field-order-title {
font-size: em(16);
}
}
}
.u-field-social {
border-bottom: none;
margin-right: 20px;
......
<div class="field u-field-order" <% if (receiptUrl) { %> role="group" aria-labelledby="order-title-<%- orderId %>" <% } else { %> aria-hidden="true" <% } %>>
<span class="u-field-order-title" <% if (receiptUrl) { %> id="order-title-<%- orderId %>" <% } %>><%- title %></span>
<span class="u-field-order-date"><span class="sr">Date Placed: </span><%- orderDate %></span>
<span class="u-field-order-value u-field-order-price"><span class="sr">Cost: </span><% if (!isNaN(parseFloat(totalPrice))) { %>$<% } %><%- totalPrice %></span>
<span class="u-field-order-value u-field-order-number"><span class="sr">Order Number: </span><%- orderId %></span>
<span class="u-field-order-link">
<% if (receiptUrl) { %>
<a class="u-field-link" target="_blank" href="<%- receiptUrl %>"><%- gettext('Order Details') %><span class="sr"> for <%- orderId %></span></a>
<% } %>
</span>
</div>
......@@ -32,12 +32,14 @@ from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_str
<%block name="js_extra">
<%static:require_module module_name="js/student_account/views/account_settings_factory" class_name="AccountSettingsFactory">
var fieldsData = ${ fields | n, dump_js_escaped_json };
var authData = ${ auth | n, dump_js_escaped_json };
var platformName = '${ static.get_platform_name() | n, js_escaped_string }';
var fieldsData = ${ fields | n, dump_js_escaped_json },
ordersHistoryData = ${ order_history | n, dump_js_escaped_json },
authData = ${ auth | n, dump_js_escaped_json },
platformName = '${ static.get_platform_name() | n, js_escaped_string }';
AccountSettingsFactory(
fieldsData,
ordersHistoryData,
authData,
'${ user_accounts_api_url | n, js_escaped_string }',
'${ user_preferences_api_url | n, js_escaped_string }',
......
<% _.each(sections, function(section) { %>
<div class="section">
<% if (section.subtitle) { %>
<p id="header-subtitle-<%- activeTabName %>" class="account-settings-header-subtitle"><%- gettext(section.subtitle) %></p>
<p id="header-subtitle-<%- activeTabName %>" class="account-settings-header-subtitle"><%- section.subtitle %></p>
<% } %>
<h3 class="section-header"><%- gettext(section.title) %></h3>
<div class="account-settings-section-body">
......
......@@ -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)
......@@ -106,6 +106,11 @@ class Env(object):
'programs': {
'port': 8090,
'log': BOK_CHOY_LOG_DIR / "bok_choy_programs.log",
},
'ecommerce': {
'port': 8043,
'log': BOK_CHOY_LOG_DIR / "bok_choy_ecommerce.log",
}
}
......
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