Commit 66914fa0 by Clinton Blackburn

Updated CSV filename

File names are now compatible with opaque keys.
parent af18880a
# 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 StringIO
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 analytics_data_api.v0.tests.utils import flatten
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 @@ class CourseViewTestCaseMixin(object):
"""
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 @@ class CourseViewTestCaseMixin(object):
"""
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 @@ class CourseViewTestCaseMixin(object):
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 @@ class CourseViewTestCaseMixin(object):
# 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,
# 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=self.course_id, interval_start=interval_start,
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 @@ class CourseActivityLastWeekTest(TestCaseWithAuthentication):
@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 @@ class CourseActivityLastWeekTest(TestCaseWithAuthentication):
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 @@ class CourseActivityLastWeekTest(TestCaseWithAuthentication):
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 @@ class AnswerDistributionTests(TestCaseWithAuthentication):
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 @@ class CourseEnrollmentViewTests(CourseEnrollmentViewTestCaseMixin, TestCaseWithA
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 @@ class CourseEnrollmentByLocationViewTests(CourseEnrollmentViewTestCaseMixin, Tes
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 = []
......
......@@ -8,6 +8,7 @@ from django.db.models import Max
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 @@ from analytics_data_api.v0 import models, serializers
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 @@ class BaseCourseView(generics.ListAPIView):
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
......
......@@ -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
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