Commit c9debee7 by Dennis Jen Committed by GitHub

Merge pull request #496 from edx/dsjen/enrollment-tracks

Added audit and credit enrollment tracks.
parents 00c270b5 cfe1dc46
......@@ -61,7 +61,6 @@ validate_js: requirements.js
validate: validate_python validate_js
demo:
python manage.py switch display_verified_enrollment on --create
python manage.py switch show_engagement_forum_activity off --create
python manage.py switch enable_course_api off --create
python manage.py switch display_names_for_course_index off --create
......
......@@ -62,7 +62,6 @@ TEST_VIDEO_ID = os.environ.get('TEST_VIDEO_ID',
DOC_BASE_URL = os.environ.get('DOC_BASE_URL', 'http://edx-insights.readthedocs.org/en/latest')
ENABLE_ENROLLMENT_MODES = str2bool(os.environ.get('ENABLE_ENROLLMENT_MODES', False))
ENABLE_FORUM_POSTS = str2bool(os.environ.get('ENABLE_FORUM_POSTS', False))
# Course API settings
......
from collections import OrderedDict
import datetime
from analyticsclient.constants import demographic, UNKNOWN_COUNTRY_CODE, enrollment_modes
from bok_choy.web_app_test import WebAppTest
from acceptance_tests import ENABLE_ENROLLMENT_MODES
from acceptance_tests.mixins import CoursePageTestsMixin
from acceptance_tests.pages import CourseEnrollmentActivityPage, CourseEnrollmentGeographyPage
......@@ -25,9 +25,7 @@ class CourseEnrollmentActivityTests(CoursePageTestsMixin, WebAppTest):
"""
end_date = datetime.datetime.utcnow()
end_date_string = end_date.strftime(self.analytics_api_client.DATE_FORMAT)
_demographic = 'mode' if ENABLE_ENROLLMENT_MODES else None
return self.course.enrollment(_demographic, start_date=None, end_date=end_date_string)
return self.course.enrollment('mode', start_date=None, end_date=end_date_string)
def test_page(self):
super(CourseEnrollmentActivityTests, self).test_page()
......@@ -46,8 +44,8 @@ class CourseEnrollmentActivityTests(CoursePageTestsMixin, WebAppTest):
self.assertSummaryTooltipEquals(selector, tooltip)
def _get_valid_enrollment_modes(self, trends):
valid_modes = {enrollment_modes.AUDIT, enrollment_modes.HONOR}
invalid_modes = set(enrollment_modes.ALL) - valid_modes
valid_modes = set()
invalid_modes = set(enrollment_modes.ALL)
for datum in trends:
for candidate in list(invalid_modes):
......@@ -76,14 +74,13 @@ class CourseEnrollmentActivityTests(CoursePageTestsMixin, WebAppTest):
tooltip = u'Net difference in current enrollment in the last week.'
self.assertMetricTileValid('enrollment_change_last_%s_days' % i, enrollment, tooltip)
if ENABLE_ENROLLMENT_MODES:
valid_modes = self._get_valid_enrollment_modes(enrollment_data)
valid_modes = self._get_valid_enrollment_modes(enrollment_data)
if enrollment_modes.VERIFIED in valid_modes:
# Verify the verified enrollment metric tile.
verified_enrollment = enrollment_data[-1][enrollment_modes.VERIFIED]
tooltip = u'Number of currently enrolled students pursuing a verified certificate of achievement.'
self.assertMetricTileValid('verified_enrollment', verified_enrollment, tooltip)
if enrollment_modes.VERIFIED in valid_modes:
# Verify the verified enrollment metric tile.
verified_enrollment = enrollment_data[-1][enrollment_modes.VERIFIED]
tooltip = u'Number of currently enrolled students pursuing a verified certificate of achievement.'
self.assertMetricTileValid('verified_enrollment', verified_enrollment, tooltip)
# Verify *something* rendered where the graph should be. We cannot easily verify what rendered
self.assertElementHasContent("[data-section=enrollment-basics] #enrollment-trend-view")
......@@ -96,16 +93,17 @@ class CourseEnrollmentActivityTests(CoursePageTestsMixin, WebAppTest):
table_selector = 'div[data-role=enrollment-table] table'
headings = ['Date', 'Current Enrollment']
if ENABLE_ENROLLMENT_MODES:
headings.append('Honor Code')
valid_modes = self._get_valid_enrollment_modes(enrollment_data)
if enrollment_modes.VERIFIED in valid_modes:
headings.append('Verified')
if enrollment_modes.PROFESSIONAL in valid_modes:
headings.append('Professional')
valid_modes = self._get_valid_enrollment_modes(enrollment_data)
display_names = OrderedDict([
(enrollment_modes.HONOR, 'Honor'),
(enrollment_modes.AUDIT, 'Audit'),
(enrollment_modes.VERIFIED, 'Verified'),
(enrollment_modes.PROFESSIONAL, 'Professional'),
(enrollment_modes.CREDIT, 'Verified with Credit')
])
for mode, display_name in display_names.items():
if mode in valid_modes:
headings.append(display_name)
self.assertTableColumnHeadingsEqual(table_selector, headings)
......
......@@ -4,7 +4,6 @@ import logging
from django.utils.translation import ugettext_lazy as _
from django_countries import countries
from waffle import switch_is_active
from analyticsclient.constants import demographic, UNKNOWN_COUNTRY_CODE, enrollment_modes
import analyticsclient.constants.education_level as EDUCATION_LEVEL
import analyticsclient.constants.gender as GENDER
......@@ -82,16 +81,11 @@ class CourseEnrollmentPresenter(BasePresenter):
NUMBER_TOP_COUNTRIES = 3
@property
def display_verified_enrollment(self):
return switch_is_active('display_verified_enrollment')
def get_summary_and_trend_data(self):
"""
Retrieve recent summary and all historical trend data.
"""
_demographic = 'mode' if self.display_verified_enrollment else None
trends = self.course.enrollment(_demographic, start_date=None, end_date=self.get_current_date())
trends = self.course.enrollment('mode', start_date=None, end_date=self.get_current_date())
trends = self._fill_trend(trends)
summary = self._build_summary(trends)
......@@ -101,19 +95,17 @@ class CourseEnrollmentPresenter(BasePresenter):
day_before = self.parse_api_date(trends[0]['date']) - datetime.timedelta(days=1)
trends.insert(0, self._create_empty_enrollment_datapoint(day_before))
if self.display_verified_enrollment:
trends = self._merge_audit_and_honor(trends)
summary, trends = self._remove_empty_enrollment_modes(summary, trends)
return summary, trends
return self._remove_empty_enrollment_modes(summary, trends)
def _get_valid_enrollment_modes(self, trends):
"""
Return enrollment modes for which there is currently, or have been in the past, at least one enrolled student.
Return enrollment modes for which there was at least one enrolled student.
"""
valid_modes = {enrollment_modes.AUDIT, enrollment_modes.HONOR}
invalid_modes = set(enrollment_modes.ALL) - valid_modes
# default modes
valid_modes = set()
invalid_modes = set(enrollment_modes.ALL)
# go through each day of the trend and record any tracks with enrollment as valid
for datum in trends:
for candidate in invalid_modes.copy():
if datum.get(candidate, 0) > 0:
......@@ -136,22 +128,12 @@ class CourseEnrollmentPresenter(BasePresenter):
for mode in invalid_modes:
trend.pop(mode, None)
# hides verified enrollment counts in the summary card if it doesn't exist
if enrollment_modes.VERIFIED not in valid_modes:
summary.pop('verified_enrollment')
return summary, trends
def _merge_audit_and_honor(self, data):
"""
Merge the audit and honor tracks into a single "honor" track
Note: This can be removed once the API has been deployed, as it will be updated to handle this for us.
"""
for datum in data:
datum[enrollment_modes.HONOR] = datum.get(enrollment_modes.HONOR, 0) + datum.pop(enrollment_modes.AUDIT, 0)
return data
def _fill_trend(self, api_response):
""" Fills in enrollment counts for missing days in the trend data for display. """
if api_response:
......@@ -180,12 +162,14 @@ class CourseEnrollmentPresenter(BasePresenter):
"""
Create an enrollment datapoint with all counts set to zero.
"""
trend = {'date': day.isoformat(), 'count': 0}
trend = {
'date': day.isoformat(),
'count': 0,
'cumulative_count': 0
}
if self.display_verified_enrollment:
trend['cumulative_count'] = 0
for mode in enrollment_modes.ALL:
trend[mode] = 0
for mode in enrollment_modes.ALL:
trend[mode] = 0
return trend
......@@ -273,8 +257,7 @@ class CourseEnrollmentPresenter(BasePresenter):
# Add the first values to the returned data dictionary using the most-recent enrollment data
current_enrollment = recent_enrollment['count']
verified_enrollment = recent_enrollment.get(enrollment_modes.VERIFIED,
0) if self.display_verified_enrollment else None
verified_enrollment = recent_enrollment.get(enrollment_modes.VERIFIED, 0)
data.update({
'last_updated': last_enrollment_date,
......
......@@ -77,17 +77,15 @@ Individual course-centric enrollment activity view.
{% summary_point summary.enrollment_change_last_7_days label tooltip=tooltip %}
</div>
{% switch display_verified_enrollment %}
{% if not summary.verified_enrollment == None %}
<div class="col-xs-12 col-sm-3" data-stat-type="verified_enrollment">
{# Translators: This is a label to identify enrollment of students on the verified track. #}
{% trans "Verified Enrollment" as label %}
{# Translators: This is a label indicating the number of students enrolled in a course on the verified track. #}
{% trans "Number of currently enrolled students pursuing a verified certificate of achievement." as tooltip %}
{% summary_point summary.verified_enrollment label tooltip=tooltip %}
</div>
{% endif %}
{% endswitch %}
{% if not summary.verified_enrollment == None %}
<div class="col-xs-12 col-sm-3" data-stat-type="verified_enrollment">
{# Translators: This is a label to identify enrollment of students on the verified track. #}
{% trans "Verified Enrollment" as label %}
{# Translators: This is a label indicating the number of students enrolled in a course on the verified track. #}
{% trans "Number of currently enrolled students pursuing a verified certificate of achievement." as tooltip %}
{% summary_point summary.verified_enrollment label tooltip=tooltip %}
</div>
{% endif %}
</div>
</div>
{% else %}
......
......@@ -3,6 +3,7 @@ import copy
import datetime
import analyticsclient.constants.activity_type as AT
from analyticsclient.constants import enrollment_modes
from django.conf import settings
from django.core.cache import cache
from django.core.urlresolvers import reverse
......@@ -579,9 +580,6 @@ class CourseEngagementVideoPresenterTests(SwitchMixin, TestCase):
class CourseEnrollmentPresenterTests(SwitchMixin, TestCase):
@classmethod
def setUpClass(cls):
cls.toggle_switch('display_verified_enrollment', True)
def setUp(self):
self.course_id = 'edX/DemoX/Demo_Course'
......@@ -659,14 +657,36 @@ class CourseEnrollmentPresenterTests(SwitchMixin, TestCase):
@mock.patch('analyticsclient.course.Course.enrollment')
def test_hide_empty_enrollment_modes(self, mock_enrollment):
""" Enrollment modes with no enrolled students should not be returned. """
mock_enrollment.return_value = utils.get_mock_api_enrollment_data(self.course_id, include_verified=False)
# set trend for one mode to be all 0
mock_api_data = utils.get_mock_api_enrollment_data(self.course_id)
for day in mock_api_data:
day[enrollment_modes.PROFESSIONAL] = 0
mock_enrollment.return_value = mock_api_data
actual_summary, actual_trend = self.presenter.get_summary_and_trend_data()
self.assertDictEqual(actual_summary, utils.get_mock_enrollment_summary(include_verified=False))
self.assertDictEqual(actual_summary, utils.get_mock_enrollment_summary())
# trends without enrollment shouldn't be present in the returned trend
expected_trend = utils.get_mock_presenter_enrollment_trend(self.course_id)
for day in expected_trend:
del day[enrollment_modes.PROFESSIONAL]
expected_trend = utils.get_mock_presenter_enrollment_trend(self.course_id, include_verified=False)
self.assertListEqual(actual_trend, expected_trend)
@mock.patch('analyticsclient.course.Course.enrollment')
def test_remove_verified_summary(self, mock_enrollment):
""" Verified summary should be removed none are enrolled. """
mock_api_data = utils.get_mock_api_enrollment_data(self.course_id)
for day in mock_api_data:
day[enrollment_modes.VERIFIED] = 0
mock_enrollment.return_value = mock_api_data
actual_summary, _actual_trend = self.presenter.get_summary_and_trend_data()
expected_summary = utils.get_mock_enrollment_summary()
del expected_summary['verified_enrollment']
self.assertDictEqual(actual_summary, expected_summary)
class CourseEnrollmentDemographicsPresenterTests(TestCase):
def setUp(self):
......
......@@ -90,10 +90,6 @@ class CourseEnrollmentModeCSVViewTests(SwitchMixin, CourseCSVTestMixin, TestCase
base_file_name = 'enrollment'
api_method = 'analyticsclient.course.Course.enrollment'
@classmethod
def setUpClass(cls):
cls.toggle_switch('display_verified_enrollment', True)
def get_mock_data(self, course_id):
return get_mock_api_enrollment_data(course_id)
......
......@@ -20,10 +20,10 @@ GAP_START = 2
GAP_END = 4
def get_mock_api_enrollment_data(course_id, include_verified=True):
def get_mock_api_enrollment_data(course_id):
data = []
start_date = datetime.date(year=2014, month=1, day=1)
modes = enrollment_modes.ALL if include_verified else [enrollment_modes.AUDIT, enrollment_modes.HONOR]
modes = enrollment_modes.ALL
for index in range(31):
date = start_date + datetime.timedelta(days=index)
......@@ -32,12 +32,10 @@ def get_mock_api_enrollment_data(course_id, include_verified=True):
'date': date.strftime(Client.DATE_FORMAT),
'course_id': unicode(course_id),
'count': index * len(modes),
'created': CREATED_DATETIME_STRING
'created': CREATED_DATETIME_STRING,
'cumulative_count': index * len(modes) * 2
}
if include_verified:
datum['cumulative_count'] = datum['count'] * 2
for mode in modes:
datum[mode] = index
......@@ -52,22 +50,14 @@ def get_mock_api_enrollment_data_with_gaps(course_id):
return data
def get_mock_enrollment_summary(include_verified=True):
def get_mock_enrollment_summary():
summary = {
'last_updated': CREATED_DATETIME,
'current_enrollment': 60,
'total_enrollment': None,
'enrollment_change_last_7_days': 14,
'current_enrollment': 150,
'total_enrollment': 300,
'enrollment_change_last_7_days': 35,
'verified_enrollment': 30,
}
if include_verified:
summary.update({
'current_enrollment': 120,
'total_enrollment': 240,
'enrollment_change_last_7_days': 28,
'verified_enrollment': 30,
})
return summary
......@@ -84,17 +74,8 @@ def _get_empty_enrollment(date):
return enrollment
def _clean_modes(data):
for datum in data:
datum[enrollment_modes.HONOR] = datum[enrollment_modes.AUDIT] + datum[enrollment_modes.HONOR]
datum.pop(enrollment_modes.AUDIT)
return data
def get_mock_presenter_enrollment_trend(course_id, include_verified=True):
trend = get_mock_api_enrollment_data(course_id, include_verified=include_verified)
trend = _clean_modes(trend)
def get_mock_presenter_enrollment_trend(course_id):
trend = get_mock_api_enrollment_data(course_id)
return trend
......@@ -119,19 +100,13 @@ def get_mock_presenter_enrollment_trend_with_gaps_filled(course_id):
def get_mock_presenter_enrollment_data_small(course_id):
data = [_get_empty_enrollment('2014-01-30'), get_mock_api_enrollment_data(course_id)[-1]]
data = _clean_modes(data)
return data
def get_mock_presenter_enrollment_summary_small():
return {
'last_updated': CREATED_DATETIME,
'current_enrollment': 120,
'total_enrollment': 240,
'enrollment_change_last_7_days': None,
'verified_enrollment': 30,
}
summary = get_mock_enrollment_summary()
summary['enrollment_change_last_7_days'] = None
return summary
def get_mock_api_enrollment_geography_data(course_id):
......
......@@ -4,7 +4,6 @@ import urllib
from django.http import HttpResponse
from waffle import switch_is_active
from analyticsclient.constants import data_format, demographic
from analyticsclient.client import Client
......@@ -66,9 +65,8 @@ class CourseEnrollmentCSV(CSVResponseMixin, CourseView):
csv_filename_suffix = u'enrollment'
def get_data(self):
_demographic = 'mode' if switch_is_active('display_verified_enrollment') else None
end_date = datetime.datetime.utcnow().strftime(Client.DATE_FORMAT)
return self.course.enrollment(_demographic, data_format=data_format.CSV, end_date=end_date)
return self.course.enrollment('mode', data_format=data_format.CSV, end_date=end_date)
class CourseEngagementActivityTrendCSV(CSVResponseMixin, CourseView):
......
......@@ -11,40 +11,49 @@ require(['vendor/domReady!', 'load/init-page'], function (doc, page) {
'views/stacked-trends-view'],
function (DataTableView, StackedTrendsView) {
var settings = [
var colors = ['#4BB4FB', '#CA0061', '#CCCCCC'],
numericColumn = {
className: 'text-right',
type: 'number'
},
settings = [
{
key: 'date',
title: gettext('Date'),
type: 'date'
},
{
_.defaults({}, numericColumn, {
key: 'count',
title: gettext('Current Enrollment'),
className: 'text-right',
type: 'number',
color: '#4BB4FB'
},
{
color: colors[0]
}),
_.defaults({}, numericColumn, {
key: 'honor',
title: gettext('Honor Code'),
className: 'text-right',
type: 'number',
color: '#4BB4FB'
},
{
// Translators: this describe the learner's enrollment track (e.g. Honor certificate)
title: gettext('Honor'),
color: colors[0]
}),
_.defaults({}, numericColumn, {
key: 'audit',
title: gettext('Audit'),
color: colors[0]
}),
_.defaults({}, numericColumn, {
key: 'verified',
title: gettext('Verified'),
className: 'text-right',
type: 'number',
color: '#CA0061'
},
{
color: colors[1]
}),
_.defaults({}, numericColumn, {
key: 'professional',
title: gettext('Professional'),
className: 'text-right',
type: 'number',
color: '#CCCCCC'
}
color: colors[2]
}),
_.defaults({}, numericColumn, {
key: 'credit',
// Translators: this label indicates the learner has registered for academic credit
title: gettext('Verified with Credit'),
color: colors[2]
})
],
trendSettings,
enrollmentTrackTrendSettings;
......
......@@ -26,7 +26,7 @@ requests==2.10.0 # Apache 2.0
git+https://github.com/pinax/django-announcements.git@f85e690705e038a62407abe54ac195f60760934b#egg=django-announcements # MIT
git+https://github.com/edx/django-lang-pref-middleware.git@0.1.0#egg=django-lang-pref-middleware
git+https://github.com/edx/edx-analytics-data-api-client.git@0.6.1#egg=edx-analytics-data-api-client==0.6.1 # edX
git+https://github.com/edx/edx-analytics-data-api-client.git@0.8.0#egg=edx-analytics-data-api-client==0.8.0 # edX
git+https://github.com/edx/i18n-tools.git@0d7847f9dfa2281640527b4dc51f5854f950f9b7#egg=i18n_tools
git+https://github.com/edx/opaque-keys.git@d45d0bd8d64c69531be69178b9505b5d38806ce0#egg=opaque-keys
# custom opaque-key implementations for ccx
......
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