Skip to content

Commit

Permalink
Localized Country Names, Numbers and Dates
Browse files Browse the repository at this point in the history
- Updated map and graphs, tables, and metrics.
- Enrollment geography page now uses translated country names from django-countries
- Using jQuery Globalize for client-side number formatting
  • Loading branch information
Clinton Blackburn committed Oct 6, 2014
1 parent 140e529 commit 61970b2
Show file tree
Hide file tree
Showing 28 changed files with 246 additions and 56 deletions.
6 changes: 6 additions & 0 deletions .bowerrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"directory": "analytics_dashboard/static/bower_components",
"scripts": {
"postinstall": "./node_modules/cldr-data-downloader/bin/download.js -i analytics_dashboard/static/bower_components/cldr-data/index.json -o analytics_dashboard/static/bower_components/cldr-data/"
}
}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Desktop.ini
.idea/
analytics_dashboard/assets/
node_modules/
analytics_dashboard/static/bower_components/
.coverage
nosetests.xml
reports/
Expand Down
7 changes: 6 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
.PHONY: requirements

ROOT = $(shell echo "$$PWD")
COVERAGE = $(ROOT)/build/coverage
PACKAGES = analytics_dashboard courses

requirements:
pip install -q -r requirements/base.txt --exists-action w
npm install
bower install

test.requirements: requirements
pip install -q -r requirements/test.txt --exists-action w
Expand Down Expand Up @@ -43,7 +47,8 @@ validate_python: test.requirements test_python quality

validate_js:
npm install
gulp
gulp test
gulp lint

validate: validate_python validate_js

Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ Dashboard to display course analytics to course teams
Prerequisites
-------------
* Python 2.7.x (not tested with Python 3.x)
* [gettext](http://www.gnu.org/software/gettext/)
* [gettext](http://www.gnu.org/software/gettext/)
* [npm](https://www.npmjs.org/)

Getting Started
---------------
1. Get the code (e.g. clone the repository).
2. Install the Python requirements:
2. Install the Python/Node/Bower requirements:

$ make develop

Expand Down
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DATE_FORMAT = 'F d, Y'
5 changes: 4 additions & 1 deletion analytics_dashboard/analytics_dashboard/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@

# See: https://docs.djangoproject.com/en/dev/ref/settings/#use-tz
USE_TZ = True

FORMAT_MODULE_PATH = 'analytics_dashboard.formats'
########## END GENERAL CONFIGURATION


Expand Down Expand Up @@ -210,7 +212,8 @@

# Admin panel and documentation:
'django.contrib.admin',
'waffle'
'waffle',
'django_countries',
)

# Apps specific for this project go here.
Expand Down
36 changes: 30 additions & 6 deletions analytics_dashboard/courses/presenters.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import datetime
import logging

from django.utils.translation import ugettext as _
from django.conf import settings

from django_countries import countries
from waffle import switch_is_active
from analyticsclient.client import Client
import analyticsclient.constants.activity_type as AT
from analyticsclient.constants import demographic
from analyticsclient.constants import UNKNOWN_COUNTRY_CODE
from analyticsclient.constants import demographic, UNKNOWN_COUNTRY_CODE

from waffle import switch_is_active

logger = logging.getLogger(__name__)
COUNTRIES = dict(countries)


class BasePresenter(object):
Expand Down Expand Up @@ -118,6 +122,23 @@ def get_summary_and_trend_data(self):
summary = self._build_summary(trends)
return summary, trends

def _translate_country_names(self, data):
""" Translate full country name from English to the language of the logged in user. """

for datum in data:
if datum['country']['name'] == UNKNOWN_COUNTRY_CODE:
# Translators: This is a placeholder for enrollment data collected without a known geolocation.
datum['country']['name'] = _('Unknown Country')
else:
country_code = datum['country']['alpha3']

try:
datum['country']['name'] = unicode(COUNTRIES[datum['country']['alpha2']])
except KeyError:
logger.warning('Unable to locate %s in django_countries.', country_code)

return data

def get_geography_data(self):
"""
Returns a list of course geography data and the updated date (ex. 2014-1-31).
Expand All @@ -132,6 +153,9 @@ def get_geography_data(self):
# Sort data by descending enrollment count
api_response = sorted(api_response, key=lambda i: i['count'], reverse=True)

# Translate the country names
api_response = self._translate_country_names(api_response)

# get the sum as a float so we can divide by it to get a percent
total_enrollment = float(sum([datum['count'] for datum in api_response]))

Expand All @@ -142,8 +166,8 @@ def get_geography_data(self):
'percent': datum['count'] / total_enrollment if total_enrollment > 0 else 0.0}
for datum in api_response]

# do not include unknown country metrics in summary information
data_without_unknown = [datum for datum in data if datum['countryName'] != UNKNOWN_COUNTRY_CODE]
# Filter out the unknown entry for the summary data
data_without_unknown = [datum for datum in data if datum['countryCode'] is not None]

# Include a summary of the number of countries and the top 3 countries, excluding unknown.
summary = {
Expand Down
4 changes: 1 addition & 3 deletions analytics_dashboard/courses/tests/test_presenters.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,7 @@ def test_get_summary_and_trend_data(self, mock_enrollment):

@mock.patch('analyticsclient.course.Course.enrollment')
def test_get_geography_data(self, mock_enrollment):
# test with a full set of countries
mock_data = get_mock_api_enrollment_geography_data(self.course_id)
mock_enrollment.return_value = mock_data
mock_enrollment.return_value = get_mock_api_enrollment_geography_data(self.course_id)

expected_summary, expected_data = get_mock_presenter_enrollment_geography_data()
summary, actual_data = self.presenter.get_geography_data()
Expand Down
12 changes: 5 additions & 7 deletions analytics_dashboard/courses/tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,29 +59,27 @@ def get_mock_api_enrollment_geography_data_limited(course_id):


def get_mock_presenter_enrollment_geography_data():
# top three are used in the summary (unknown is excluded)
data = [
{'countryCode': 'USA', 'countryName': 'United States', 'count': 500, 'percent': 0.5},
{'countryCode': 'GER', 'countryName': 'Germany', 'count': 100, 'percent': 0.1},
{'countryCode': 'CAN', 'countryName': 'Canada', 'count': 100, 'percent': 0.1},
{'countryCode': None, 'countryName': UNKNOWN_COUNTRY_CODE, 'count': 300, 'percent': 0.3},
{'countryCode': None, 'countryName': 'Unknown Country', 'count': 300, 'percent': 0.3},
]
summary = {
'last_updated': CREATED_DATETIME,
'num_countries': 3,
'top_countries': data[:3] # unknown countries are excluded in the top 3
'top_countries': data[:3] # The unknown entry is excluded from the list of top countries.
}

# sort so unknown is corrected placed in the returned data
# Sort so that the unknown entry is in the correct location within the list.
data = sorted(data, key=lambda i: i['count'], reverse=True)

return summary, data


def get_mock_presenter_enrollment_geography_data_limited():
'''
Returns a smaller set of countries.
'''
""" Returns a smaller set of countries. """

summary, data = get_mock_presenter_enrollment_geography_data()
data = data[0:1]
data[0]['percent'] = 1.0
Expand Down
19 changes: 18 additions & 1 deletion analytics_dashboard/static/js/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,13 @@ var require = {
nvd3: 'vendor/nvd3/nv.d3',
topojson: 'vendor/topojson/topojson',
datamaps: 'vendor/datamaps/datamaps.world.min',
moment: 'vendor/moment/moment-with-locales'
moment: 'vendor/moment/moment-with-locales',
text: 'bower_components/requirejs-plugins/lib/text',
json: 'bower_components/requirejs-plugins/src/json',
cldr: 'bower_components/cldrjs/dist/cldr',
'cldr-data': 'bower_components/cldr-data',
globalize: 'bower_components/globalize/dist/globalize',
globalization: 'js/utils/globalization'
},
shim: {
bootstrap: {
Expand Down Expand Up @@ -56,6 +62,17 @@ var require = {
},
moment: {
noGlobal: true
},
json: {
deps: ['text']
},
globalize: {
deps: ['jquery', 'cldr'],
exports: 'Globalize'
},
globalization: {
deps: ['globalize'],
exports: 'Globalize'
}
},
// load jquery automatically
Expand Down
12 changes: 8 additions & 4 deletions analytics_dashboard/static/js/engagement-content-main.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,26 @@ require(['vendor/domReady!', 'load/init-page'], function(doc, page){
key: 'any',
title: gettext('Active Students'),
color: '#8DA0CB',
className: 'text-right'
className: 'text-right',
type: 'number'
},{
key: 'played_video',
title: gettext('Watched a Video'),
color: '#66C2A5',
className: 'text-right'
className: 'text-right',
type: 'number'
},{
key: 'attempted_problem',
title: gettext('Tried a Problem'),
color: '#FC8D62',
className: 'text-right'
className: 'text-right',
type: 'number'
},{
key: 'posted_forum',
title: gettext('Posted in Forum'),
color: '#E78AC3',
className: 'text-right'
className: 'text-right',
type: 'number'
}],
trendSettings;

Expand Down
2 changes: 1 addition & 1 deletion analytics_dashboard/static/js/enrollment-activity-main.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ require(['vendor/domReady!', 'load/init-page'], function(doc, page){
columns: [
{key: 'date', title: gettext('Date'), type: 'date'},
// Translators: The noun count (e.g. number of students)
{key: 'count', title: gettext('Total Enrollment'), className: 'text-right'}
{key: 'count', title: gettext('Total Enrollment'), className: 'text-right', type: 'number'}
],
sorting: ['-date']
});
Expand Down
2 changes: 1 addition & 1 deletion analytics_dashboard/static/js/enrollment-geography-main.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ require(['vendor/domReady!', 'load/init-page'], function (doc, page) {
{key: 'countryName', title: gettext('Country')},
{key: 'percent', title: gettext('Percent'), className: 'text-right', type: 'percent'},
// Translators: The noun count (e.g. number of students)
{key: 'count', title: gettext('Total Enrollment'), className: 'text-right'}
{key: 'count', title: gettext('Total Enrollment'), className: 'text-right', type: 'number'}
],
sorting: ['-count']
});
Expand Down
33 changes: 25 additions & 8 deletions analytics_dashboard/static/js/test/spec-runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,13 @@ var config = {
nvd3: 'vendor/nvd3/nv.d3',
topojson: 'vendor/topojson/topojson',
datamaps: 'vendor/datamaps/datamaps.world.min',
moment: 'vendor/moment/moment-with-locales'
moment: 'vendor/moment/moment-with-locales',
text: 'bower_components/requirejs-plugins/lib/text',
json: 'bower_components/requirejs-plugins/src/json',
cldr: 'bower_components/cldrjs/dist/cldr',
'cldr-data': 'bower_components/cldr-data',
globalize: 'bower_components/globalize/dist/globalize',
globalization: 'js/utils/globalization'
},
shim: {
bootstrap: {
Expand Down Expand Up @@ -54,23 +60,33 @@ var config = {
datamaps: {
deps: ['topojson', 'd3'],
exports: 'datamap'
},
json: {
deps: ['text']
},
globalize: {
deps: ['jquery', 'cldr'],
exports: 'Globalize'
},
globalization: {
deps: ['globalize'],
exports: 'Globalize'
}
}
};

// there are two paths -- one for running this in browser and one for running
// via gulp
// Two execution paths: browser or gulp
if(isBrowser) {
// unfortunately, we can't read directories in the browser, so we need to
// list them here -- sorry!
// The browser cannot read directories, so all files must be enumerated below.
specs = [
config.baseUrl + 'js/spec/specs/attribute-view-spec.js',
config.baseUrl + 'js/spec/specs/course-model-spec.js',
config.baseUrl + 'js/spec/specs/tracking-model-spec.js',
config.baseUrl + 'js/spec/specs/trends-view-spec.js',
config.baseUrl + 'js/spec/specs/world-map-view-spec.js',
config.baseUrl + 'js/spec/specs/tracking-view-spec.js',
config.baseUrl + 'js/spec/specs/utils-spec.js'
config.baseUrl + 'js/spec/specs/utils-spec.js',
config.baseUrl + 'js/spec/specs/globalization-spec.js'
];
} else {
// you can automatically get the test files using karma's configs
Expand All @@ -79,9 +95,10 @@ if(isBrowser) {
specs.push(file);
}
}
// this is where karma puts the files
// This is where karma puts the files
config.baseUrl = '/base/analytics_dashboard/static/';
// karam lets you list the test files here

// Karma lets you list the test files here
config.deps = specs;
config.callback = window.__karma__.start;
}
Expand Down
20 changes: 20 additions & 0 deletions analytics_dashboard/static/js/test/specs/globalization-spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
define(['utils/globalization'], function (Globalization) {
'use strict';

describe('fixLanguageCode', function () {
it('should default to English if an invalid argument is provided', function () {
expect(fixLanguageCode(null)).toEqual('en');
expect(fixLanguageCode('')).toEqual('en');
expect(fixLanguageCode(1)).toEqual('en');
expect(fixLanguageCode(true)).toEqual('en');
});

it('should return zh if the given language code is zh-cn', function () {
expect(fixLanguageCode('zh-cn')).toEqual('zh');
});

it('should return en if the given language code is not known to CLDR', function () {
expect(fixLanguageCode('not-real')).toEqual('en');
});
});
});
38 changes: 38 additions & 0 deletions analytics_dashboard/static/js/utils/globalization.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
function fixLanguageCode(languageCode) {
if (!languageCode || typeof languageCode !== 'string') {
return 'en';
}

languageCode = languageCode.toLowerCase();

// Django uses zh-cn. CLDR uses zh.
if (languageCode === 'zh-cn') {
return 'zh';
}

// There doesn't seem to be an onFailure event for the text! plugin. Make sure we only pass valid language codes
// so the plugin does not attempt to load non-existent files.
if (['ar', 'ca', 'cs', 'da', 'de', 'el', 'en-001', 'en-au', 'en-ca', 'en-gb', 'en-hk', 'en-in', 'en', 'es', 'fi',
'fr', 'he', 'hi', 'hr', 'hu', 'it', 'ja', 'ko', 'nb', 'nl', 'pl', 'pt-pt', 'pt', 'ro', 'root', 'ru', 'sk', 'sl',
'sr', 'sv', 'th', 'tr', 'uk', 'vi', 'zh-hant', 'zh'].indexOf(languageCode) > -1) {
return languageCode;
}

return 'en';
}

window.language = fixLanguageCode(window.language);

define([
'globalize',
'json!cldr-data/supplemental/likelySubtags.json',
'json!cldr-data/main/' + window.language + '/numbers.json',
'globalize/number'
], function (Globalize, likelySubtags, numbers, _number) {
'use strict';

Globalize.load(likelySubtags);
Globalize.load(numbers);

return Globalize(window.language);
});
Loading

0 comments on commit 61970b2

Please sign in to comment.