From af18880a6c9cb50ef605340770cf4a68e133c302 Mon Sep 17 00:00:00 2001 From: Clinton Blackburn Date: Mon, 6 Oct 2014 11:30:32 -0400 Subject: [PATCH 1/2] Updated Course ID Regex This regex is nearly identical to the one used by LMS. The only difference is the internal capture groups have been replaced with character sets to avoid issues with the Swagger UI attempting to parse the internal capture groups and pass them along as arguments. --- analytics_data_api/v0/urls/courses.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/analytics_data_api/v0/urls/courses.py b/analytics_data_api/v0/urls/courses.py index 328efc21..22dc7d4a 100644 --- a/analytics_data_api/v0/urls/courses.py +++ b/analytics_data_api/v0/urls/courses.py @@ -1,10 +1,8 @@ -import re - from django.conf.urls import patterns, url from analytics_data_api.v0.views import courses as views - +COURSE_ID_PATTERN = r'(?P[^/+]+[/+][^/+]+[/+][^/]+)' COURSE_URLS = [ ('activity', views.CourseActivityWeeklyView, 'activity'), ('recent_activity', views.CourseActivityMostRecentWeekView, 'recent_activity'), @@ -18,4 +16,5 @@ urlpatterns = [] for path, view, name in COURSE_URLS: - urlpatterns += patterns('', url(r'^(?P.+)/' + re.escape(path) + r'/$', view.as_view(), name=name)) + regex = r'^{0}/{1}/$'.format(COURSE_ID_PATTERN, path) + urlpatterns += patterns('', url(regex, view.as_view(), name=name)) From 66914fa0087ec7b3f19c19c0c05d304dc733e86a Mon Sep 17 00:00:00 2001 From: Clinton Blackburn Date: Tue, 7 Oct 2014 16:11:07 -0400 Subject: [PATCH 2/2] Updated CSV filename File names are now compatible with opaque keys. --- analytics_data_api/v0/tests/test_views.py | 209 ++++++++++++++-------- analytics_data_api/v0/views/courses.py | 27 ++- requirements/base.txt | 1 + 3 files changed, 162 insertions(+), 75 deletions(-) diff --git a/analytics_data_api/v0/tests/test_views.py b/analytics_data_api/v0/tests/test_views.py index 0d17efc3..a166f7cd 100644 --- a/analytics_data_api/v0/tests/test_views.py +++ b/analytics_data_api/v0/tests/test_views.py @@ -1,3 +1,4 @@ +# coding=utf-8 # NOTE: Full URLs are used throughout these tests to ensure that the API contract is fulfilled. The URLs should *not* # change for versions greater than 1.0.0. Tests target a specific version of the API, additional tests should be added # for subsequent versions if there are breaking changes introduced in those versions. @@ -5,11 +6,13 @@ import csv import datetime from itertools import groupby +import urllib from django.conf import settings from django_dynamic_fixture import G from iso3166 import countries import pytz +from opaque_keys.edx.keys import CourseKey from analytics_data_api.v0 import models from analytics_data_api.v0.constants import UNKNOWN_COUNTRY, UNKNOWN_COUNTRY_CODE @@ -19,12 +22,29 @@ from analyticsdataserver.tests import TestCaseWithAuthentication +DEMO_COURSE_ID = u'course-v1:edX+DemoX+Demo_2014' + + +class DemoCourseMixin(object): + course_key = None + course_id = None + + def setUp(self): + self.course_id = DEMO_COURSE_ID + self.course_key = CourseKey.from_string(self.course_id) + super(DemoCourseMixin, self).setUp() + + # pylint: disable=no-member -class CourseViewTestCaseMixin(object): +class CourseViewTestCaseMixin(DemoCourseMixin): model = None api_root_path = '/api/v0/' path = None order_by = [] + csv_filename_slug = None + + def generate_data(self, course_id=None): + raise NotImplementedError def format_as_response(self, *args): """ @@ -35,7 +55,7 @@ def format_as_response(self, *args): """ raise NotImplementedError - def get_latest_data(self): + def get_latest_data(self, course_id=None): """ Return the latest row/rows that would be returned if a user made a call to the endpoint with no date filtering. @@ -44,34 +64,37 @@ def get_latest_data(self): """ raise NotImplementedError + def get_csv_filename(self): + return u'edX-DemoX-Demo_2014--{0}.csv'.format(self.csv_filename_slug) + def test_get_not_found(self): """ Requests made against non-existent courses should return a 404 """ - course_id = 'edX/DemoX/Non_Existent_Course' - response = self.authenticated_get('%scourses/%s%s' % (self.api_root_path, course_id, self.path)) + course_id = u'edX/DemoX/Non_Existent_Course' + response = self.authenticated_get(u'%scourses/%s%s' % (self.api_root_path, course_id, self.path)) self.assertEquals(response.status_code, 404) def test_get(self): """ Verify the endpoint returns an HTTP 200 status and the correct data. """ # Validate the basic response status - response = self.authenticated_get('%scourses/%s%s' % (self.api_root_path, self.course_id, self.path)) + response = self.authenticated_get(u'%scourses/%s%s' % (self.api_root_path, self.course_id, self.path)) self.assertEquals(response.status_code, 200) # Validate the data is correct and sorted chronologically expected = self.format_as_response(*self.get_latest_data()) self.assertEquals(response.data, expected) - def test_get_csv(self): - """ Verify the endpoint returns data that has been properly converted to CSV. """ - path = '%scourses/%s%s' % (self.api_root_path, self.course_id, self.path) + def assertCSVIsValid(self, course_id, filename): + path = u'{0}courses/{1}{2}'.format(self.api_root_path, course_id, self.path) csv_content_type = 'text/csv' response = self.authenticated_get(path, HTTP_ACCEPT=csv_content_type) - # Validate the basic response status and content code + # Validate the basic response status, content type, and filename self.assertEquals(response.status_code, 200) self.assertEquals(response['Content-Type'].split(';')[0], csv_content_type) + self.assertEquals(response['Content-Disposition'], u'attachment; filename={}'.format(filename)) # Validate the actual data - data = self.format_as_response(*self.get_latest_data()) + data = self.format_as_response(*self.get_latest_data(course_id=course_id)) data = map(flatten, data) # The CSV renderer sorts the headers alphabetically @@ -82,9 +105,21 @@ def test_get_csv(self): writer = csv.DictWriter(expected, fieldnames) writer.writeheader() writer.writerows(data) - self.assertEqual(response.content, expected.getvalue()) + def test_get_csv(self): + """ Verify the endpoint returns data that has been properly converted to CSV. """ + self.assertCSVIsValid(self.course_id, self.get_csv_filename()) + + def test_get_csv_with_deprecated_key(self): + """ + Verify the endpoint returns data that has been properly converted to CSV even if the course ID is deprecated. + """ + course_id = u'edX/DemoX/Demo_Course' + self.generate_data(course_id) + filename = u'{0}--{1}.csv'.format(u'edX-DemoX-Demo_Course', self.csv_filename_slug) + self.assertCSVIsValid(course_id, filename) + def test_get_with_intervals(self): """ Verify the endpoint returns multiple data points when supplied with an interval of dates. """ raise NotImplementedError @@ -115,46 +150,50 @@ def assertIntervalFilteringWorks(self, expected_response, start_date, end_date): # pylint: disable=abstract-method class CourseEnrollmentViewTestCaseMixin(CourseViewTestCaseMixin): + date = None + def setUp(self): super(CourseEnrollmentViewTestCaseMixin, self).setUp() - self.course_id = 'edX/DemoX/Demo_Course' self.date = datetime.date(2014, 1, 1) - def get_latest_data(self): - return self.model.objects.filter(date=self.date).order_by('date', *self.order_by) + def get_latest_data(self, course_id=None): + course_id = course_id or self.course_id + return self.model.objects.filter(course_id=course_id, date=self.date).order_by('date', *self.order_by) def test_get_with_intervals(self): expected = self.format_as_response(*self.model.objects.filter(date=self.date)) self.assertIntervalFilteringWorks(expected, self.date, self.date + datetime.timedelta(days=1)) -class CourseActivityLastWeekTest(TestCaseWithAuthentication): - # pylint: disable=line-too-long - def setUp(self): - super(CourseActivityLastWeekTest, self).setUp() - self.course_id = 'edX/DemoX/Demo_Course' +class CourseActivityLastWeekTest(DemoCourseMixin, TestCaseWithAuthentication): + def generate_data(self, course_id=None): + course_id = course_id or self.course_id interval_start = datetime.datetime(2014, 1, 1, tzinfo=pytz.utc) interval_end = interval_start + datetime.timedelta(weeks=1) - # G(models.CourseActivityWeekly, course_id=self.course_id, interval_start=interval_start, - # interval_end=interval_end, - # activity_type='POSTED_FORUM', count=100) - G(models.CourseActivityWeekly, course_id=self.course_id, interval_start=interval_start, + # G(models.CourseActivityWeekly, course_id=course_id, interval_start=interval_start, + # interval_end=interval_end, + # activity_type='POSTED_FORUM', count=100) + G(models.CourseActivityWeekly, course_id=course_id, interval_start=interval_start, interval_end=interval_end, activity_type='ATTEMPTED_PROBLEM', count=200) - G(models.CourseActivityWeekly, course_id=self.course_id, interval_start=interval_start, + G(models.CourseActivityWeekly, course_id=course_id, interval_start=interval_start, interval_end=interval_end, activity_type='ACTIVE', count=300) - G(models.CourseActivityWeekly, course_id=self.course_id, interval_start=interval_start, + G(models.CourseActivityWeekly, course_id=course_id, interval_start=interval_start, interval_end=interval_end, activity_type='PLAYED_VIDEO', count=400) + def setUp(self): + super(CourseActivityLastWeekTest, self).setUp() + self.generate_data() + def test_activity(self): - response = self.authenticated_get('/api/v0/courses/{0}/recent_activity'.format(self.course_id)) + response = self.authenticated_get(u'/api/v0/courses/{0}/recent_activity'.format(self.course_id)) self.assertEquals(response.status_code, 200) self.assertEquals(response.data, self.get_activity_record()) def assertValidActivityResponse(self, activity_type, count): - response = self.authenticated_get('/api/v0/courses/{0}/recent_activity?activity_type={1}'.format( + response = self.authenticated_get(u'/api/v0/courses/{0}/recent_activity?activity_type={1}'.format( self.course_id, activity_type)) self.assertEquals(response.status_code, 200) self.assertEquals(response.data, self.get_activity_record(activity_type=activity_type, count=count)) @@ -162,7 +201,7 @@ def assertValidActivityResponse(self, activity_type, count): @staticmethod def get_activity_record(**kwargs): default = { - 'course_id': 'edX/DemoX/Demo_Course', + 'course_id': DEMO_COURSE_ID, 'interval_start': datetime.datetime(2014, 1, 1, 0, 0, tzinfo=pytz.utc), 'interval_end': datetime.datetime(2014, 1, 8, 0, 0, tzinfo=pytz.utc), 'activity_type': 'any', @@ -173,11 +212,12 @@ def get_activity_record(**kwargs): return default def test_activity_auth(self): - response = self.client.get('/api/v0/courses/{0}/recent_activity'.format(self.course_id), follow=True) + response = self.client.get(u'/api/v0/courses/{0}/recent_activity'.format(self.course_id), follow=True) self.assertEquals(response.status_code, 401) def test_url_encoded_course_id(self): - response = self.authenticated_get('/api/v0/courses/edX%2FDemoX%2FDemo_Course/recent_activity') + url_encoded_course_id = urllib.quote_plus(self.course_id) + response = self.authenticated_get(u'/api/v0/courses/{}/recent_activity'.format(url_encoded_course_id)) self.assertEquals(response.status_code, 200) self.assertEquals(response.data, self.get_activity_record()) @@ -190,21 +230,21 @@ def test_video_activity(self): def test_unknown_activity(self): activity_type = 'missing_activity_type' - response = self.authenticated_get('/api/v0/courses/{0}/recent_activity?activity_type={1}'.format( + response = self.authenticated_get(u'/api/v0/courses/{0}/recent_activity?activity_type={1}'.format( self.course_id, activity_type)) self.assertEquals(response.status_code, 404) def test_unknown_course_id(self): - response = self.authenticated_get('/api/v0/courses/{0}/recent_activity'.format('foo')) + response = self.authenticated_get(u'/api/v0/courses/{0}/recent_activity'.format('foo')) self.assertEquals(response.status_code, 404) def test_missing_course_id(self): - response = self.authenticated_get('/api/v0/courses/recent_activity') + response = self.authenticated_get(u'/api/v0/courses/recent_activity') self.assertEquals(response.status_code, 404) def test_label_parameter(self): activity_type = 'played_video' - response = self.authenticated_get('/api/v0/courses/{0}/recent_activity?label={1}'.format( + response = self.authenticated_get(u'/api/v0/courses/{0}/recent_activity?label={1}'.format( self.course_id, activity_type)) self.assertEquals(response.status_code, 200) self.assertEquals(response.data, self.get_activity_record(activity_type=activity_type, count=400)) @@ -214,17 +254,22 @@ class CourseEnrollmentByBirthYearViewTests(CourseEnrollmentViewTestCaseMixin, Te path = '/enrollment/birth_year' model = models.CourseEnrollmentByBirthYear order_by = ['birth_year'] + csv_filename_slug = u'enrollment-age' + + def generate_data(self, course_id=None): + course_id = course_id or self.course_id + G(self.model, course_id=course_id, date=self.date, birth_year=1956) + G(self.model, course_id=course_id, date=self.date, birth_year=1986) + G(self.model, course_id=course_id, date=self.date - datetime.timedelta(days=10), birth_year=1956) + G(self.model, course_id=course_id, date=self.date - datetime.timedelta(days=10), birth_year=1986) def setUp(self): super(CourseEnrollmentByBirthYearViewTests, self).setUp() - G(self.model, course_id=self.course_id, date=self.date, birth_year=1956) - G(self.model, course_id=self.course_id, date=self.date, birth_year=1986) - G(self.model, course_id=self.course_id, date=self.date - datetime.timedelta(days=10), birth_year=1956) - G(self.model, course_id=self.course_id, date=self.date - datetime.timedelta(days=10), birth_year=1986) + self.generate_data() def format_as_response(self, *args): return [ - {'course_id': str(ce.course_id), 'count': ce.count, 'date': ce.date.strftime(settings.DATE_FORMAT), + {'course_id': unicode(ce.course_id), 'count': ce.count, 'date': ce.date.strftime(settings.DATE_FORMAT), 'birth_year': ce.birth_year, 'created': ce.created.strftime(settings.DATETIME_FORMAT)} for ce in args] def test_get(self): @@ -239,19 +284,23 @@ class CourseEnrollmentByEducationViewTests(CourseEnrollmentViewTestCaseMixin, Te path = '/enrollment/education/' model = models.CourseEnrollmentByEducation order_by = ['education_level'] + csv_filename_slug = u'enrollment-education' + + def generate_data(self, course_id=None): + course_id = course_id or self.course_id + G(self.model, course_id=course_id, date=self.date, education_level=self.el1) + G(self.model, course_id=course_id, date=self.date, education_level=self.el2) + G(self.model, course_id=course_id, date=self.date - datetime.timedelta(days=2), education_level=self.el2) def setUp(self): super(CourseEnrollmentByEducationViewTests, self).setUp() self.el1 = G(models.EducationLevel, name='Doctorate', short_name='doctorate') self.el2 = G(models.EducationLevel, name='Top Secret', short_name='top_secret') - G(self.model, course_id=self.course_id, date=self.date, education_level=self.el1) - G(self.model, course_id=self.course_id, date=self.date, education_level=self.el2) - G(self.model, course_id=self.course_id, date=self.date - datetime.timedelta(days=2), - education_level=self.el2) + self.generate_data() def format_as_response(self, *args): return [ - {'course_id': str(ce.course_id), 'count': ce.count, 'date': ce.date.strftime(settings.DATE_FORMAT), + {'course_id': unicode(ce.course_id), 'count': ce.count, 'date': ce.date.strftime(settings.DATE_FORMAT), 'education_level': {'name': ce.education_level.name, 'short_name': ce.education_level.short_name}, 'created': ce.created.strftime(settings.DATETIME_FORMAT)} for ce in args] @@ -261,16 +310,21 @@ class CourseEnrollmentByGenderViewTests(CourseEnrollmentViewTestCaseMixin, TestC path = '/enrollment/gender/' model = models.CourseEnrollmentByGender order_by = ['gender'] + csv_filename_slug = u'enrollment-gender' + + def generate_data(self, course_id=None): + course_id = course_id or self.course_id + G(self.model, course_id=course_id, gender='m', date=self.date, count=34) + G(self.model, course_id=course_id, gender='f', date=self.date, count=45) + G(self.model, course_id=course_id, gender='f', date=self.date - datetime.timedelta(days=2), count=45) def setUp(self): super(CourseEnrollmentByGenderViewTests, self).setUp() - G(self.model, course_id=self.course_id, gender='m', date=self.date, count=34) - G(self.model, course_id=self.course_id, gender='f', date=self.date, count=45) - G(self.model, course_id=self.course_id, gender='f', date=self.date - datetime.timedelta(days=2), count=45) + self.generate_data() def format_as_response(self, *args): return [ - {'course_id': str(ce.course_id), 'count': ce.count, 'date': ce.date.strftime(settings.DATE_FORMAT), + {'course_id': unicode(ce.course_id), 'count': ce.count, 'date': ce.date.strftime(settings.DATE_FORMAT), 'gender': ce.gender, 'created': ce.created.strftime(settings.DATETIME_FORMAT)} for ce in args] @@ -308,15 +362,20 @@ def test_get_404(self): class CourseEnrollmentViewTests(CourseEnrollmentViewTestCaseMixin, TestCaseWithAuthentication): model = models.CourseEnrollmentDaily path = '/enrollment' + csv_filename_slug = u'enrollment' + + def generate_data(self, course_id=None): + course_id = course_id or self.course_id + G(self.model, course_id=course_id, date=self.date, count=203) + G(self.model, course_id=course_id, date=self.date - datetime.timedelta(days=5), count=203) def setUp(self): super(CourseEnrollmentViewTests, self).setUp() - G(self.model, course_id=self.course_id, date=self.date, count=203) - G(self.model, course_id=self.course_id, date=self.date - datetime.timedelta(days=5), count=203) + self.generate_data() def format_as_response(self, *args): return [ - {'course_id': str(ce.course_id), 'count': ce.count, 'date': ce.date.strftime(settings.DATE_FORMAT), + {'course_id': unicode(ce.course_id), 'count': ce.count, 'date': ce.date.strftime(settings.DATE_FORMAT), 'created': ce.created.strftime(settings.DATETIME_FORMAT)} for ce in args] @@ -324,6 +383,7 @@ def format_as_response(self, *args): class CourseEnrollmentByLocationViewTests(CourseEnrollmentViewTestCaseMixin, TestCaseWithAuthentication): path = '/enrollment/location/' model = models.CourseEnrollmentByCountry + csv_filename_slug = u'enrollment-location' def format_as_response(self, *args): unknown = {'course_id': None, 'count': 0, 'date': None, @@ -341,26 +401,29 @@ def format_as_response(self, *args): response = [unknown] response += [ - {'course_id': str(ce.course_id), 'count': ce.count, 'date': ce.date.strftime(settings.DATE_FORMAT), + {'course_id': unicode(ce.course_id), 'count': ce.count, 'date': ce.date.strftime(settings.DATE_FORMAT), 'country': {'alpha2': ce.country.alpha2, 'alpha3': ce.country.alpha3, 'name': ce.country.name}, 'created': ce.created.strftime(settings.DATETIME_FORMAT)} for ce in args] return response + def generate_data(self, course_id=None): + course_id = course_id or self.course_id + G(self.model, course_id=course_id, country_code='US', count=455, date=self.date) + G(self.model, course_id=course_id, country_code='CA', count=356, date=self.date) + G(self.model, course_id=course_id, country_code='IN', count=12, date=self.date - datetime.timedelta(days=29)) + G(self.model, course_id=course_id, country_code='', count=356, date=self.date) + G(self.model, course_id=course_id, country_code='A1', count=1, date=self.date) + G(self.model, course_id=course_id, country_code='A2', count=2, date=self.date) + G(self.model, course_id=course_id, country_code='AP', count=1, date=self.date) + G(self.model, course_id=course_id, country_code='EU', count=4, date=self.date) + G(self.model, course_id=course_id, country_code='O1', count=7, date=self.date) + def setUp(self): super(CourseEnrollmentByLocationViewTests, self).setUp() self.country = countries.get('US') - G(self.model, course_id=self.course_id, country_code='US', count=455, date=self.date) - G(self.model, course_id=self.course_id, country_code='CA', count=356, date=self.date) - G(self.model, course_id=self.course_id, country_code='IN', count=12, - date=self.date - datetime.timedelta(days=29)) - G(self.model, course_id=self.course_id, country_code='', count=356, date=self.date) - G(self.model, course_id=self.course_id, country_code='A1', count=1, date=self.date) - G(self.model, course_id=self.course_id, country_code='A2', count=2, date=self.date) - G(self.model, course_id=self.course_id, country_code='AP', count=1, date=self.date) - G(self.model, course_id=self.course_id, country_code='EU', count=4, date=self.date) - G(self.model, course_id=self.course_id, country_code='O1', count=7, date=self.date) + self.generate_data() class CourseActivityWeeklyViewTests(CourseViewTestCaseMixin, TestCaseWithAuthentication): @@ -369,23 +432,29 @@ class CourseActivityWeeklyViewTests(CourseViewTestCaseMixin, TestCaseWithAuthent model = CourseActivityWeekly # activity_types = ['ACTIVE', 'ATTEMPTED_PROBLEM', 'PLAYED_VIDEO', 'POSTED_FORUM'] activity_types = ['ACTIVE', 'ATTEMPTED_PROBLEM', 'PLAYED_VIDEO'] + csv_filename_slug = u'engagement-activity' - def setUp(self): - super(CourseActivityWeeklyViewTests, self).setUp() - self.course_id = 'edX/DemoX/Demo_Course' - self.interval_start = datetime.datetime(2014, 1, 1, tzinfo=pytz.utc) - self.interval_end = self.interval_start + datetime.timedelta(weeks=1) + def generate_data(self, course_id=None): + course_id = course_id or self.course_id for activity_type in self.activity_types: G(CourseActivityWeekly, - course_id=self.course_id, + course_id=course_id, interval_start=self.interval_start, interval_end=self.interval_end, activity_type=activity_type, count=100) - def get_latest_data(self): - return self.model.objects.filter(course_id=self.course_id, interval_end=self.interval_end) + def setUp(self): + super(CourseActivityWeeklyViewTests, self).setUp() + self.interval_start = datetime.datetime(2014, 1, 1, tzinfo=pytz.utc) + self.interval_end = self.interval_start + datetime.timedelta(weeks=1) + + self.generate_data() + + def get_latest_data(self, course_id=None): + course_id = course_id or self.course_id + return self.model.objects.filter(course_id=course_id, interval_end=self.interval_end) def format_as_response(self, *args): response = [] diff --git a/analytics_data_api/v0/views/courses.py b/analytics_data_api/v0/views/courses.py index c553200f..66ce9928 100644 --- a/analytics_data_api/v0/views/courses.py +++ b/analytics_data_api/v0/views/courses.py @@ -8,6 +8,7 @@ from django.http import Http404 from django.utils.timezone import make_aware, utc from rest_framework import generics +from opaque_keys.edx.keys import CourseKey from analytics_data_api.v0 import models, serializers @@ -15,8 +16,11 @@ class BaseCourseView(generics.ListAPIView): start_date = None end_date = None + course_id = None + slug = None def get(self, request, *args, **kwargs): + self.course_id = self.kwargs.get('course_id') start_date = request.QUERY_PARAMS.get('start_date') end_date = request.QUERY_PARAMS.get('end_date') timezone = utc @@ -44,12 +48,21 @@ def apply_date_filtering(self, queryset): raise NotImplementedError def get_queryset(self): - course_id = self.kwargs.get('course_id') - self.verify_course_exists_or_404(course_id) - queryset = self.model.objects.filter(course_id=course_id) + self.verify_course_exists_or_404(self.course_id) + queryset = self.model.objects.filter(course_id=self.course_id) queryset = self.apply_date_filtering(queryset) return queryset + def get_csv_filename(self): + course_key = CourseKey.from_string(self.course_id) + course_id = u'-'.join([course_key.org, course_key.course, course_key.run]) + return u'{0}--{1}.csv'.format(course_id, self.slug) + + def finalize_response(self, request, response, *args, **kwargs): + if request.META.get('HTTP_ACCEPT') == u'text/csv': + response['Content-Disposition'] = u'attachment; filename={}'.format(self.get_csv_filename()) + return super(BaseCourseView, self).finalize_response(request, response, *args, **kwargs) + # pylint: disable=line-too-long class CourseActivityWeeklyView(BaseCourseView): @@ -80,6 +93,7 @@ class CourseActivityWeeklyView(BaseCourseView): end_date -- Date before which all data should be returned (exclusive) """ + slug = u'engagement-activity' model = models.CourseActivityWeekly serializer_class = serializers.CourseActivityWeeklySerializer @@ -244,6 +258,7 @@ class CourseEnrollmentByBirthYearView(BaseCourseEnrollmentView): end_date -- Date before which all data should be returned (exclusive) """ + slug = u'enrollment-age' serializer_class = serializers.CourseEnrollmentByBirthYearSerializer model = models.CourseEnrollmentByBirthYear @@ -263,6 +278,7 @@ class CourseEnrollmentByEducationView(BaseCourseEnrollmentView): start_date -- Date after which all data should be returned (inclusive) end_date -- Date before which all data should be returned (exclusive) """ + slug = u'enrollment-education' serializer_class = serializers.CourseEnrollmentByEducationSerializer model = models.CourseEnrollmentByEducation @@ -287,6 +303,7 @@ class CourseEnrollmentByGenderView(BaseCourseEnrollmentView): start_date -- Date after which all data should be returned (inclusive) end_date -- Date before which all data should be returned (exclusive) """ + slug = u'enrollment-gender' serializer_class = serializers.CourseEnrollmentByGenderSerializer model = models.CourseEnrollmentByGender @@ -304,7 +321,7 @@ class CourseEnrollmentView(BaseCourseEnrollmentView): start_date -- Date after which all data should be returned (inclusive) end_date -- Date before which all data should be returned (exclusive) """ - + slug = u'enrollment' serializer_class = serializers.CourseEnrollmentDailySerializer model = models.CourseEnrollmentDaily @@ -329,7 +346,7 @@ class CourseEnrollmentByLocationView(BaseCourseEnrollmentView): start_date -- Date after which all data should be returned (inclusive) end_date -- Date before which all data should be returned (exclusive) """ - + slug = u'enrollment-location' serializer_class = serializers.CourseEnrollmentByCountrySerializer model = models.CourseEnrollmentByCountry diff --git a/requirements/base.txt b/requirements/base.txt index 02309369..df384538 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -7,3 +7,4 @@ ipython==2.1.0 # BSD django-rest-swagger==0.1.14 # BSD djangorestframework-csv==1.3.3 # BSD iso3166==0.1 # MIT +-e git+https://github.com/edx/opaque-keys.git@d45d0bd8d64c69531be69178b9505b5d38806ce0#egg=opaque-keys