Merge pull request #16135 from edx/LEARNER-2412
LEARNER-2412 Use local currency in LMS
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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:',
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
<div id="currency_data" value='{"CAN": {"rate": 2.2, "code": "CAD", "symbol": "$"}}'></div>
|
||||
<input type="submit" name="verified_mode" value="Pursue a Verified Certificate ($100 USD)">
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
<%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
|
||||
});
|
||||
</script>
|
||||
</%block>
|
||||
<%static:webpack entry="Currency">
|
||||
new Currency();
|
||||
</%static:webpack>
|
||||
|
||||
<%block name="content">
|
||||
% if error:
|
||||
@@ -72,6 +76,7 @@ from openedx.core.djangolib.markup import HTML, Text
|
||||
</div>
|
||||
%endif
|
||||
|
||||
<div id="currency_data" value="${currency_data}"></div>
|
||||
<div class="container">
|
||||
<section class="wrapper">
|
||||
<div class="wrapper-register-choose wrapper-content-main">
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user