diff --git a/common/djangoapps/course_modes/views.py b/common/djangoapps/course_modes/views.py index 6ca76524f4..316558884e 100644 --- a/common/djangoapps/course_modes/views.py +++ b/common/djangoapps/course_modes/views.py @@ -5,6 +5,7 @@ Views for the course_mode module import decimal import urllib +import waffle from babel.dates import format_datetime from django.contrib.auth.decorators import login_required from django.core.urlresolvers import reverse @@ -23,6 +24,7 @@ from courseware.access import has_access from edxmako.shortcuts import render_to_response from lms.djangoapps.commerce.utils import EcommerceService from lms.djangoapps.experiments.utils import get_experiment_user_metadata_context +from openedx.core.djangoapps.catalog.utils import get_currency_data from openedx.core.djangoapps.embargo import api as embargo_api from student.models import CourseEnrollment from third_party_auth.decorators import tpa_hint_ends_existing_session @@ -185,6 +187,11 @@ class ChooseModeView(View): context["sku"] = verified_mode.sku context["bulk_sku"] = verified_mode.bulk_sku + context['currency_data'] = [] + if waffle.switch_is_active('local_currency'): + if 'edx-price-l10n' not in request.COOKIES: + context['currency_data'] = get_currency_data() + return render_to_response("course_modes/choose.html", context) @method_decorator(tpa_hint_ends_existing_session) diff --git a/lms/static/lms/js/build.js b/lms/static/lms/js/build.js index b4a74f5a52..3ca49e4f8a 100644 --- a/lms/static/lms/js/build.js +++ b/lms/static/lms/js/build.js @@ -84,6 +84,7 @@ 'URI': 'empty:', 'common/js/discussion/views/discussion_inline_view': 'empty:', 'modernizr': 'empty', + 'which-country': 'empty', // Don't bundle UI Toolkit helpers as they are loaded into the "edx" namespace 'edx-ui-toolkit/js/utils/html-utils': 'empty:', diff --git a/openedx/core/djangoapps/catalog/tests/test_utils.py b/openedx/core/djangoapps/catalog/tests/test_utils.py index a3c08d8481..5cd9f42e28 100644 --- a/openedx/core/djangoapps/catalog/tests/test_utils.py +++ b/openedx/core/djangoapps/catalog/tests/test_utils.py @@ -16,6 +16,7 @@ from openedx.core.djangoapps.catalog.tests.mixins import CatalogIntegrationMixin from openedx.core.djangoapps.catalog.utils import ( get_course_runs, get_course_run_details, + get_currency_data, get_program_types, get_programs, get_programs_with_type @@ -237,6 +238,29 @@ class TestGetProgramTypes(CatalogIntegrationMixin, TestCase): self.assertEqual(data, program) +@mock.patch(UTILS_MODULE + '.get_edx_api_data') +class TestGetCurrency(CatalogIntegrationMixin, TestCase): + """Tests covering retrieval of currency data from the catalog service.""" + @override_settings(COURSE_CATALOG_API_URL='https://api.example.com/v1/') + def test_get_currency_data(self, mock_get_edx_api_data): + """Verify get_currency_data returns the currency data.""" + currency_data = { + "code": "CAD", + "rate": 1.257237, + "symbol": "$" + } + mock_get_edx_api_data.return_value = currency_data + + # Catalog integration is disabled. + data = get_currency_data() + self.assertEqual(data, []) + + catalog_integration = self.create_catalog_integration() + UserFactory(username=catalog_integration.service_username) + data = get_currency_data() + self.assertEqual(data, currency_data) + + @skip_unless_lms @mock.patch(UTILS_MODULE + '.get_edx_api_data') class TestGetCourseRuns(CatalogIntegrationMixin, TestCase): diff --git a/openedx/core/djangoapps/catalog/utils.py b/openedx/core/djangoapps/catalog/utils.py index efa4951bce..eecd118c79 100644 --- a/openedx/core/djangoapps/catalog/utils.py +++ b/openedx/core/djangoapps/catalog/utils.py @@ -119,6 +119,29 @@ def get_program_types(name=None): return [] +def get_currency_data(): + """Retrieve currency data from the catalog service. + + Returns: + list of dict, representing program types. + dict, if a specific program type is requested. + """ + catalog_integration = CatalogIntegration.current() + if catalog_integration.enabled: + try: + user = catalog_integration.get_service_user() + except ObjectDoesNotExist: + return [] + + api = create_catalog_api_client(user) + cache_key = '{base}.currency'.format(base=catalog_integration.CACHE_KEY) + + return get_edx_api_data(catalog_integration, 'currency', api=api, + cache_key=cache_key if catalog_integration.is_cache_enabled else None) + else: + return [] + + def get_programs_with_type(site, include_hidden=True): """ Return the list of programs. You can filter the types of programs returned by using the optional diff --git a/openedx/features/course_experience/static/course_experience/fixtures/course-currency-fragment.html b/openedx/features/course_experience/static/course_experience/fixtures/course-currency-fragment.html new file mode 100644 index 0000000000..9a65d28651 --- /dev/null +++ b/openedx/features/course_experience/static/course_experience/fixtures/course-currency-fragment.html @@ -0,0 +1,2 @@ +
+ diff --git a/openedx/features/course_experience/static/course_experience/js/currency.js b/openedx/features/course_experience/static/course_experience/js/currency.js new file mode 100644 index 0000000000..be1d37f6e6 --- /dev/null +++ b/openedx/features/course_experience/static/course_experience/js/currency.js @@ -0,0 +1,78 @@ +import whichCountry from 'which-country'; +import 'jquery.cookie'; +import $ from 'jquery'; // eslint-disable-line import/extensions + +export class Currency { // eslint-disable-line import/prefer-default-export + + setCookie(countryCode, l10nData) { + function pick(curr, arr) { + const obj = {}; + arr.forEach((key) => { + obj[key] = curr[key]; + }); + return obj; + } + const userCountryData = pick(l10nData, [countryCode]); + let countryL10nData = userCountryData[countryCode]; + + if (countryL10nData) { + countryL10nData.countryCode = countryCode; + } else { + countryL10nData = { + countryCode: 'USA', + symbol: '$', + rate: '1', + code: 'USD', + }; + } + this.countryL10nData = countryL10nData; + $.cookie('edx-price-l10n', JSON.stringify(countryL10nData), { + expires: 1, + }); + } + + setPrice() { + const l10nCookie = this.countryL10nData; + const lmsregex = /(\$)(\d*)( USD)/g; + const price = $('input[name="verified_mode"]').filter(':visible')[0]; + const regexMatch = lmsregex.exec(price.value); + const dollars = parseFloat(regexMatch[2]); + const converted = dollars * l10nCookie.rate; + const string = `${l10nCookie.symbol}${Math.round(converted)} ${l10nCookie.code}`; + // Use regex to change displayed price on track selection + // based on edx-price-l10n cookie currency_data + price.value = price.value.replace(regexMatch[0], string); + } + + getL10nData(countryCode) { + const l10nData = JSON.parse($('#currency_data').attr('value')); + if (l10nData) { + this.setCookie(countryCode, l10nData); + } + } + + getCountry(position) { + const countryCode = whichCountry([position.coords.longitude, position.coords.latitude]); + this.countryL10nData = JSON.parse($.cookie('edx-price-l10n')); + + if (countryCode) { + if (!(this.countryL10nData && this.countryL10nData.countryCode === countryCode)) { + // If pricing cookie has not been set or the country is not correct + // Make API call and set the cookie + this.getL10nData(countryCode); + } + } + this.setPrice(); + } + + getUserLocation() { + // Get user location from browser + navigator.geolocation.getCurrentPosition(this.getCountry.bind(this)); + } + + constructor(skipInitialize) { + if (!skipInitialize) { + this.getUserLocation(); + } + } +} diff --git a/openedx/features/course_experience/static/course_experience/js/spec/Currency_spec.js b/openedx/features/course_experience/static/course_experience/js/spec/Currency_spec.js new file mode 100644 index 0000000000..8e6db6b8f0 --- /dev/null +++ b/openedx/features/course_experience/static/course_experience/js/spec/Currency_spec.js @@ -0,0 +1,64 @@ +/* globals loadFixtures */ + +import $ from 'jquery'; // eslint-disable-line import/extensions +import { Currency } from '../currency'; + +describe('Currency factory', () => { + let currency; + let canadaPosition; + let usaPosition; + let japanPosition; + + beforeEach(() => { + loadFixtures('course_experience/fixtures/course-currency-fragment.html'); + currency = new Currency(true); + canadaPosition = { + coords: { + latitude: 58.773884, + longitude: -124.882581, + }, + }; + usaPosition = { + coords: { + latitude: 42.366202, + longitude: -71.973095, + }, + }; + japanPosition = { + coords: { + latitude: 35.857826, + longitude: 137.737495, + }, + }; + $.cookie('edx-price-l10n', null, { path: '/' }); + }); + + describe('converts price to local currency', () => { + it('when location is US', () => { + currency.getCountry(usaPosition); + expect($('input[name="verified_mode"]').filter(':visible')[0].value).toEqual('Pursue a Verified Certificate ($100 USD)'); + }); + + it('when location is an unsupported country', () => { + currency.getCountry(japanPosition); + expect($('input[name="verified_mode"]').filter(':visible')[0].value).toEqual('Pursue a Verified Certificate ($100 USD)'); + }); + + it('when cookie is not set and country is supported', () => { + currency.getCountry(canadaPosition); + expect($('input[name="verified_mode"]').filter(':visible')[0].value).toEqual('Pursue a Verified Certificate ($220 CAD)'); + }); + + it('when cookie is set to same country', () => { + currency.getCountry(canadaPosition); + $.cookie('edx-price-l10n', '{"rate":2.2,"code":"CAD","symbol":"$","countryCode":"CAN"}', { expires: 1 }); + expect($('input[name="verified_mode"]').filter(':visible')[0].value).toEqual('Pursue a Verified Certificate ($220 CAD)'); + }); + + it('when cookie is set to different country', () => { + currency.getCountry(canadaPosition); + $.cookie('edx-price-l10n', '{"rate":1,"code":"USD","symbol":"$","countryCode":"USA"}', { expires: 1 }); + expect($('input[name="verified_mode"]').filter(':visible')[0].value).toEqual('Pursue a Verified Certificate ($220 CAD)'); + }); + }); +}); diff --git a/package.json b/package.json index d4c241246d..4d2a900cb9 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,8 @@ "underscore": "~1.8.3", "underscore.string": "~3.3.4", "webpack": "^2.2.1", - "webpack-bundle-tracker": "^0.2.0" + "webpack-bundle-tracker": "^0.2.0", + "which-country": "1.0.0" }, "devDependencies": { "edx-custom-a11y-rules": "0.1.3", diff --git a/pavelib/assets.py b/pavelib/assets.py index a199ff8adb..afca91b335 100644 --- a/pavelib/assets.py +++ b/pavelib/assets.py @@ -67,7 +67,8 @@ NPM_INSTALLED_LIBRARIES = [ '@edx/studio-frontend/dist/assets.min.js', '@edx/studio-frontend/dist/assets.min.js.map', '@edx/studio-frontend/dist/studio-frontend.min.css', - '@edx/studio-frontend/dist/studio-frontend.min.css.map' + '@edx/studio-frontend/dist/studio-frontend.min.css.map', + 'which-country/index.js' ] # A list of NPM installed developer libraries that should be copied into the common diff --git a/themes/edx.org/lms/templates/course_modes/choose.html b/themes/edx.org/lms/templates/course_modes/choose.html index 2dc5eee275..3e6b60fc07 100644 --- a/themes/edx.org/lms/templates/course_modes/choose.html +++ b/themes/edx.org/lms/templates/course_modes/choose.html @@ -7,6 +7,7 @@ from openedx.core.djangolib.js_utils import js_escaped_string from openedx.core.djangolib.markup import HTML, Text %> +<%namespace name='static' file='/static_content.html'/> <%block name="bodyclass">register verification-process step-select-track <%block name="pagetitle"> ${_("Enroll In {course_name} | Choose Your Track").format(course_name=course_name)} @@ -56,6 +57,9 @@ from openedx.core.djangolib.markup import HTML, Text }); +<%static:webpack entry="Currency"> + new Currency(); + <%block name="content"> % if error: @@ -72,6 +76,7 @@ from openedx.core.djangolib.markup import HTML, Text %endif +
diff --git a/webpack.config.js b/webpack.config.js index 6e56c1ef03..afd6ce26f6 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -28,6 +28,7 @@ var wpconfig = { CourseOutline: './openedx/features/course_experience/static/course_experience/js/CourseOutline.js', CourseSock: './openedx/features/course_experience/static/course_experience/js/CourseSock.js', CourseTalkReviews: './openedx/features/course_experience/static/course_experience/js/CourseTalkReviews.js', + Currency: './openedx/features/course_experience/static/course_experience/js/currency.js', Enrollment: './openedx/features/course_experience/static/course_experience/js/Enrollment.js', LatestUpdate: './openedx/features/course_experience/static/course_experience/js/LatestUpdate.js', WelcomeMessage: './openedx/features/course_experience/static/course_experience/js/WelcomeMessage.js'