From b2f21ef4f3ae42a2d0a1ee4facca23c09b41f904 Mon Sep 17 00:00:00 2001 From: Waheed Ahmed Date: Mon, 23 May 2016 18:50:16 +0500 Subject: [PATCH 1/2] Complete Order History tab for students on account settings page. ECOM-2361 --- common/djangoapps/terrain/stubs/ecommerce.py | 64 ++++++++++++++ common/djangoapps/terrain/stubs/start.py | 3 + .../acceptance/pages/lms/account_settings.py | 16 ++++ .../tests/lms/test_account_settings.py | 22 +++++ common/test/db_fixtures/commerce_config.json | 10 +++ lms/envs/bok_choy.py | 4 + .../account_settings_factory_spec.js | 2 +- .../views/account_settings_factory.js | 51 ++++++++++- .../views/account_settings_fields.js | 32 ++++++- .../views/account_settings_view.js | 3 +- lms/static/sass/base/_variables.scss | 1 + lms/static/sass/views/_account-settings.scss | 84 +++++++++++++++++++ .../fields/field_order_history.underscore | 11 +++ .../student_account/account_settings.html | 8 +- .../account_settings_section.underscore | 2 +- pavelib/utils/envs.py | 5 ++ 16 files changed, 307 insertions(+), 11 deletions(-) create mode 100644 common/djangoapps/terrain/stubs/ecommerce.py create mode 100644 common/test/db_fixtures/commerce_config.json create mode 100644 lms/templates/fields/field_order_history.underscore diff --git a/common/djangoapps/terrain/stubs/ecommerce.py b/common/djangoapps/terrain/stubs/ecommerce.py new file mode 100644 index 0000000000..082b09e6cf --- /dev/null +++ b/common/djangoapps/terrain/stubs/ecommerce.py @@ -0,0 +1,64 @@ +""" +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 diff --git a/common/djangoapps/terrain/stubs/start.py b/common/djangoapps/terrain/stubs/start.py index a5bee75f3e..ad6d826eac 100644 --- a/common/djangoapps/terrain/stubs/start.py +++ b/common/djangoapps/terrain/stubs/start.py @@ -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 diff --git a/common/test/acceptance/pages/lms/account_settings.py b/common/test/acceptance/pages/lms/account_settings.py index 33d852de43..d3e6e38405 100644 --- a/common/test/acceptance/pages/lms/account_settings.py +++ b/common/test/acceptance/pages/lms/account_settings.py @@ -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 diff --git a/common/test/acceptance/tests/lms/test_account_settings.py b/common/test/acceptance/tests/lms/test_account_settings.py index 09d1ec457c..02b8c84889 100644 --- a/common/test/acceptance/tests/lms/test_account_settings.py +++ b/common/test/acceptance/tests/lms/test_account_settings.py @@ -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): diff --git a/common/test/db_fixtures/commerce_config.json b/common/test/db_fixtures/commerce_config.json new file mode 100644 index 0000000000..473546f88c --- /dev/null +++ b/common/test/db_fixtures/commerce_config.json @@ -0,0 +1,10 @@ +[ + { + "pk": 2, + "model": "commerce.commerceconfiguration", + "fields": { + "enabled": 1, + "change_date": "2016-04-21 10:19:32.034856" + } + } +] diff --git a/lms/envs/bok_choy.py b/lms/envs/bok_choy.py index 91f3a4e1d6..724506dd45 100644 --- a/lms/envs/bok_choy.py +++ b/lms/envs/bok_choy.py @@ -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: diff --git a/lms/static/js/spec/student_account/account_settings_factory_spec.js b/lms/static/js/spec/student_account/account_settings_factory_spec.js index 87a90e01c2..713a237b2d 100644 --- a/lms/static/js/spec/student_account/account_settings_factory_spec.js +++ b/lms/static/js/spec/student_account/account_settings_factory_spec.js @@ -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; }; diff --git a/lms/static/js/student_account/views/account_settings_factory.js b/lms/static/js/student_account/views/account_settings_factory.js index 4a64b1a097..f3613a4e95 100644 --- a/lms/static/js/student_account/views/account_settings_factory.js +++ b/lms/static/js/student_account/views/account_settings_factory.js @@ -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 }); diff --git a/lms/static/js/student_account/views/account_settings_fields.js b/lms/static/js/student_account/views/account_settings_fields.js index 5031de7ab6..6cd797985a 100644 --- a/lms/static/js/student_account/views/account_settings_fields.js +++ b/lms/static/js/student_account/views/account_settings_fields.js @@ -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; diff --git a/lms/static/js/student_account/views/account_settings_view.js b/lms/static/js/student_account/views/account_settings_view.js index b3b80a5edd..bf55d19382 100644 --- a/lms/static/js/student_account/views/account_settings_view.js +++ b/lms/static/js/student_account/views/account_settings_view.js @@ -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' diff --git a/lms/static/sass/base/_variables.scss b/lms/static/sass/base/_variables.scss index 8a10d5845f..36ff69b2f4 100644 --- a/lms/static/sass/base/_variables.scss +++ b/lms/static/sass/base/_variables.scss @@ -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); diff --git a/lms/static/sass/views/_account-settings.scss b/lms/static/sass/views/_account-settings.scss index 88fff0224e..de6cde161c 100644 --- a/lms/static/sass/views/_account-settings.scss +++ b/lms/static/sass/views/_account-settings.scss @@ -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; diff --git a/lms/templates/fields/field_order_history.underscore b/lms/templates/fields/field_order_history.underscore new file mode 100644 index 0000000000..4d2daedce2 --- /dev/null +++ b/lms/templates/fields/field_order_history.underscore @@ -0,0 +1,11 @@ +
role="group" aria-labelledby="order-title-<%- orderId %>" <% } else { %> aria-hidden="true" <% } %>> + id="order-title-<%- orderId %>" <% } %>><%- title %> + Date Placed: <%- orderDate %> + Cost: <% if (!isNaN(parseFloat(totalPrice))) { %>$<% } %><%- totalPrice %> + Order Number: <%- orderId %> + + <% if (receiptUrl) { %> + <%- gettext('Order Details') %> for <%- orderId %> + <% } %> + +
diff --git a/lms/templates/student_account/account_settings.html b/lms/templates/student_account/account_settings.html index 5580a9b0ed..c8653c0a9b 100644 --- a/lms/templates/student_account/account_settings.html +++ b/lms/templates/student_account/account_settings.html @@ -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 }', diff --git a/lms/templates/student_account/account_settings_section.underscore b/lms/templates/student_account/account_settings_section.underscore index e6f763d21d..0370c6ad51 100644 --- a/lms/templates/student_account/account_settings_section.underscore +++ b/lms/templates/student_account/account_settings_section.underscore @@ -1,7 +1,7 @@ <% _.each(sections, function(section) { %>
<% if (section.subtitle) { %> - + <% } %>

<%- gettext(section.title) %>