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* # 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 # 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. # for subsequent versions if there are breaking changes introduced in those versions.
...@@ -5,11 +6,13 @@ import StringIO ...@@ -5,11 +6,13 @@ import StringIO
import csv import csv
import datetime import datetime
from itertools import groupby from itertools import groupby
import urllib
from django.conf import settings from django.conf import settings
from django_dynamic_fixture import G from django_dynamic_fixture import G
from iso3166 import countries from iso3166 import countries
import pytz import pytz
from opaque_keys.edx.keys import CourseKey
from analytics_data_api.v0 import models from analytics_data_api.v0 import models
from analytics_data_api.v0.constants import UNKNOWN_COUNTRY, UNKNOWN_COUNTRY_CODE 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 ...@@ -19,12 +22,29 @@ from analytics_data_api.v0.tests.utils import flatten
from analyticsdataserver.tests import TestCaseWithAuthentication 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 # pylint: disable=no-member
class CourseViewTestCaseMixin(object): class CourseViewTestCaseMixin(DemoCourseMixin):
model = None model = None
api_root_path = '/api/v0/' api_root_path = '/api/v0/'
path = None path = None
order_by = [] order_by = []
csv_filename_slug = None
def generate_data(self, course_id=None):
raise NotImplementedError
def format_as_response(self, *args): def format_as_response(self, *args):
""" """
...@@ -35,7 +55,7 @@ class CourseViewTestCaseMixin(object): ...@@ -35,7 +55,7 @@ class CourseViewTestCaseMixin(object):
""" """
raise NotImplementedError 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 Return the latest row/rows that would be returned if a user made a call
to the endpoint with no date filtering. to the endpoint with no date filtering.
...@@ -44,34 +64,37 @@ class CourseViewTestCaseMixin(object): ...@@ -44,34 +64,37 @@ class CourseViewTestCaseMixin(object):
""" """
raise NotImplementedError 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): def test_get_not_found(self):
""" Requests made against non-existent courses should return a 404 """ """ Requests made against non-existent courses should return a 404 """
course_id = 'edX/DemoX/Non_Existent_Course' course_id = u'edX/DemoX/Non_Existent_Course'
response = self.authenticated_get('%scourses/%s%s' % (self.api_root_path, course_id, self.path)) response = self.authenticated_get(u'%scourses/%s%s' % (self.api_root_path, course_id, self.path))
self.assertEquals(response.status_code, 404) self.assertEquals(response.status_code, 404)
def test_get(self): def test_get(self):
""" Verify the endpoint returns an HTTP 200 status and the correct data. """ """ Verify the endpoint returns an HTTP 200 status and the correct data. """
# Validate the basic response status # 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) self.assertEquals(response.status_code, 200)
# Validate the data is correct and sorted chronologically # Validate the data is correct and sorted chronologically
expected = self.format_as_response(*self.get_latest_data()) expected = self.format_as_response(*self.get_latest_data())
self.assertEquals(response.data, expected) self.assertEquals(response.data, expected)
def test_get_csv(self): def assertCSVIsValid(self, course_id, filename):
""" Verify the endpoint returns data that has been properly converted to CSV. """ path = u'{0}courses/{1}{2}'.format(self.api_root_path, course_id, self.path)
path = '%scourses/%s%s' % (self.api_root_path, self.course_id, self.path)
csv_content_type = 'text/csv' csv_content_type = 'text/csv'
response = self.authenticated_get(path, HTTP_ACCEPT=csv_content_type) 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.status_code, 200)
self.assertEquals(response['Content-Type'].split(';')[0], csv_content_type) self.assertEquals(response['Content-Type'].split(';')[0], csv_content_type)
self.assertEquals(response['Content-Disposition'], u'attachment; filename={}'.format(filename))
# Validate the actual data # 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) data = map(flatten, data)
# The CSV renderer sorts the headers alphabetically # The CSV renderer sorts the headers alphabetically
...@@ -82,9 +105,21 @@ class CourseViewTestCaseMixin(object): ...@@ -82,9 +105,21 @@ class CourseViewTestCaseMixin(object):
writer = csv.DictWriter(expected, fieldnames) writer = csv.DictWriter(expected, fieldnames)
writer.writeheader() writer.writeheader()
writer.writerows(data) writer.writerows(data)
self.assertEqual(response.content, expected.getvalue()) 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): def test_get_with_intervals(self):
""" Verify the endpoint returns multiple data points when supplied with an interval of dates. """ """ Verify the endpoint returns multiple data points when supplied with an interval of dates. """
raise NotImplementedError raise NotImplementedError
...@@ -115,46 +150,50 @@ class CourseViewTestCaseMixin(object): ...@@ -115,46 +150,50 @@ class CourseViewTestCaseMixin(object):
# pylint: disable=abstract-method # pylint: disable=abstract-method
class CourseEnrollmentViewTestCaseMixin(CourseViewTestCaseMixin): class CourseEnrollmentViewTestCaseMixin(CourseViewTestCaseMixin):
date = None
def setUp(self): def setUp(self):
super(CourseEnrollmentViewTestCaseMixin, self).setUp() super(CourseEnrollmentViewTestCaseMixin, self).setUp()
self.course_id = 'edX/DemoX/Demo_Course'
self.date = datetime.date(2014, 1, 1) self.date = datetime.date(2014, 1, 1)
def get_latest_data(self): def get_latest_data(self, course_id=None):
return self.model.objects.filter(date=self.date).order_by('date', *self.order_by) 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): def test_get_with_intervals(self):
expected = self.format_as_response(*self.model.objects.filter(date=self.date)) expected = self.format_as_response(*self.model.objects.filter(date=self.date))
self.assertIntervalFilteringWorks(expected, self.date, self.date + datetime.timedelta(days=1)) self.assertIntervalFilteringWorks(expected, self.date, self.date + datetime.timedelta(days=1))
class CourseActivityLastWeekTest(TestCaseWithAuthentication): class CourseActivityLastWeekTest(DemoCourseMixin, TestCaseWithAuthentication):
# pylint: disable=line-too-long def generate_data(self, course_id=None):
def setUp(self): course_id = course_id or self.course_id
super(CourseActivityLastWeekTest, self).setUp()
self.course_id = 'edX/DemoX/Demo_Course'
interval_start = datetime.datetime(2014, 1, 1, tzinfo=pytz.utc) interval_start = datetime.datetime(2014, 1, 1, tzinfo=pytz.utc)
interval_end = interval_start + datetime.timedelta(weeks=1) 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, # interval_end=interval_end,
# activity_type='POSTED_FORUM', count=100) # 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, interval_end=interval_end,
activity_type='ATTEMPTED_PROBLEM', count=200) 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, interval_end=interval_end,
activity_type='ACTIVE', count=300) 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, interval_end=interval_end,
activity_type='PLAYED_VIDEO', count=400) activity_type='PLAYED_VIDEO', count=400)
def setUp(self):
super(CourseActivityLastWeekTest, self).setUp()
self.generate_data()
def test_activity(self): 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.status_code, 200)
self.assertEquals(response.data, self.get_activity_record()) self.assertEquals(response.data, self.get_activity_record())
def assertValidActivityResponse(self, activity_type, count): 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.course_id, activity_type))
self.assertEquals(response.status_code, 200) self.assertEquals(response.status_code, 200)
self.assertEquals(response.data, self.get_activity_record(activity_type=activity_type, count=count)) self.assertEquals(response.data, self.get_activity_record(activity_type=activity_type, count=count))
...@@ -162,7 +201,7 @@ class CourseActivityLastWeekTest(TestCaseWithAuthentication): ...@@ -162,7 +201,7 @@ class CourseActivityLastWeekTest(TestCaseWithAuthentication):
@staticmethod @staticmethod
def get_activity_record(**kwargs): def get_activity_record(**kwargs):
default = { 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_start': datetime.datetime(2014, 1, 1, 0, 0, tzinfo=pytz.utc),
'interval_end': datetime.datetime(2014, 1, 8, 0, 0, tzinfo=pytz.utc), 'interval_end': datetime.datetime(2014, 1, 8, 0, 0, tzinfo=pytz.utc),
'activity_type': 'any', 'activity_type': 'any',
...@@ -173,11 +212,12 @@ class CourseActivityLastWeekTest(TestCaseWithAuthentication): ...@@ -173,11 +212,12 @@ class CourseActivityLastWeekTest(TestCaseWithAuthentication):
return default return default
def test_activity_auth(self): 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) self.assertEquals(response.status_code, 401)
def test_url_encoded_course_id(self): 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.status_code, 200)
self.assertEquals(response.data, self.get_activity_record()) self.assertEquals(response.data, self.get_activity_record())
...@@ -190,21 +230,21 @@ class CourseActivityLastWeekTest(TestCaseWithAuthentication): ...@@ -190,21 +230,21 @@ class CourseActivityLastWeekTest(TestCaseWithAuthentication):
def test_unknown_activity(self): def test_unknown_activity(self):
activity_type = 'missing_activity_type' 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.course_id, activity_type))
self.assertEquals(response.status_code, 404) self.assertEquals(response.status_code, 404)
def test_unknown_course_id(self): 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) self.assertEquals(response.status_code, 404)
def test_missing_course_id(self): 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) self.assertEquals(response.status_code, 404)
def test_label_parameter(self): def test_label_parameter(self):
activity_type = 'played_video' 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.course_id, activity_type))
self.assertEquals(response.status_code, 200) self.assertEquals(response.status_code, 200)
self.assertEquals(response.data, self.get_activity_record(activity_type=activity_type, count=400)) self.assertEquals(response.data, self.get_activity_record(activity_type=activity_type, count=400))
...@@ -214,17 +254,22 @@ class CourseEnrollmentByBirthYearViewTests(CourseEnrollmentViewTestCaseMixin, Te ...@@ -214,17 +254,22 @@ class CourseEnrollmentByBirthYearViewTests(CourseEnrollmentViewTestCaseMixin, Te
path = '/enrollment/birth_year' path = '/enrollment/birth_year'
model = models.CourseEnrollmentByBirthYear model = models.CourseEnrollmentByBirthYear
order_by = ['birth_year'] 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): def setUp(self):
super(CourseEnrollmentByBirthYearViewTests, self).setUp() super(CourseEnrollmentByBirthYearViewTests, self).setUp()
G(self.model, course_id=self.course_id, date=self.date, birth_year=1956) self.generate_data()
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)
def format_as_response(self, *args): def format_as_response(self, *args):
return [ 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] 'birth_year': ce.birth_year, 'created': ce.created.strftime(settings.DATETIME_FORMAT)} for ce in args]
def test_get(self): def test_get(self):
...@@ -239,19 +284,23 @@ class CourseEnrollmentByEducationViewTests(CourseEnrollmentViewTestCaseMixin, Te ...@@ -239,19 +284,23 @@ class CourseEnrollmentByEducationViewTests(CourseEnrollmentViewTestCaseMixin, Te
path = '/enrollment/education/' path = '/enrollment/education/'
model = models.CourseEnrollmentByEducation model = models.CourseEnrollmentByEducation
order_by = ['education_level'] 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): def setUp(self):
super(CourseEnrollmentByEducationViewTests, self).setUp() super(CourseEnrollmentByEducationViewTests, self).setUp()
self.el1 = G(models.EducationLevel, name='Doctorate', short_name='doctorate') self.el1 = G(models.EducationLevel, name='Doctorate', short_name='doctorate')
self.el2 = G(models.EducationLevel, name='Top Secret', short_name='top_secret') 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) self.generate_data()
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)
def format_as_response(self, *args): def format_as_response(self, *args):
return [ 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}, 'education_level': {'name': ce.education_level.name, 'short_name': ce.education_level.short_name},
'created': ce.created.strftime(settings.DATETIME_FORMAT)} for 'created': ce.created.strftime(settings.DATETIME_FORMAT)} for
ce in args] ce in args]
...@@ -261,16 +310,21 @@ class CourseEnrollmentByGenderViewTests(CourseEnrollmentViewTestCaseMixin, TestC ...@@ -261,16 +310,21 @@ class CourseEnrollmentByGenderViewTests(CourseEnrollmentViewTestCaseMixin, TestC
path = '/enrollment/gender/' path = '/enrollment/gender/'
model = models.CourseEnrollmentByGender model = models.CourseEnrollmentByGender
order_by = ['gender'] 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): def setUp(self):
super(CourseEnrollmentByGenderViewTests, self).setUp() super(CourseEnrollmentByGenderViewTests, self).setUp()
G(self.model, course_id=self.course_id, gender='m', date=self.date, count=34) self.generate_data()
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)
def format_as_response(self, *args): def format_as_response(self, *args):
return [ 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] 'gender': ce.gender, 'created': ce.created.strftime(settings.DATETIME_FORMAT)} for ce in args]
...@@ -308,15 +362,20 @@ class AnswerDistributionTests(TestCaseWithAuthentication): ...@@ -308,15 +362,20 @@ class AnswerDistributionTests(TestCaseWithAuthentication):
class CourseEnrollmentViewTests(CourseEnrollmentViewTestCaseMixin, TestCaseWithAuthentication): class CourseEnrollmentViewTests(CourseEnrollmentViewTestCaseMixin, TestCaseWithAuthentication):
model = models.CourseEnrollmentDaily model = models.CourseEnrollmentDaily
path = '/enrollment' 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): def setUp(self):
super(CourseEnrollmentViewTests, self).setUp() super(CourseEnrollmentViewTests, self).setUp()
G(self.model, course_id=self.course_id, date=self.date, count=203) self.generate_data()
G(self.model, course_id=self.course_id, date=self.date - datetime.timedelta(days=5), count=203)
def format_as_response(self, *args): def format_as_response(self, *args):
return [ 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)} 'created': ce.created.strftime(settings.DATETIME_FORMAT)}
for ce in args] for ce in args]
...@@ -324,6 +383,7 @@ class CourseEnrollmentViewTests(CourseEnrollmentViewTestCaseMixin, TestCaseWithA ...@@ -324,6 +383,7 @@ class CourseEnrollmentViewTests(CourseEnrollmentViewTestCaseMixin, TestCaseWithA
class CourseEnrollmentByLocationViewTests(CourseEnrollmentViewTestCaseMixin, TestCaseWithAuthentication): class CourseEnrollmentByLocationViewTests(CourseEnrollmentViewTestCaseMixin, TestCaseWithAuthentication):
path = '/enrollment/location/' path = '/enrollment/location/'
model = models.CourseEnrollmentByCountry model = models.CourseEnrollmentByCountry
csv_filename_slug = u'enrollment-location'
def format_as_response(self, *args): def format_as_response(self, *args):
unknown = {'course_id': None, 'count': 0, 'date': None, unknown = {'course_id': None, 'count': 0, 'date': None,
...@@ -341,26 +401,29 @@ class CourseEnrollmentByLocationViewTests(CourseEnrollmentViewTestCaseMixin, Tes ...@@ -341,26 +401,29 @@ class CourseEnrollmentByLocationViewTests(CourseEnrollmentViewTestCaseMixin, Tes
response = [unknown] response = [unknown]
response += [ 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}, 'country': {'alpha2': ce.country.alpha2, 'alpha3': ce.country.alpha3, 'name': ce.country.name},
'created': ce.created.strftime(settings.DATETIME_FORMAT)} for ce in 'created': ce.created.strftime(settings.DATETIME_FORMAT)} for ce in
args] args]
return response 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): def setUp(self):
super(CourseEnrollmentByLocationViewTests, self).setUp() super(CourseEnrollmentByLocationViewTests, self).setUp()
self.country = countries.get('US') self.country = countries.get('US')
G(self.model, course_id=self.course_id, country_code='US', count=455, date=self.date) self.generate_data()
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)
class CourseActivityWeeklyViewTests(CourseViewTestCaseMixin, TestCaseWithAuthentication): class CourseActivityWeeklyViewTests(CourseViewTestCaseMixin, TestCaseWithAuthentication):
...@@ -369,23 +432,29 @@ class CourseActivityWeeklyViewTests(CourseViewTestCaseMixin, TestCaseWithAuthent ...@@ -369,23 +432,29 @@ class CourseActivityWeeklyViewTests(CourseViewTestCaseMixin, TestCaseWithAuthent
model = CourseActivityWeekly model = CourseActivityWeekly
# activity_types = ['ACTIVE', 'ATTEMPTED_PROBLEM', 'PLAYED_VIDEO', 'POSTED_FORUM'] # activity_types = ['ACTIVE', 'ATTEMPTED_PROBLEM', 'PLAYED_VIDEO', 'POSTED_FORUM']
activity_types = ['ACTIVE', 'ATTEMPTED_PROBLEM', 'PLAYED_VIDEO'] activity_types = ['ACTIVE', 'ATTEMPTED_PROBLEM', 'PLAYED_VIDEO']
csv_filename_slug = u'engagement-activity'
def setUp(self): def generate_data(self, course_id=None):
super(CourseActivityWeeklyViewTests, self).setUp() course_id = course_id or self.course_id
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)
for activity_type in self.activity_types: for activity_type in self.activity_types:
G(CourseActivityWeekly, G(CourseActivityWeekly,
course_id=self.course_id, course_id=course_id,
interval_start=self.interval_start, interval_start=self.interval_start,
interval_end=self.interval_end, interval_end=self.interval_end,
activity_type=activity_type, activity_type=activity_type,
count=100) count=100)
def get_latest_data(self): def setUp(self):
return self.model.objects.filter(course_id=self.course_id, interval_end=self.interval_end) 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): def format_as_response(self, *args):
response = [] response = []
......
...@@ -8,6 +8,7 @@ from django.db.models import Max ...@@ -8,6 +8,7 @@ from django.db.models import Max
from django.http import Http404 from django.http import Http404
from django.utils.timezone import make_aware, utc from django.utils.timezone import make_aware, utc
from rest_framework import generics from rest_framework import generics
from opaque_keys.edx.keys import CourseKey
from analytics_data_api.v0 import models, serializers from analytics_data_api.v0 import models, serializers
...@@ -15,8 +16,11 @@ from analytics_data_api.v0 import models, serializers ...@@ -15,8 +16,11 @@ from analytics_data_api.v0 import models, serializers
class BaseCourseView(generics.ListAPIView): class BaseCourseView(generics.ListAPIView):
start_date = None start_date = None
end_date = None end_date = None
course_id = None
slug = None
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
self.course_id = self.kwargs.get('course_id')
start_date = request.QUERY_PARAMS.get('start_date') start_date = request.QUERY_PARAMS.get('start_date')
end_date = request.QUERY_PARAMS.get('end_date') end_date = request.QUERY_PARAMS.get('end_date')
timezone = utc timezone = utc
...@@ -44,12 +48,21 @@ class BaseCourseView(generics.ListAPIView): ...@@ -44,12 +48,21 @@ class BaseCourseView(generics.ListAPIView):
raise NotImplementedError raise NotImplementedError
def get_queryset(self): def get_queryset(self):
course_id = self.kwargs.get('course_id') self.verify_course_exists_or_404(self.course_id)
self.verify_course_exists_or_404(course_id) queryset = self.model.objects.filter(course_id=self.course_id)
queryset = self.model.objects.filter(course_id=course_id)
queryset = self.apply_date_filtering(queryset) queryset = self.apply_date_filtering(queryset)
return 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 # pylint: disable=line-too-long
class CourseActivityWeeklyView(BaseCourseView): class CourseActivityWeeklyView(BaseCourseView):
...@@ -80,6 +93,7 @@ class CourseActivityWeeklyView(BaseCourseView): ...@@ -80,6 +93,7 @@ class CourseActivityWeeklyView(BaseCourseView):
end_date -- Date before which all data should be returned (exclusive) end_date -- Date before which all data should be returned (exclusive)
""" """
slug = u'engagement-activity'
model = models.CourseActivityWeekly model = models.CourseActivityWeekly
serializer_class = serializers.CourseActivityWeeklySerializer serializer_class = serializers.CourseActivityWeeklySerializer
...@@ -244,6 +258,7 @@ class CourseEnrollmentByBirthYearView(BaseCourseEnrollmentView): ...@@ -244,6 +258,7 @@ class CourseEnrollmentByBirthYearView(BaseCourseEnrollmentView):
end_date -- Date before which all data should be returned (exclusive) end_date -- Date before which all data should be returned (exclusive)
""" """
slug = u'enrollment-age'
serializer_class = serializers.CourseEnrollmentByBirthYearSerializer serializer_class = serializers.CourseEnrollmentByBirthYearSerializer
model = models.CourseEnrollmentByBirthYear model = models.CourseEnrollmentByBirthYear
...@@ -263,6 +278,7 @@ class CourseEnrollmentByEducationView(BaseCourseEnrollmentView): ...@@ -263,6 +278,7 @@ class CourseEnrollmentByEducationView(BaseCourseEnrollmentView):
start_date -- Date after which all data should be returned (inclusive) start_date -- Date after which all data should be returned (inclusive)
end_date -- Date before which all data should be returned (exclusive) end_date -- Date before which all data should be returned (exclusive)
""" """
slug = u'enrollment-education'
serializer_class = serializers.CourseEnrollmentByEducationSerializer serializer_class = serializers.CourseEnrollmentByEducationSerializer
model = models.CourseEnrollmentByEducation model = models.CourseEnrollmentByEducation
...@@ -287,6 +303,7 @@ class CourseEnrollmentByGenderView(BaseCourseEnrollmentView): ...@@ -287,6 +303,7 @@ class CourseEnrollmentByGenderView(BaseCourseEnrollmentView):
start_date -- Date after which all data should be returned (inclusive) start_date -- Date after which all data should be returned (inclusive)
end_date -- Date before which all data should be returned (exclusive) end_date -- Date before which all data should be returned (exclusive)
""" """
slug = u'enrollment-gender'
serializer_class = serializers.CourseEnrollmentByGenderSerializer serializer_class = serializers.CourseEnrollmentByGenderSerializer
model = models.CourseEnrollmentByGender model = models.CourseEnrollmentByGender
...@@ -304,7 +321,7 @@ class CourseEnrollmentView(BaseCourseEnrollmentView): ...@@ -304,7 +321,7 @@ class CourseEnrollmentView(BaseCourseEnrollmentView):
start_date -- Date after which all data should be returned (inclusive) start_date -- Date after which all data should be returned (inclusive)
end_date -- Date before which all data should be returned (exclusive) end_date -- Date before which all data should be returned (exclusive)
""" """
slug = u'enrollment'
serializer_class = serializers.CourseEnrollmentDailySerializer serializer_class = serializers.CourseEnrollmentDailySerializer
model = models.CourseEnrollmentDaily model = models.CourseEnrollmentDaily
...@@ -329,7 +346,7 @@ class CourseEnrollmentByLocationView(BaseCourseEnrollmentView): ...@@ -329,7 +346,7 @@ class CourseEnrollmentByLocationView(BaseCourseEnrollmentView):
start_date -- Date after which all data should be returned (inclusive) start_date -- Date after which all data should be returned (inclusive)
end_date -- Date before which all data should be returned (exclusive) end_date -- Date before which all data should be returned (exclusive)
""" """
slug = u'enrollment-location'
serializer_class = serializers.CourseEnrollmentByCountrySerializer serializer_class = serializers.CourseEnrollmentByCountrySerializer
model = models.CourseEnrollmentByCountry model = models.CourseEnrollmentByCountry
......
...@@ -7,3 +7,4 @@ ipython==2.1.0 # BSD ...@@ -7,3 +7,4 @@ ipython==2.1.0 # BSD
django-rest-swagger==0.1.14 # BSD django-rest-swagger==0.1.14 # BSD
djangorestframework-csv==1.3.3 # BSD djangorestframework-csv==1.3.3 # BSD
iso3166==0.1 # MIT 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