Skip to content

Commit 6f7f1d8

Browse files
committed
Merge pull request openedx-unsupported#121 from edx/l10n
Localized Graphs and Tables
2 parents ed0527a + ab2735b commit 6f7f1d8

File tree

145 files changed

+330
-62582
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

145 files changed

+330
-62582
lines changed

.bowerrc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"directory": "analytics_dashboard/static/bower_components",
3+
"scripts": {
4+
"postinstall": "./bower-post-install.sh"
5+
}
6+
}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ Desktop.ini
4141
.idea/
4242
analytics_dashboard/assets/
4343
node_modules/
44+
analytics_dashboard/static/bower_components/
4445
.coverage
4546
nosetests.xml
4647
reports/

.pep8

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
[pep8]
22
ignore=E501
33
max_line_length=119
4-
exclude=settings,migrations,bower_components
4+
exclude=settings,migrations,analytics_dashboard/static,bower_components

.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ before_install:
44
- export PATH=./node_modules/.bin:$PATH
55
- sudo apt-get update -qq
66
- sudo apt-get install -y npm
7+
- sudo apt-get --reinstall install -qq language-pack-en
78
- "export DISPLAY=:99.0"
89
- "sh -e /etc/init.d/xvfb start"
910
install:

Makefile

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
1+
.PHONY: requirements
2+
13
ROOT = $(shell echo "$$PWD")
24
COVERAGE = $(ROOT)/build/coverage
35
PACKAGES = analytics_dashboard courses
46
NUM_PROCESSES = 2
57

68
.PHONY: requirements clean
79

8-
requirements:
10+
requirements: requirements.js
911
pip install -q -r requirements/base.txt --exists-action w
1012

13+
requirements.js:
14+
npm install
15+
bower install
16+
1117
test.requirements: requirements
1218
pip install -q -r requirements/test.txt --exists-action w
1319

@@ -48,9 +54,9 @@ quality:
4854

4955
validate_python: test.requirements test_python quality
5056

51-
validate_js:
52-
npm install
53-
gulp
57+
validate_js: requirements.js
58+
gulp test
59+
gulp lint
5460

5561
validate: validate_python validate_js
5662

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ Dashboard to display course analytics to course teams
77
Prerequisites
88
-------------
99
* Python 2.7.x (not tested with Python 3.x)
10-
* [gettext](http://www.gnu.org/software/gettext/)
10+
* [gettext](http://www.gnu.org/software/gettext/)
11+
* [npm](https://www.npmjs.org/)
1112

1213
Getting Started
1314
---------------
1415
1. Get the code (e.g. clone the repository).
15-
2. Install the Python requirements:
16+
2. Install the Python/Node/Bower requirements:
1617

1718
$ make develop
1819

acceptance_tests/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
import locale
12
import os
23

4+
locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')
5+
36

47
def str2bool(s):
58
s = unicode(s)
@@ -28,7 +31,7 @@ def str2bool(s):
2831
LMS_USERNAME = os.environ.get('LMS_USERNAME')
2932
LMS_PASSWORD = os.environ.get('LMS_PASSWORD')
3033

31-
if ENABLE_OAUTH_TESTS and not(LMS_HOSTNAME and LMS_USERNAME and LMS_PASSWORD):
34+
if ENABLE_OAUTH_TESTS and not (LMS_HOSTNAME and LMS_USERNAME and LMS_PASSWORD):
3235
raise Exception('LMS settings must be set in order to test OAuth.')
3336

3437
TEST_COURSE_ID = os.environ.get('TEST_COURSE_ID', u'edX/DemoX/Demo_Course')

acceptance_tests/mixins.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import locale
12
from bok_choy.promise import EmptyPromise
23
from analyticsclient.client import Client
34
from acceptance_tests import API_SERVER_URL, API_AUTH_TOKEN, DASHBOARD_FEEDBACK_EMAIL, SUPPORT_URL, LMS_USERNAME, \
@@ -209,3 +210,8 @@ def test_page(self):
209210
self._test_user_menu()
210211
self._test_footer()
211212
self._test_data_update_message()
213+
214+
@staticmethod
215+
def format_number(value):
216+
""" Format the given value for the current locale (e.g. include decimal separator). """
217+
return locale.format("%d", value, grouping=True)

acceptance_tests/test_course_engagement.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import datetime
22

33
from bok_choy.web_app_test import WebAppTest
4-
54
from analyticsclient.constants import activity_type as at
5+
66
from acceptance_tests.mixins import CoursePageTestsMixin
77
from acceptance_tests.pages import CourseEngagementContentPage
88

@@ -50,7 +50,7 @@ def _test_engagement_metrics(self):
5050
}
5151
for activity_type in activity_types:
5252
data_selector = 'data-activity-type={0}'.format(activity_type)
53-
self.assertSummaryPointValueEquals(data_selector, unicode(recent_activity[activity_type]))
53+
self.assertSummaryPointValueEquals(data_selector, self.format_number(recent_activity[activity_type]))
5454
self.assertSummaryTooltipEquals(data_selector, expected_tooltips[activity_type])
5555

5656
def _test_engagement_graph(self):
@@ -85,9 +85,11 @@ def _test_engagement_table(self):
8585
weekly_activity = trend_activity[i]
8686
expected_date = self.format_time_as_dashboard((
8787
datetime.datetime.strptime(weekly_activity['interval_end'], date_time_format)) - datetime.timedelta(days=1)).replace(' 0', ' ')
88-
expected = [expected_date, weekly_activity[at.ANY], weekly_activity[at.PLAYED_VIDEO],
89-
weekly_activity[at.ATTEMPTED_PROBLEM]]
90-
actual = [columns[0].text, int(columns[1].text), int(columns[2].text), int(columns[3].text)]
88+
expected = [expected_date,
89+
self.format_number(weekly_activity[at.ANY]),
90+
self.format_number(weekly_activity[at.PLAYED_VIDEO]),
91+
self.format_number(weekly_activity[at.ATTEMPTED_PROBLEM])]
92+
actual = [columns[0].text, columns[1].text, columns[2].text, columns[3].text]
9193
self.assertListEqual(actual, expected)
9294

9395
for j in range(1, 4):

acceptance_tests/test_course_enrollment.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import datetime
22

33
from bok_choy.web_app_test import WebAppTest
4+
from analyticsclient.constants import demographic, UNKNOWN_COUNTRY_CODE
45

5-
from analyticsclient.constants import demographic
66
from acceptance_tests.mixins import CoursePageTestsMixin
77
from acceptance_tests.pages import CourseEnrollmentActivityPage, CourseEnrollmentGeographyPage
88

@@ -44,7 +44,7 @@ def _test_enrollment_metrics_and_graph(self):
4444
# Check values of summary boxes
4545
current_enrollment_count = current_enrollment['count']
4646
data_selector = 'data-stat-type=current_enrollment'
47-
self.assertSummaryPointValueEquals(data_selector, unicode(current_enrollment_count))
47+
self.assertSummaryPointValueEquals(data_selector, self.format_number(current_enrollment_count))
4848
self.assertSummaryTooltipEquals(data_selector, u'Students enrolled in the course.')
4949

5050
# Check value of summary box for last week
@@ -74,8 +74,8 @@ def _test_enrollment_trend_table(self):
7474
enrollment = enrollment_data[i]
7575
expected_date = datetime.datetime.strptime(enrollment['date'], self.api_date_format).strftime(
7676
"%B %d, %Y").replace(' 0', ' ')
77-
expected = [expected_date, enrollment['count']]
78-
actual = [columns[0].text, int(columns[1].text)]
77+
expected = [expected_date, self.format_number(enrollment['count'])]
78+
actual = [columns[0].text, columns[1].text]
7979
self.assertListEqual(actual, expected)
8080
self.assertIn('text-right', columns[1].get_attribute('class'))
8181

@@ -159,7 +159,12 @@ def _test_enrollment_country_table(self):
159159
enrollment = self.enrollment_data[i]
160160
expected_percent = enrollment['count'] / sum_count * 100
161161
expected_percent_display = '{:.1f}%'.format(expected_percent) if expected_percent >= 1.0 else '< 1%'
162-
expected = [enrollment['country']['name'], expected_percent_display, enrollment['count']]
162+
163+
country_name = enrollment['country']['name']
164+
if country_name == UNKNOWN_COUNTRY_CODE:
165+
country_name = u'Unknown Country'
166+
167+
expected = [country_name, expected_percent_display, enrollment['count']]
163168
actual = [columns[0].text, columns[1].text, int(columns[2].text)]
164169
self.assertListEqual(actual, expected)
165170
self.assertIn('text-right', columns[1].get_attribute('class'))

analytics_dashboard/analytics_dashboard/formats/__init__.py

Whitespace-only changes.

analytics_dashboard/analytics_dashboard/formats/en/__init__.py

Whitespace-only changes.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DATE_FORMAT = 'F d, Y'

analytics_dashboard/analytics_dashboard/settings/base.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@
7777

7878
# See: https://docs.djangoproject.com/en/dev/ref/settings/#use-tz
7979
USE_TZ = True
80+
81+
FORMAT_MODULE_PATH = 'analytics_dashboard.formats'
8082
########## END GENERAL CONFIGURATION
8183

8284

@@ -210,7 +212,8 @@
210212

211213
# Admin panel and documentation:
212214
'django.contrib.admin',
213-
'waffle'
215+
'waffle',
216+
'django_countries',
214217
)
215218

216219
# Apps specific for this project go here.

analytics_dashboard/courses/presenters.py

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
import datetime
2+
import logging
23

4+
from django.utils.translation import ugettext as _
35
from django.conf import settings
4-
6+
from django_countries import countries
7+
from waffle import switch_is_active
58
from analyticsclient.client import Client
69
import analyticsclient.constants.activity_type as AT
7-
from analyticsclient.constants import demographic
8-
from analyticsclient.constants import UNKNOWN_COUNTRY_CODE
10+
from analyticsclient.constants import demographic, UNKNOWN_COUNTRY_CODE
911

10-
from waffle import switch_is_active
12+
13+
logger = logging.getLogger(__name__)
14+
COUNTRIES = dict(countries)
1115

1216

1317
class BasePresenter(object):
@@ -140,6 +144,23 @@ def _build_empty_trend(self, day):
140144
trend = {'date': day.isoformat(), 'count': 0}
141145
return trend
142146

147+
def _translate_country_names(self, data):
148+
""" Translate full country name from English to the language of the logged in user. """
149+
150+
for datum in data:
151+
if datum['country']['name'] == UNKNOWN_COUNTRY_CODE:
152+
# Translators: This is a placeholder for enrollment data collected without a known geolocation.
153+
datum['country']['name'] = _('Unknown Country')
154+
else:
155+
country_code = datum['country']['alpha3']
156+
157+
try:
158+
datum['country']['name'] = unicode(COUNTRIES[datum['country']['alpha2']])
159+
except KeyError:
160+
logger.warning('Unable to locate %s in django_countries.', country_code)
161+
162+
return data
163+
143164
def get_geography_data(self):
144165
"""
145166
Returns a list of course geography data and the updated date (ex. 2014-1-31).
@@ -154,6 +175,9 @@ def get_geography_data(self):
154175
# Sort data by descending enrollment count
155176
api_response = sorted(api_response, key=lambda i: i['count'], reverse=True)
156177

178+
# Translate the country names
179+
api_response = self._translate_country_names(api_response)
180+
157181
# get the sum as a float so we can divide by it to get a percent
158182
total_enrollment = float(sum([datum['count'] for datum in api_response]))
159183

@@ -164,8 +188,8 @@ def get_geography_data(self):
164188
'percent': datum['count'] / total_enrollment if total_enrollment > 0 else 0.0}
165189
for datum in api_response]
166190

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

170194
# Include a summary of the number of countries and the top 3 countries, excluding unknown.
171195
summary = {

analytics_dashboard/courses/tests/test_presenters.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,9 +151,7 @@ def test_get_summary_and_trend_data_small(self, mock_enrollment):
151151

152152
@mock.patch('analyticsclient.course.Course.enrollment')
153153
def test_get_geography_data(self, mock_enrollment):
154-
# test with a full set of countries
155-
mock_data = get_mock_api_enrollment_geography_data(self.course_id)
156-
mock_enrollment.return_value = mock_data
154+
mock_enrollment.return_value = get_mock_api_enrollment_geography_data(self.course_id)
157155

158156
expected_summary, expected_data = get_mock_presenter_enrollment_geography_data()
159157
summary, actual_data = self.presenter.get_geography_data()

analytics_dashboard/courses/tests/utils.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,20 +77,19 @@ def get_mock_api_enrollment_geography_data_limited(course_id):
7777

7878

7979
def get_mock_presenter_enrollment_geography_data():
80-
# top three are used in the summary (unknown is excluded)
8180
data = [
8281
{'countryCode': 'USA', 'countryName': 'United States', 'count': 500, 'percent': 0.5},
8382
{'countryCode': 'GER', 'countryName': 'Germany', 'count': 100, 'percent': 0.1},
8483
{'countryCode': 'CAN', 'countryName': 'Canada', 'count': 100, 'percent': 0.1},
85-
{'countryCode': None, 'countryName': UNKNOWN_COUNTRY_CODE, 'count': 300, 'percent': 0.3},
84+
{'countryCode': None, 'countryName': 'Unknown Country', 'count': 300, 'percent': 0.3},
8685
]
8786
summary = {
8887
'last_updated': CREATED_DATETIME,
8988
'num_countries': 3,
90-
'top_countries': data[:3] # unknown countries are excluded in the top 3
89+
'top_countries': data[:3] # The unknown entry is excluded from the list of top countries.
9190
}
9291

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

9695
return summary, data

analytics_dashboard/static/js/common.js

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,33 @@
22
* This defines our libraries across the application. Each page
33
* should load this file.
44
*/
5-
var require = {
5+
6+
var require_config = {
67
baseUrl: '/static/',
78
waitSeconds: 60,
89
paths: {
9-
jquery: 'vendor/jquery-1.11.1.min',
10-
underscore: 'vendor/underscore-min',
11-
backbone: 'vendor/backbone-min',
12-
bootstrap: 'vendor/bootstrap/javascripts/bootstrap.min',
13-
bootstrap_accessibility: 'vendor/bootstrap-accessibility-plugin/js/bootstrap-accessibility.min',
10+
jquery: 'bower_components/jquery/dist/jquery.min',
11+
underscore: 'bower_components/underscore/underscore-min',
12+
backbone: 'bower_components/backbone/backbone',
13+
bootstrap: 'bower_components/bootstrap-sass-official/assets/javascripts/bootstrap',
14+
bootstrap_accessibility: 'bower_components/bootstrapaccessibilityplugin/plugins/js/bootstrap-accessibility.min',
1415
models: 'js/models',
1516
views: 'js/views',
1617
utils: 'js/utils',
1718
load: 'js/load',
18-
holder: 'vendor/holder',
19-
dataTables: 'vendor/dataTables/jquery.dataTables.min',
19+
dataTables: 'bower_components/datatables/media/js/jquery.dataTables.min',
2020
dataTablesBootstrap: 'vendor/dataTables/dataTables.bootstrap',
21-
d3: 'vendor/d3/d3',
22-
nvd3: 'vendor/nvd3/nv.d3',
23-
topojson: 'vendor/topojson/topojson',
24-
datamaps: 'vendor/datamaps/datamaps.world.min',
25-
moment: 'vendor/moment/moment-with-locales'
21+
d3: 'bower_components/d3/d3.min',
22+
nvd3: 'bower_components/nvd3/nv.d3.min',
23+
topojson: 'bower_components/topojson/topojson',
24+
datamaps: 'bower_components/datamaps/dist/datamaps.world.min',
25+
moment: 'bower_components/moment/min/moment-with-locales.min',
26+
text: 'bower_components/requirejs-plugins/lib/text',
27+
json: 'bower_components/requirejs-plugins/src/json',
28+
cldr: 'bower_components/cldrjs/dist/cldr',
29+
'cldr-data': 'bower_components/cldr-data',
30+
globalize: 'bower_components/globalize/dist/globalize',
31+
globalization: 'js/utils/globalization'
2632
},
2733
shim: {
2834
bootstrap: {
@@ -46,6 +52,9 @@ var require = {
4652
dataTablesBootstrap: {
4753
deps: ['jquery', 'dataTables']
4854
},
55+
d3: {
56+
exports: 'd3'
57+
},
4958
nvd3: {
5059
deps: ['d3'],
5160
exports: 'nv'
@@ -56,8 +65,21 @@ var require = {
5665
},
5766
moment: {
5867
noGlobal: true
68+
},
69+
json: {
70+
deps: ['text']
71+
},
72+
globalize: {
73+
deps: ['jquery', 'cldr'],
74+
exports: 'Globalize'
75+
},
76+
globalization: {
77+
deps: ['globalize'],
78+
exports: 'Globalize'
5979
}
6080
},
6181
// load jquery automatically
6282
deps: ['jquery']
6383
};
84+
85+
requirejs.config(require_config);

0 commit comments

Comments
 (0)