Commit 147bdd34 by Dennis Jen

Merge pull request #181 from edx/dsjen/edu-demographics

Updated education levels to new API format.
parents b6449201 8e84455e
......@@ -2,6 +2,7 @@ import datetime
from unittest import skipUnless
from bok_choy.web_app_test import WebAppTest
import analyticsclient.constants.education_level as EDUCATION_LEVEL
from analyticsclient.constants import demographic
from acceptance_tests import ENABLE_DEMOGRAPHICS_TESTS
from acceptance_tests.mixins import CourseDemographicsPageTestsMixin
......@@ -139,6 +140,20 @@ class CourseEnrollmentDemographicsGenderTests(CourseDemographicsPageTestsMixin,
@skipUnless(ENABLE_DEMOGRAPHICS_TESTS, 'Demographics tests are not enabled.')
class CourseEnrollmentDemographicsEducationTests(CourseDemographicsPageTestsMixin, WebAppTest):
EDUCATION_NAMES = {
EDUCATION_LEVEL.NONE: 'None',
EDUCATION_LEVEL.OTHER: 'Other',
EDUCATION_LEVEL.PRIMARY: 'Primary',
EDUCATION_LEVEL.JUNIOR_SECONDARY: 'Middle',
EDUCATION_LEVEL.SECONDARY: 'Secondary',
EDUCATION_LEVEL.ASSOCIATES: 'Associate',
EDUCATION_LEVEL.BACHELORS: "Bachelor's",
EDUCATION_LEVEL.MASTERS: "Master's",
EDUCATION_LEVEL.DOCTORATE: 'Doctorate',
None: 'Unknown'
}
help_path = 'enrollment/Demographics_Education.html'
demographic_type = demographic.EDUCATION
......@@ -175,13 +190,13 @@ class CourseEnrollmentDemographicsEducationTests(CourseDemographicsPageTestsMixi
for group in education_groups:
selector = 'data-stat-type={}'.format(group['stat_type'])
filtered_group = ([education for education in self.demographic_data
if education['education_level']['short_name'] in group['levels']])
if education['education_level'] in group['levels']])
group_total = float(sum([datum['count'] for datum in filtered_group]))
expected_percent_display = self.build_display_percentage(group_total, total)
self.assertSummaryPointValueEquals(selector, expected_percent_display)
def _test_table_row(self, datum, column, sum_count):
expected = [datum['education_level']['name'],
expected = [self.EDUCATION_NAMES[datum['education_level']],
unicode(datum['count'])]
actual = [column[0].text, column[1].text]
self.assertListEqual(actual, expected)
......
......@@ -2,7 +2,7 @@ import copy
import datetime
import logging
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy as _
from django.conf import settings
from django_countries import countries
from waffle import switch_is_active
......@@ -34,19 +34,35 @@ GENDER_ORDER = {
GENDER.OTHER: 2
}
KNOWN_EDUCATION_LEVELS = [EDUCATION_LEVEL.NONE, EDUCATION_LEVEL.OTHER, EDUCATION_LEVEL.PRIMARY,
EDUCATION_LEVEL.JUNIOR_SECONDARY, EDUCATION_LEVEL.SECONDARY, EDUCATION_LEVEL.ASSOCIATES,
EDUCATION_LEVEL.BACHELORS, EDUCATION_LEVEL.MASTERS, EDUCATION_LEVEL.DOCTORATE]
# for display
EDUCATION_SHORT_NAMES = {
EDUCATION_LEVEL.NONE: 'None',
EDUCATION_LEVEL.OTHER: 'Other',
EDUCATION_LEVEL.PRIMARY: 'Elementary',
EDUCATION_LEVEL.JUNIOR_SECONDARY: 'Middle',
EDUCATION_LEVEL.SECONDARY: 'High',
EDUCATION_LEVEL.ASSOCIATES: 'Associates',
EDUCATION_LEVEL.BACHELORS: 'Bachelors',
EDUCATION_LEVEL.MASTERS: 'Masters',
EDUCATION_LEVEL.DOCTORATE: 'Doctorate'
EDUCATION_NAMES = {
# Translators: This describes the learner's education level.
EDUCATION_LEVEL.NONE: _('None'),
# Translators: This describes the learner's education level.
EDUCATION_LEVEL.OTHER: _('Other'),
# Translators: This describes the learner's education level (e.g. Elementary School Degree).
EDUCATION_LEVEL.PRIMARY: _('Primary'),
# Translators: This describes the learner's education level (e.g. Middle School Degree).
EDUCATION_LEVEL.JUNIOR_SECONDARY: _('Middle'),
# Translators: This describes the learner's education level.
EDUCATION_LEVEL.SECONDARY: _('Secondary'),
# Translators: This describes the learner's education level (e.g. Associate's Degree).
EDUCATION_LEVEL.ASSOCIATES: _("Associate"),
# Translators: This describes the learner's education level (e.g. Bachelor's Degree).
EDUCATION_LEVEL.BACHELORS: _("Bachelor's"),
# Translators: This describes the learner's education level (e.g. Master's Degree).
EDUCATION_LEVEL.MASTERS: _("Master's"),
# Translators: This describes the learner's education level (e.g. Doctorate Degree).
EDUCATION_LEVEL.DOCTORATE: _('Doctorate')
}
# Translators: This describes the learner's education level.
UNKNOWN_EDUCATION_LEVEL_NAME = _('Unknown')
# order for displaying in the chart
EDUCATION_ORDER = {
EDUCATION_LEVEL.NONE: 0,
......@@ -564,7 +580,7 @@ class CourseEnrollmentDemographicsPresenter(BaseCourseEnrollmentPresenter):
def _calculate_education_percent(self, api_response, levels):
""" Aggregates levels of education and returns the percent of the total. """
filtered_levels = ([education for education in api_response
if education['education_level']['short_name'] in levels])
if education['education_level'] in levels])
subset_enrollment = self._calculate_total_enrollment(filtered_levels)
return self._calculate_percent(subset_enrollment, self._calculate_total_enrollment(api_response))
......@@ -585,11 +601,10 @@ class CourseEnrollmentDemographicsPresenter(BaseCourseEnrollmentPresenter):
def _build_education_levels(self, api_response):
known_education = [i for i in api_response if i['education_level']]
known_enrollment_total = self._calculate_total_enrollment(known_education)
levels = [{'educationLevelShort': EDUCATION_SHORT_NAMES[datum['education_level']['short_name']],
'educationLevelLong': datum['education_level']['name'],
levels = [{'educationLevel': EDUCATION_NAMES[datum['education_level']],
'count': datum['count'],
'percent': self._calculate_percent(datum['count'], known_enrollment_total),
'order': EDUCATION_ORDER[datum['education_level']['short_name']]}
'order': EDUCATION_ORDER[datum['education_level']]}
for datum in known_education]
levels = sorted(levels, key=lambda i: i['order'], reverse=False)
......@@ -599,13 +614,25 @@ class CourseEnrollmentDemographicsPresenter(BaseCourseEnrollmentPresenter):
if unknown:
unknown_count = unknown[0]['count']
levels.append({
'educationLevelShort': 'Unknown',
'educationLevelLong': 'Unknown',
'educationLevel': UNKNOWN_EDUCATION_LEVEL_NAME,
'count': unknown_count
})
return levels
def _fill_empty_education_levels(self, api_response):
found_levels = [level['education_level'] for level in api_response]
# get the symmetric difference
missed_levels = list(set(found_levels) ^ set(KNOWN_EDUCATION_LEVELS))
for level in missed_levels:
api_response.append({
'education_level': level,
'count': 0
})
return api_response
def get_education(self):
api_response = self.course.enrollment(demographic.EDUCATION)
education_levels = None
......@@ -615,6 +642,7 @@ class CourseEnrollmentDemographicsPresenter(BaseCourseEnrollmentPresenter):
if api_response:
last_updated = self.parse_api_datetime(api_response[0]['created'])
api_response = self._fill_empty_education_levels(api_response)
education_levels = self._build_education_levels(api_response)
education_summary = self._build_education_summary(api_response)
known_enrollment_percent = self._calculate_known_total_percent(api_response, 'education_level')
......
from django.core.serializers.json import DjangoJSONEncoder
from django.utils.encoding import force_text
from django.utils.functional import Promise
class LazyEncoder(DjangoJSONEncoder):
"""
Force the conversion of lazy translations so that they can be serialized to JSON.
via https://docs.djangoproject.com/en/dev/topics/serialization/
"""
# pylint: disable=method-hidden
def default(self, obj):
if isinstance(obj, Promise):
return force_text(obj)
return super(LazyEncoder, self).default(obj)
import json
from django.test import TestCase
from django.utils.translation import ugettext_lazy as _
from courses.serializers import LazyEncoder
class CourseEngagementPresenterTests(TestCase):
def test_lazy_encode(self):
primary = _('primary')
expected = '{{"education_level": "{0}"}}'.format(unicode(primary))
actual = json.dumps({'education_level': primary}, cls=LazyEncoder)
self.assertEqual(actual, expected)
......@@ -275,90 +275,63 @@ def get_mock_api_enrollment_education_data(course_id):
{
'course_id': course_id,
'date': '2014-09-22',
'education_level': {
'name': 'None',
'short_name': EDUCATION_LEVEL.NONE
},
'education_level': EDUCATION_LEVEL.NONE,
'count': 100,
'created': CREATED_DATETIME_STRING
},
{
'course_id': course_id,
'date': '2014-09-22',
'education_level': {
'name': 'Other',
'short_name': EDUCATION_LEVEL.OTHER
},
'education_level': EDUCATION_LEVEL.OTHER,
'count': 200,
'created': CREATED_DATETIME_STRING
},
{
'course_id': course_id,
'date': '2014-09-22',
'education_level': {
'name': 'Elementary/Primary School',
'short_name': EDUCATION_LEVEL.PRIMARY
},
'education_level': EDUCATION_LEVEL.PRIMARY,
'count': 100,
'created': CREATED_DATETIME_STRING
},
{
'course_id': course_id,
'date': '2014-09-22',
'education_level': {
'name': 'Junior Secondary/Junior High/Middle School',
'short_name': EDUCATION_LEVEL.JUNIOR_SECONDARY
},
'education_level': EDUCATION_LEVEL.JUNIOR_SECONDARY,
'count': 100,
'created': CREATED_DATETIME_STRING
},
{
'course_id': course_id,
'date': '2014-09-22',
'education_level': {
'name': 'Secondary/High School',
'short_name': EDUCATION_LEVEL.SECONDARY
},
'education_level': EDUCATION_LEVEL.SECONDARY,
'count': 100,
'created': CREATED_DATETIME_STRING
},
{
'course_id': course_id,
'date': '2014-09-22',
'education_level': {
'name': "Associate's Degree",
'short_name': EDUCATION_LEVEL.ASSOCIATES
},
'education_level': EDUCATION_LEVEL.ASSOCIATES,
'count': 100,
'created': CREATED_DATETIME_STRING
},
{
'course_id': course_id,
'date': '2014-09-22',
'education_level': {
'name': "Bachelor's Degree",
'short_name': EDUCATION_LEVEL.BACHELORS
},
'education_level': EDUCATION_LEVEL.BACHELORS,
'count': 100,
'created': CREATED_DATETIME_STRING
},
{
'course_id': course_id,
'date': '2014-09-22',
'education_level': {
'name': "Master's or Professional Degree",
'short_name': EDUCATION_LEVEL.MASTERS
},
'education_level': EDUCATION_LEVEL.MASTERS,
'count': 100,
'created': CREATED_DATETIME_STRING
},
{
'course_id': course_id,
'date': '2014-09-22',
'education_level': {
'name': 'Doctorate',
'short_name': EDUCATION_LEVEL.DOCTORATE
},
'education_level': EDUCATION_LEVEL.DOCTORATE,
'count': 100,
'created': CREATED_DATETIME_STRING
},
......@@ -377,71 +350,61 @@ def get_mock_api_enrollment_education_data(course_id):
def get_mock_presenter_enrollment_education_data():
data = [
{
'educationLevelShort': 'None',
'educationLevelLong': 'None',
'educationLevel': 'None',
'count': 100,
'percent': 0.1,
'order': 0
},
{
'educationLevelShort': 'Elementary',
'educationLevelLong': 'Elementary/Primary School',
'educationLevel': 'Primary',
'count': 100,
'percent': 0.1,
'order': 1
},
{
'educationLevelShort': 'Middle',
'educationLevelLong': 'Junior Secondary/Junior High/Middle School',
'educationLevel': 'Middle',
'count': 100,
'percent': 0.1,
'order': 2
},
{
'educationLevelShort': 'High',
'educationLevelLong': 'Secondary/High School',
'educationLevel': 'Secondary',
'count': 100,
'percent': 0.1,
'order': 3
},
{
'educationLevelShort': 'Associates',
'educationLevelLong': "Associate's Degree",
'educationLevel': "Associate",
'count': 100,
'percent': 0.1,
'order': 4
},
{
'educationLevelShort': 'Bachelors',
'educationLevelLong': "Bachelor's Degree",
'educationLevel': "Bachelor's",
'count': 100,
'percent': 0.1,
'order': 5
},
{
'educationLevelShort': 'Masters',
'educationLevelLong': "Master's or Professional Degree",
'educationLevel': "Master's",
'count': 100,
'percent': 0.1,
'order': 6
},
{
'educationLevelShort': 'Doctorate',
'educationLevelLong': 'Doctorate',
'educationLevel': 'Doctorate',
'count': 100,
'percent': 0.1,
'order': 7
},
{
'educationLevelShort': 'Other',
'educationLevelLong': 'Other',
'educationLevel': 'Other',
'count': 200,
'percent': 0.2,
'order': 8
},
{
'educationLevelShort': 'Unknown',
'educationLevelLong': 'Unknown',
'educationLevel': 'Unknown',
'count': 1000
}
]
......
......@@ -22,6 +22,7 @@ from waffle import switch_is_active
from courses import permissions
from courses.presenters import CourseEngagementPresenter, CourseEnrollmentPresenter, \
CourseEnrollmentDemographicsPresenter
from courses.serializers import LazyEncoder
from courses.utils import is_feature_enabled
from help.views import ContextSensitiveHelpMixin
......@@ -49,7 +50,17 @@ class TrackedViewMixin(object):
return context
class CourseContextMixin(TrackedViewMixin):
class LazyEncoderMixin(object):
def get_page_data(self, context):
""" Returns JSON serialized data with lazy translations converted. """
if 'js_data' in context:
return json.dumps(context['js_data'], cls=LazyEncoder)
else:
return None
class CourseContextMixin(TrackedViewMixin, LazyEncoderMixin):
"""
Adds default course context data.
......@@ -371,13 +382,6 @@ class CSVResponseMixin(object):
return urllib.quote(filename)
class JSONResponseMixin(object):
def render_to_response(self, context, **response_kwargs):
content = json.dumps(context['data'])
return HttpResponse(content, content_type='application/json',
**response_kwargs)
class EnrollmentActivityView(EnrollmentTemplateView):
template_name = 'courses/enrollment_activity.html'
page_title = _('Enrollment Activity')
......@@ -406,10 +410,10 @@ class EnrollmentActivityView(EnrollmentTemplateView):
context['js_data']['course']['enrollmentTrends'] = trend
context.update({
'page_data': json.dumps(context['js_data']),
'summary': summary,
'update_message': self.get_last_updated_message(last_updated)
})
context['page_data'] = self.get_page_data(context)
return context
......@@ -440,12 +444,12 @@ class EnrollmentDemographicsAgeView(EnrollmentDemographicsTemplateView):
context['js_data']['course']['ages'] = binned_ages
context.update({
'page_data': json.dumps(context['js_data']),
'summary': summary,
'chart_tip': self._build_chart_tooltip(known_enrollment_percent),
'update_message': self.get_last_updated_message(last_updated),
'data_information_message': self.data_information_message
})
context['page_data'] = self.get_page_data(context)
return context
......@@ -476,12 +480,12 @@ class EnrollmentDemographicsEducationView(EnrollmentDemographicsTemplateView):
context['js_data']['course']['education'] = binned_education
context.update({
'page_data': json.dumps(context['js_data']),
'summary': summary,
'chart_tip': self._build_chart_tooltip(known_enrollment_percent),
'update_message': self.get_last_updated_message(last_updated),
'data_information_message': self.data_information_message
})
context['page_data'] = self.get_page_data(context)
return context
......@@ -513,11 +517,11 @@ class EnrollmentDemographicsGenderView(EnrollmentDemographicsTemplateView):
context['js_data']['course']['genderTrend'] = trend
context.update({
'page_data': json.dumps(context['js_data']),
'update_message': self.get_last_updated_message(last_updated),
'chart_tip': self._build_chart_tooltip(known_enrollment_percent),
'data_information_message': self.data_information_message
})
context['page_data'] = self.get_page_data(context)
return context
......@@ -550,9 +554,9 @@ class EnrollmentGeographyView(EnrollmentTemplateView):
context['js_data']['course']['enrollmentByCountry'] = data
context.update({
'page_data': json.dumps(context['js_data']),
'update_message': self.get_last_updated_message(last_updated)
})
context['page_data'] = self.get_page_data(context)
return context
......@@ -583,9 +587,9 @@ class EngagementContentView(EngagementTemplateView):
context['js_data']['course']['engagementTrends'] = trends
context.update({
'summary': summary,
'page_data': json.dumps(context['js_data']),
'update_message': self.get_last_updated_message(last_updated)
})
context['page_data'] = self.get_page_data(context)
return context
......@@ -644,7 +648,7 @@ class CourseHome(LoginRequiredMixin, RedirectView):
return reverse('courses:enrollment_activity', kwargs={'course_id': course_id})
class CourseIndex(LoginRequiredMixin, TrackedViewMixin, TemplateView):
class CourseIndex(LoginRequiredMixin, TrackedViewMixin, LazyEncoderMixin, TemplateView):
template_name = 'courses/index.html'
page_name = 'course_index'
......@@ -658,9 +662,6 @@ class CourseIndex(LoginRequiredMixin, TrackedViewMixin, TemplateView):
raise PermissionDenied
context['courses'] = sorted(courses)
context.update({
'page_data': json.dumps(context['js_data']),
})
context['page_data'] = self.get_page_data(context)
return context
......@@ -20,7 +20,7 @@ require(['vendor/domReady!', 'load/init-page'], function(doc, page) {
title: gettext('Education'),
color: 'rgb(58, 162, 224)'
}],
x: { key: 'educationLevelShort' },
x: { key: 'educationLevel' },
y: { key: 'percent' }
});
......@@ -29,7 +29,7 @@ require(['vendor/domReady!', 'load/init-page'], function(doc, page) {
model: page.models.courseModel,
modelAttribute: 'education',
columns: [
{key: 'educationLevelLong', title: gettext('Educational Background')},
{key: 'educationLevel', title: gettext('Educational Background')},
{key: 'count', title: gettext('Number of Students'), className: 'text-right'}
],
sorting: ['-count']
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment