Commit d2fcb7b3 by Dennis Jen Committed by GitHub

Added ccx library (#143)

* Refactored tests to use a variety of course IDs using ddt
* Bump versions of opaque keys and ccx libraries
* Added timeout check to see if the elasticsearch index is ready in tests (fixes flaky tests)
parent 75467f6e
...@@ -2,26 +2,18 @@ import json ...@@ -2,26 +2,18 @@ import json
import StringIO import StringIO
import csv import csv
from opaque_keys.edx.keys import CourseKey
from rest_framework import status from rest_framework import status
from analytics_data_api.utils import get_filename_safe_course_id
from analytics_data_api.v0.tests.utils import flatten from analytics_data_api.v0.tests.utils import flatten
DEMO_COURSE_ID = u'course-v1:edX+DemoX+Demo_2014' class CourseSamples(object):
SANITIZED_DEMO_COURSE_ID = get_filename_safe_course_id(DEMO_COURSE_ID)
course_ids = [
class DemoCourseMixin(object): 'edX/DemoX/Demo_Course',
course_key = None 'course-v1:edX+DemoX+Demo_2014',
course_id = None 'ccx-v1:edx+1.005x-CCX+rerun+ccx@15'
]
@classmethod
def setUpClass(cls):
cls.course_id = DEMO_COURSE_ID
cls.course_key = CourseKey.from_string(cls.course_id)
super(DemoCourseMixin, cls).setUpClass()
class VerifyCourseIdMixin(object): class VerifyCourseIdMixin(object):
......
...@@ -7,21 +7,22 @@ import datetime ...@@ -7,21 +7,22 @@ import datetime
from itertools import groupby from itertools import groupby
import urllib import urllib
import ddt
from django.conf import settings from django.conf import settings
from django_dynamic_fixture import G from django_dynamic_fixture import G
import pytz import pytz
from opaque_keys.edx.keys import CourseKey
from mock import patch, Mock from mock import patch, Mock
from analytics_data_api.constants import country, enrollment_modes, genders
from analytics_data_api.constants.country import get_country from analytics_data_api.constants.country import get_country
from analytics_data_api.v0 import models from analytics_data_api.v0 import models
from analytics_data_api.constants import country, enrollment_modes, genders from analytics_data_api.v0.tests.views import CourseSamples, VerifyCsvResponseMixin
from analytics_data_api.v0.models import CourseActivityWeekly from analytics_data_api.utils import get_filename_safe_course_id
from analytics_data_api.v0.tests.views import (
DemoCourseMixin, VerifyCsvResponseMixin, DEMO_COURSE_ID, SANITIZED_DEMO_COURSE_ID,
)
from analyticsdataserver.tests import TestCaseWithAuthentication from analyticsdataserver.tests import TestCaseWithAuthentication
@ddt.ddt
class DefaultFillTestMixin(object): class DefaultFillTestMixin(object):
""" """
Test that the view fills in missing data with a default value. Test that the view fills in missing data with a default value.
...@@ -32,19 +33,21 @@ class DefaultFillTestMixin(object): ...@@ -32,19 +33,21 @@ class DefaultFillTestMixin(object):
def destroy_data(self): def destroy_data(self):
self.model.objects.all().delete() self.model.objects.all().delete()
def test_default_fill(self): @ddt.data(*CourseSamples.course_ids)
def test_default_fill(self, course_id):
raise NotImplementedError raise NotImplementedError
# pylint: disable=no-member # pylint: disable=no-member
class CourseViewTestCaseMixin(DemoCourseMixin, VerifyCsvResponseMixin): @ddt.ddt
class CourseViewTestCaseMixin(VerifyCsvResponseMixin):
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 csv_filename_slug = None
def generate_data(self, course_id=None): def generate_data(self, course_id):
raise NotImplementedError raise NotImplementedError
def format_as_response(self, *args): def format_as_response(self, *args):
...@@ -56,7 +59,7 @@ class CourseViewTestCaseMixin(DemoCourseMixin, VerifyCsvResponseMixin): ...@@ -56,7 +59,7 @@ class CourseViewTestCaseMixin(DemoCourseMixin, VerifyCsvResponseMixin):
""" """
raise NotImplementedError raise NotImplementedError
def get_latest_data(self, course_id=None): def get_latest_data(self, course_id):
""" """
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.
...@@ -65,9 +68,10 @@ class CourseViewTestCaseMixin(DemoCourseMixin, VerifyCsvResponseMixin): ...@@ -65,9 +68,10 @@ class CourseViewTestCaseMixin(DemoCourseMixin, VerifyCsvResponseMixin):
""" """
raise NotImplementedError raise NotImplementedError
@property def csv_filename(self, course_id):
def csv_filename(self): course_key = CourseKey.from_string(course_id)
return u'edX-DemoX-Demo_2014--{0}.csv'.format(self.csv_filename_slug) safe_course_id = u'-'.join([course_key.org, course_key.course, course_key.run])
return u'{0}--{1}.csv'.format(safe_course_id, 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 """
...@@ -75,62 +79,58 @@ class CourseViewTestCaseMixin(DemoCourseMixin, VerifyCsvResponseMixin): ...@@ -75,62 +79,58 @@ class CourseViewTestCaseMixin(DemoCourseMixin, VerifyCsvResponseMixin):
response = self.authenticated_get(u'%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 assertViewReturnsExpectedData(self, expected): def assertViewReturnsExpectedData(self, expected, course_id):
# Validate the basic response status # Validate the basic response status
response = self.authenticated_get(u'%scourses/%s%s' % (self.api_root_path, self.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, 200) self.assertEquals(response.status_code, 200)
# Validate the data is correct and sorted chronologically # Validate the data is correct and sorted chronologically
self.assertEquals(response.data, expected) self.assertEquals(response.data, expected)
def test_get(self): @ddt.data(*CourseSamples.course_ids)
def test_get(self, course_id):
""" Verify the endpoint returns an HTTP 200 status and the correct data. """ """ Verify the endpoint returns an HTTP 200 status and the correct data. """
expected = self.format_as_response(*self.get_latest_data()) self.generate_data(course_id)
self.assertViewReturnsExpectedData(expected) expected = self.format_as_response(*self.get_latest_data(course_id))
self.assertViewReturnsExpectedData(expected, course_id)
def assertCSVIsValid(self, course_id, filename): def assertCSVIsValid(self, course_id, filename):
path = u'{0}courses/{1}{2}'.format(self.api_root_path, course_id, self.path) path = u'{0}courses/{1}{2}'.format(self.api_root_path, 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)
data = self.format_as_response(*self.get_latest_data(course_id=course_id)) data = self.format_as_response(*self.get_latest_data(course_id))
self.assertCsvResponseIsValid(response, filename, data) self.assertCsvResponseIsValid(response, filename, data)
def test_get_csv(self): @ddt.data(*CourseSamples.course_ids)
def test_get_csv(self, course_id):
""" Verify the endpoint returns data that has been properly converted to CSV. """ """ Verify the endpoint returns data that has been properly converted to CSV. """
self.assertCSVIsValid(self.course_id, self.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) self.generate_data(course_id)
filename = u'{0}--{1}.csv'.format(u'edX-DemoX-Demo_Course', self.csv_filename_slug) self.assertCSVIsValid(course_id, self.csv_filename(course_id))
self.assertCSVIsValid(course_id, filename)
def test_get_with_intervals(self): @ddt.data(*CourseSamples.course_ids)
def test_get_with_intervals(self, course_id):
""" 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
def assertIntervalFilteringWorks(self, expected_response, start_date, end_date): def assertIntervalFilteringWorks(self, expected_response, course_id, start_date, end_date):
# If start date is after date of existing data, return a 404 # If start date is after date of existing data, return a 404
date = (start_date + datetime.timedelta(days=30)).strftime(settings.DATETIME_FORMAT) date = (start_date + datetime.timedelta(days=30)).strftime(settings.DATETIME_FORMAT)
response = self.authenticated_get( response = self.authenticated_get(
'%scourses/%s%s?start_date=%s' % (self.api_root_path, self.course_id, self.path, date)) '%scourses/%s%s?start_date=%s' % (self.api_root_path, course_id, self.path, date))
self.assertEquals(response.status_code, 404) self.assertEquals(response.status_code, 404)
# If end date is before date of existing data, return a 404 # If end date is before date of existing data, return a 404
date = (start_date - datetime.timedelta(days=30)).strftime(settings.DATETIME_FORMAT) date = (start_date - datetime.timedelta(days=30)).strftime(settings.DATETIME_FORMAT)
response = self.authenticated_get( response = self.authenticated_get(
'%scourses/%s%s?end_date=%s' % (self.api_root_path, self.course_id, self.path, date)) '%scourses/%s%s?end_date=%s' % (self.api_root_path, course_id, self.path, date))
self.assertEquals(response.status_code, 404) self.assertEquals(response.status_code, 404)
# If data falls in date range, data should be returned # If data falls in date range, data should be returned
start = start_date.strftime(settings.DATETIME_FORMAT) start = start_date.strftime(settings.DATETIME_FORMAT)
end = end_date.strftime(settings.DATETIME_FORMAT) end = end_date.strftime(settings.DATETIME_FORMAT)
response = self.authenticated_get('%scourses/%s%s?start_date=%s&end_date=%s' % ( response = self.authenticated_get('%scourses/%s%s?start_date=%s&end_date=%s' % (
self.api_root_path, self.course_id, self.path, start, end)) self.api_root_path, course_id, self.path, start, end))
self.assertEquals(response.status_code, 200) self.assertEquals(response.status_code, 200)
self.assertListEqual(response.data, expected_response) self.assertListEqual(response.data, expected_response)
...@@ -138,31 +138,34 @@ class CourseViewTestCaseMixin(DemoCourseMixin, VerifyCsvResponseMixin): ...@@ -138,31 +138,34 @@ class CourseViewTestCaseMixin(DemoCourseMixin, VerifyCsvResponseMixin):
start = start_date.strftime(settings.DATE_FORMAT) start = start_date.strftime(settings.DATE_FORMAT)
end = end_date.strftime(settings.DATE_FORMAT) end = end_date.strftime(settings.DATE_FORMAT)
response = self.authenticated_get('%scourses/%s%s?start_date=%s&end_date=%s' % ( response = self.authenticated_get('%scourses/%s%s?start_date=%s&end_date=%s' % (
self.api_root_path, self.course_id, self.path, start, end)) self.api_root_path, course_id, self.path, start, end))
self.assertEquals(response.status_code, 200) self.assertEquals(response.status_code, 200)
self.assertListEqual(response.data, expected_response) self.assertListEqual(response.data, expected_response)
# pylint: disable=abstract-method # pylint: disable=abstract-method
@ddt.ddt
class CourseEnrollmentViewTestCaseMixin(CourseViewTestCaseMixin): class CourseEnrollmentViewTestCaseMixin(CourseViewTestCaseMixin):
date = None date = None
def setUp(self): @classmethod
super(CourseEnrollmentViewTestCaseMixin, self).setUp() def setUpClass(cls):
self.date = datetime.date(2014, 1, 1) super(CourseEnrollmentViewTestCaseMixin, cls).setUpClass()
cls.date = datetime.date(2014, 1, 1)
def get_latest_data(self, course_id=None): def get_latest_data(self, course_id):
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) return self.model.objects.filter(course_id=course_id, date=self.date).order_by('date', *self.order_by)
def test_get_with_intervals(self): @ddt.data(*CourseSamples.course_ids)
def test_get_with_intervals(self, course_id):
self.generate_data(course_id)
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, course_id, self.date, self.date + datetime.timedelta(days=1))
class CourseActivityLastWeekTest(DemoCourseMixin, TestCaseWithAuthentication): @ddt.ddt
def generate_data(self, course_id=None): class CourseActivityLastWeekTest(TestCaseWithAuthentication):
course_id = course_id or self.course_id def generate_data(self, course_id):
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=course_id, interval_start=interval_start, G(models.CourseActivityWeekly, course_id=course_id, interval_start=interval_start,
...@@ -177,26 +180,25 @@ class CourseActivityLastWeekTest(DemoCourseMixin, TestCaseWithAuthentication): ...@@ -177,26 +180,25 @@ class CourseActivityLastWeekTest(DemoCourseMixin, TestCaseWithAuthentication):
interval_end=interval_end, interval_end=interval_end,
activity_type='PLAYED_VIDEO', count=400) activity_type='PLAYED_VIDEO', count=400)
def setUp(self): @ddt.data(*CourseSamples.course_ids)
super(CourseActivityLastWeekTest, self).setUp() def test_activity(self, course_id):
self.generate_data() self.generate_data(course_id)
response = self.authenticated_get(u'/api/v0/courses/{0}/recent_activity'.format(course_id))
def test_activity(self):
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(course_id=course_id))
def assertValidActivityResponse(self, activity_type, count): def assertValidActivityResponse(self, course_id, activity_type, count):
response = self.authenticated_get(u'/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)) 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(course_id=course_id, activity_type=activity_type,
count=count))
@staticmethod @staticmethod
def get_activity_record(**kwargs): def get_activity_record(**kwargs):
datetime_format = "%Y-%m-%dT%H:%M:%SZ" datetime_format = "%Y-%m-%dT%H:%M:%SZ"
default = { default = {
'course_id': DEMO_COURSE_ID, 'course_id': kwargs['course_id'],
'interval_start': datetime.datetime(2014, 1, 1, 0, 0, tzinfo=pytz.utc).strftime(datetime_format), 'interval_start': datetime.datetime(2014, 1, 1, 0, 0, tzinfo=pytz.utc).strftime(datetime_format),
'interval_end': datetime.datetime(2014, 1, 8, 0, 0, tzinfo=pytz.utc).strftime(datetime_format), 'interval_end': datetime.datetime(2014, 1, 8, 0, 0, tzinfo=pytz.utc).strftime(datetime_format),
'activity_type': 'any', 'activity_type': 'any',
...@@ -206,27 +208,37 @@ class CourseActivityLastWeekTest(DemoCourseMixin, TestCaseWithAuthentication): ...@@ -206,27 +208,37 @@ class CourseActivityLastWeekTest(DemoCourseMixin, TestCaseWithAuthentication):
default['activity_type'] = default['activity_type'].lower() default['activity_type'] = default['activity_type'].lower()
return default return default
def test_activity_auth(self): @ddt.data(*CourseSamples.course_ids)
response = self.client.get(u'/api/v0/courses/{0}/recent_activity'.format(self.course_id), follow=True) def test_activity_auth(self, course_id):
self.generate_data(course_id)
response = self.client.get(u'/api/v0/courses/{0}/recent_activity'.format(course_id), follow=True)
self.assertEquals(response.status_code, 401) self.assertEquals(response.status_code, 401)
def test_url_encoded_course_id(self): @ddt.data(*CourseSamples.course_ids)
url_encoded_course_id = urllib.quote_plus(self.course_id) def test_url_encoded_course_id(self, course_id):
self.generate_data(course_id)
url_encoded_course_id = urllib.quote_plus(course_id)
response = self.authenticated_get(u'/api/v0/courses/{}/recent_activity'.format(url_encoded_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(course_id=course_id))
def test_any_activity(self): @ddt.data(*CourseSamples.course_ids)
self.assertValidActivityResponse('ANY', 300) def test_any_activity(self, course_id):
self.assertValidActivityResponse('any', 300) self.generate_data(course_id)
self.assertValidActivityResponse(course_id, 'ANY', 300)
self.assertValidActivityResponse(course_id, 'any', 300)
def test_video_activity(self): @ddt.data(*CourseSamples.course_ids)
self.assertValidActivityResponse('played_video', 400) def test_video_activity(self, course_id):
self.generate_data(course_id)
self.assertValidActivityResponse(course_id, 'played_video', 400)
def test_unknown_activity(self): @ddt.data(*CourseSamples.course_ids)
def test_unknown_activity(self, course_id):
self.generate_data(course_id)
activity_type = 'missing_activity_type' activity_type = 'missing_activity_type'
response = self.authenticated_get(u'/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)) 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):
...@@ -237,38 +249,39 @@ class CourseActivityLastWeekTest(DemoCourseMixin, TestCaseWithAuthentication): ...@@ -237,38 +249,39 @@ class CourseActivityLastWeekTest(DemoCourseMixin, TestCaseWithAuthentication):
response = self.authenticated_get(u'/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): @ddt.data(*CourseSamples.course_ids)
def test_label_parameter(self, course_id):
self.generate_data(course_id)
activity_type = 'played_video' activity_type = 'played_video'
response = self.authenticated_get(u'/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)) 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(course_id=course_id, activity_type=activity_type,
count=400))
@ddt.ddt
class CourseEnrollmentByBirthYearViewTests(CourseEnrollmentViewTestCaseMixin, TestCaseWithAuthentication): class CourseEnrollmentByBirthYearViewTests(CourseEnrollmentViewTestCaseMixin, TestCaseWithAuthentication):
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' csv_filename_slug = u'enrollment-age'
def generate_data(self, course_id=None): def generate_data(self, course_id):
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=1956)
G(self.model, course_id=course_id, date=self.date, birth_year=1986) 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=1956)
G(self.model, course_id=course_id, date=self.date - datetime.timedelta(days=10), birth_year=1986) G(self.model, course_id=course_id, date=self.date - datetime.timedelta(days=10), birth_year=1986)
def setUp(self):
super(CourseEnrollmentByBirthYearViewTests, self).setUp()
self.generate_data()
def format_as_response(self, *args): def format_as_response(self, *args):
return [ return [
{'course_id': unicode(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): @ddt.data(*CourseSamples.course_ids)
response = self.authenticated_get('/api/v0/courses/%s%s' % (self.course_id, self.path,)) def test_get(self, course_id):
self.generate_data(course_id)
response = self.authenticated_get('/api/v0/courses/%s%s' % (course_id, self.path,))
self.assertEquals(response.status_code, 200) self.assertEquals(response.status_code, 200)
expected = self.format_as_response(*self.model.objects.filter(date=self.date)) expected = self.format_as_response(*self.model.objects.filter(date=self.date))
...@@ -281,17 +294,16 @@ class CourseEnrollmentByEducationViewTests(CourseEnrollmentViewTestCaseMixin, Te ...@@ -281,17 +294,16 @@ class CourseEnrollmentByEducationViewTests(CourseEnrollmentViewTestCaseMixin, Te
order_by = ['education_level'] order_by = ['education_level']
csv_filename_slug = u'enrollment-education' csv_filename_slug = u'enrollment-education'
def generate_data(self, course_id=None): def generate_data(self, course_id):
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.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, education_level=self.el2)
G(self.model, course_id=course_id, date=self.date - datetime.timedelta(days=2), 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): @classmethod
super(CourseEnrollmentByEducationViewTests, self).setUp() def setUpClass(cls):
self.el1 = 'doctorate' super(CourseEnrollmentByEducationViewTests, cls).setUpClass()
self.el2 = 'top_secret' cls.el1 = 'doctorate'
self.generate_data() cls.el2 = 'top_secret'
def format_as_response(self, *args): def format_as_response(self, *args):
return [ return [
...@@ -300,6 +312,7 @@ class CourseEnrollmentByEducationViewTests(CourseEnrollmentViewTestCaseMixin, Te ...@@ -300,6 +312,7 @@ class CourseEnrollmentByEducationViewTests(CourseEnrollmentViewTestCaseMixin, Te
ce in args] ce in args]
@ddt.ddt
class CourseEnrollmentByGenderViewTests(CourseEnrollmentViewTestCaseMixin, DefaultFillTestMixin, class CourseEnrollmentByGenderViewTests(CourseEnrollmentViewTestCaseMixin, DefaultFillTestMixin,
TestCaseWithAuthentication): TestCaseWithAuthentication):
path = '/enrollment/gender/' path = '/enrollment/gender/'
...@@ -307,8 +320,7 @@ class CourseEnrollmentByGenderViewTests(CourseEnrollmentViewTestCaseMixin, Defau ...@@ -307,8 +320,7 @@ class CourseEnrollmentByGenderViewTests(CourseEnrollmentViewTestCaseMixin, Defau
order_by = ['gender'] order_by = ['gender']
csv_filename_slug = u'enrollment-gender' csv_filename_slug = u'enrollment-gender'
def generate_data(self, course_id=None): def generate_data(self, course_id):
course_id = course_id or self.course_id
_genders = ['f', 'm', 'o', None] _genders = ['f', 'm', 'o', None]
days = 2 days = 2
...@@ -320,10 +332,6 @@ class CourseEnrollmentByGenderViewTests(CourseEnrollmentViewTestCaseMixin, Defau ...@@ -320,10 +332,6 @@ class CourseEnrollmentByGenderViewTests(CourseEnrollmentViewTestCaseMixin, Defau
gender=gender, gender=gender,
count=100 + day) count=100 + day)
def setUp(self):
super(CourseEnrollmentByGenderViewTests, self).setUp()
self.generate_data()
def tearDown(self): def tearDown(self):
self.destroy_data() self.destroy_data()
...@@ -350,11 +358,10 @@ class CourseEnrollmentByGenderViewTests(CourseEnrollmentViewTestCaseMixin, Defau ...@@ -350,11 +358,10 @@ class CourseEnrollmentByGenderViewTests(CourseEnrollmentViewTestCaseMixin, Defau
return response return response
def test_default_fill(self): @ddt.data(*CourseSamples.course_ids)
self.destroy_data() def test_default_fill(self, course_id):
# Create a single entry for a single gender # Create a single entry for a single gender
enrollment = G(self.model, course_id=self.course_id, date=self.date, gender='f', count=1) enrollment = G(self.model, course_id=course_id, date=self.date, gender='f', count=1)
# Create the expected data # Create the expected data
_genders = list(genders.ALL) _genders = list(genders.ALL)
...@@ -365,7 +372,7 @@ class CourseEnrollmentByGenderViewTests(CourseEnrollmentViewTestCaseMixin, Defau ...@@ -365,7 +372,7 @@ class CourseEnrollmentByGenderViewTests(CourseEnrollmentViewTestCaseMixin, Defau
for gender in _genders: for gender in _genders:
expected[gender] = 0 expected[gender] = 0
self.assertViewReturnsExpectedData([expected]) self.assertViewReturnsExpectedData([expected], course_id)
class CourseEnrollmentViewTests(CourseEnrollmentViewTestCaseMixin, TestCaseWithAuthentication): class CourseEnrollmentViewTests(CourseEnrollmentViewTestCaseMixin, TestCaseWithAuthentication):
...@@ -373,15 +380,10 @@ class CourseEnrollmentViewTests(CourseEnrollmentViewTestCaseMixin, TestCaseWithA ...@@ -373,15 +380,10 @@ class CourseEnrollmentViewTests(CourseEnrollmentViewTestCaseMixin, TestCaseWithA
path = '/enrollment' path = '/enrollment'
csv_filename_slug = u'enrollment' csv_filename_slug = u'enrollment'
def generate_data(self, course_id=None): def generate_data(self, course_id):
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, count=203)
G(self.model, course_id=course_id, date=self.date - datetime.timedelta(days=5), 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()
self.generate_data()
def format_as_response(self, *args): def format_as_response(self, *args):
return [ return [
{'course_id': unicode(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),
...@@ -389,19 +391,14 @@ class CourseEnrollmentViewTests(CourseEnrollmentViewTestCaseMixin, TestCaseWithA ...@@ -389,19 +391,14 @@ class CourseEnrollmentViewTests(CourseEnrollmentViewTestCaseMixin, TestCaseWithA
for ce in args] for ce in args]
@ddt.ddt
class CourseEnrollmentModeViewTests(CourseEnrollmentViewTestCaseMixin, DefaultFillTestMixin, class CourseEnrollmentModeViewTests(CourseEnrollmentViewTestCaseMixin, DefaultFillTestMixin,
TestCaseWithAuthentication): TestCaseWithAuthentication):
model = models.CourseEnrollmentModeDaily model = models.CourseEnrollmentModeDaily
path = '/enrollment/mode' path = '/enrollment/mode'
csv_filename_slug = u'enrollment_mode' csv_filename_slug = u'enrollment_mode'
def setUp(self): def generate_data(self, course_id):
super(CourseEnrollmentModeViewTests, self).setUp()
self.generate_data()
def generate_data(self, course_id=None):
course_id = course_id or self.course_id
for mode in enrollment_modes.ALL: for mode in enrollment_modes.ALL:
G(self.model, course_id=course_id, date=self.date, mode=mode) G(self.model, course_id=course_id, date=self.date, mode=mode)
...@@ -432,11 +429,12 @@ class CourseEnrollmentModeViewTests(CourseEnrollmentViewTestCaseMixin, DefaultFi ...@@ -432,11 +429,12 @@ class CourseEnrollmentModeViewTests(CourseEnrollmentViewTestCaseMixin, DefaultFi
return [response] return [response]
def test_default_fill(self): @ddt.data(*CourseSamples.course_ids)
def test_default_fill(self, course_id):
self.destroy_data() self.destroy_data()
# Create a single entry for a single enrollment mode # Create a single entry for a single enrollment mode
enrollment = G(self.model, course_id=self.course_id, date=self.date, mode=enrollment_modes.AUDIT, enrollment = G(self.model, course_id=course_id, date=self.date, mode=enrollment_modes.AUDIT,
count=1, cumulative_count=100) count=1, cumulative_count=100)
# Create the expected data # Create the expected data
...@@ -451,7 +449,7 @@ class CourseEnrollmentModeViewTests(CourseEnrollmentViewTestCaseMixin, DefaultFi ...@@ -451,7 +449,7 @@ class CourseEnrollmentModeViewTests(CourseEnrollmentViewTestCaseMixin, DefaultFi
expected[u'count'] = 1 expected[u'count'] = 1
expected[u'cumulative_count'] = 100 expected[u'cumulative_count'] = 100
self.assertViewReturnsExpectedData([expected]) self.assertViewReturnsExpectedData([expected], course_id)
class CourseEnrollmentByLocationViewTests(CourseEnrollmentViewTestCaseMixin, TestCaseWithAuthentication): class CourseEnrollmentByLocationViewTests(CourseEnrollmentViewTestCaseMixin, TestCaseWithAuthentication):
...@@ -482,8 +480,7 @@ class CourseEnrollmentByLocationViewTests(CourseEnrollmentViewTestCaseMixin, Tes ...@@ -482,8 +480,7 @@ class CourseEnrollmentByLocationViewTests(CourseEnrollmentViewTestCaseMixin, Tes
return response return response
def generate_data(self, course_id=None): def generate_data(self, course_id):
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='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='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='IN', count=12, date=self.date - datetime.timedelta(days=29))
...@@ -494,40 +491,37 @@ class CourseEnrollmentByLocationViewTests(CourseEnrollmentViewTestCaseMixin, Tes ...@@ -494,40 +491,37 @@ class CourseEnrollmentByLocationViewTests(CourseEnrollmentViewTestCaseMixin, Tes
G(self.model, course_id=course_id, country_code='EU', count=4, 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) G(self.model, course_id=course_id, country_code='O1', count=7, date=self.date)
def setUp(self): @classmethod
super(CourseEnrollmentByLocationViewTests, self).setUp() def setUpClass(cls):
self.country = get_country('US') super(CourseEnrollmentByLocationViewTests, cls).setUpClass()
self.generate_data() cls.country = get_country('US')
@ddt.ddt
class CourseActivityWeeklyViewTests(CourseViewTestCaseMixin, TestCaseWithAuthentication): class CourseActivityWeeklyViewTests(CourseViewTestCaseMixin, TestCaseWithAuthentication):
path = '/activity/' path = '/activity/'
default_order_by = 'interval_end' default_order_by = 'interval_end'
model = CourseActivityWeekly model = models.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' csv_filename_slug = u'engagement-activity'
def generate_data(self, course_id=None): def generate_data(self, course_id):
course_id = course_id or self.course_id
for activity_type in self.activity_types: for activity_type in self.activity_types:
G(CourseActivityWeekly, G(models.CourseActivityWeekly,
course_id=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 setUp(self): @classmethod
super(CourseActivityWeeklyViewTests, self).setUp() def setUpClass(cls):
self.interval_start = datetime.datetime(2014, 1, 1, tzinfo=pytz.utc) super(CourseActivityWeeklyViewTests, cls).setUpClass()
self.interval_end = self.interval_start + datetime.timedelta(weeks=1) cls.interval_start = datetime.datetime(2014, 1, 1, tzinfo=pytz.utc)
cls.interval_end = cls.interval_start + datetime.timedelta(weeks=1)
self.generate_data() def get_latest_data(self, course_id):
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) 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):
...@@ -555,15 +549,16 @@ class CourseActivityWeeklyViewTests(CourseViewTestCaseMixin, TestCaseWithAuthent ...@@ -555,15 +549,16 @@ class CourseActivityWeeklyViewTests(CourseViewTestCaseMixin, TestCaseWithAuthent
return response return response
def test_get_with_intervals(self): @ddt.data(*CourseSamples.course_ids)
def test_get_with_intervals(self, course_id):
""" 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. """
# Create additional data self.generate_data(course_id)
interval_start = self.interval_start + datetime.timedelta(weeks=1) interval_start = self.interval_start + datetime.timedelta(weeks=1)
interval_end = self.interval_end + datetime.timedelta(weeks=1) interval_end = self.interval_end + datetime.timedelta(weeks=1)
for activity_type in self.activity_types: for activity_type in self.activity_types:
G(CourseActivityWeekly, G(models.CourseActivityWeekly,
course_id=self.course_id, course_id=course_id,
interval_start=interval_start, interval_start=interval_start,
interval_end=interval_end, interval_end=interval_end,
activity_type=activity_type, activity_type=activity_type,
...@@ -571,20 +566,21 @@ class CourseActivityWeeklyViewTests(CourseViewTestCaseMixin, TestCaseWithAuthent ...@@ -571,20 +566,21 @@ class CourseActivityWeeklyViewTests(CourseViewTestCaseMixin, TestCaseWithAuthent
expected = self.format_as_response(*self.model.objects.all()) expected = self.format_as_response(*self.model.objects.all())
self.assertEqual(len(expected), 2) self.assertEqual(len(expected), 2)
self.assertIntervalFilteringWorks(expected, self.interval_start, interval_end + datetime.timedelta(days=1)) self.assertIntervalFilteringWorks(expected, course_id, self.interval_start,
interval_end + datetime.timedelta(days=1))
class CourseProblemsListViewTests(DemoCourseMixin, TestCaseWithAuthentication): @ddt.ddt
def _get_data(self, course_id=None): class CourseProblemsListViewTests(TestCaseWithAuthentication):
def _get_data(self, course_id):
""" """
Retrieve data for the specified course. Retrieve data for the specified course.
""" """
course_id = course_id or self.course_id
url = '/api/v0/courses/{}/problems/'.format(course_id) url = '/api/v0/courses/{}/problems/'.format(course_id)
return self.authenticated_get(url) return self.authenticated_get(url)
def test_get(self): @ddt.data(*CourseSamples.course_ids)
def test_get(self, course_id):
""" """
The view should return data when data exists for the course. The view should return data when data exists for the course.
""" """
...@@ -600,11 +596,11 @@ class CourseProblemsListViewTests(DemoCourseMixin, TestCaseWithAuthentication): ...@@ -600,11 +596,11 @@ class CourseProblemsListViewTests(DemoCourseMixin, TestCaseWithAuthentication):
alt_created = created + datetime.timedelta(seconds=2) alt_created = created + datetime.timedelta(seconds=2)
date_time_format = '%Y-%m-%d %H:%M:%S' date_time_format = '%Y-%m-%d %H:%M:%S'
o1 = G(models.ProblemFirstLastResponseAnswerDistribution, course_id=self.course_id, module_id=module_id, o1 = G(models.ProblemFirstLastResponseAnswerDistribution, course_id=course_id, module_id=module_id,
correct=True, last_response_count=100, created=created.strftime(date_time_format)) correct=True, last_response_count=100, created=created.strftime(date_time_format))
o2 = G(models.ProblemFirstLastResponseAnswerDistribution, course_id=self.course_id, module_id=alt_module_id, o2 = G(models.ProblemFirstLastResponseAnswerDistribution, course_id=course_id, module_id=alt_module_id,
correct=True, last_response_count=100, created=created.strftime(date_time_format)) correct=True, last_response_count=100, created=created.strftime(date_time_format))
o3 = G(models.ProblemFirstLastResponseAnswerDistribution, course_id=self.course_id, module_id=module_id, o3 = G(models.ProblemFirstLastResponseAnswerDistribution, course_id=course_id, module_id=module_id,
correct=False, last_response_count=200, created=alt_created.strftime(date_time_format)) correct=False, last_response_count=200, created=alt_created.strftime(date_time_format))
expected = [ expected = [
...@@ -624,7 +620,7 @@ class CourseProblemsListViewTests(DemoCourseMixin, TestCaseWithAuthentication): ...@@ -624,7 +620,7 @@ class CourseProblemsListViewTests(DemoCourseMixin, TestCaseWithAuthentication):
} }
] ]
response = self._get_data(self.course_id) response = self._get_data(course_id)
self.assertEquals(response.status_code, 200) self.assertEquals(response.status_code, 200)
self.assertListEqual([dict(d) for d in response.data], expected) self.assertListEqual([dict(d) for d in response.data], expected)
...@@ -637,17 +633,17 @@ class CourseProblemsListViewTests(DemoCourseMixin, TestCaseWithAuthentication): ...@@ -637,17 +633,17 @@ class CourseProblemsListViewTests(DemoCourseMixin, TestCaseWithAuthentication):
self.assertEquals(response.status_code, 404) self.assertEquals(response.status_code, 404)
class CourseProblemsAndTagsListViewTests(DemoCourseMixin, TestCaseWithAuthentication): @ddt.ddt
def _get_data(self, course_id=None): class CourseProblemsAndTagsListViewTests(TestCaseWithAuthentication):
def _get_data(self, course_id):
""" """
Retrieve data for the specified course. Retrieve data for the specified course.
""" """
course_id = course_id or self.course_id
url = '/api/v0/courses/{}/problems_and_tags/'.format(course_id) url = '/api/v0/courses/{}/problems_and_tags/'.format(course_id)
return self.authenticated_get(url) return self.authenticated_get(url)
def test_get(self): @ddt.data(*CourseSamples.course_ids)
def test_get(self, course_id):
""" """
The view should return data when data exists for the course. The view should return data when data exists for the course.
""" """
...@@ -668,13 +664,13 @@ class CourseProblemsAndTagsListViewTests(DemoCourseMixin, TestCaseWithAuthentica ...@@ -668,13 +664,13 @@ class CourseProblemsAndTagsListViewTests(DemoCourseMixin, TestCaseWithAuthentica
created = datetime.datetime.utcnow() created = datetime.datetime.utcnow()
alt_created = created + datetime.timedelta(seconds=2) alt_created = created + datetime.timedelta(seconds=2)
G(models.ProblemsAndTags, course_id=self.course_id, module_id=module_id, G(models.ProblemsAndTags, course_id=course_id, module_id=module_id,
tag_name='difficulty', tag_value=tags['difficulty'][0], tag_name='difficulty', tag_value=tags['difficulty'][0],
total_submissions=11, correct_submissions=4, created=created) total_submissions=11, correct_submissions=4, created=created)
G(models.ProblemsAndTags, course_id=self.course_id, module_id=module_id, G(models.ProblemsAndTags, course_id=course_id, module_id=module_id,
tag_name='learning_outcome', tag_value=tags['learning_outcome'][1], tag_name='learning_outcome', tag_value=tags['learning_outcome'][1],
total_submissions=11, correct_submissions=4, created=alt_created) total_submissions=11, correct_submissions=4, created=alt_created)
G(models.ProblemsAndTags, course_id=self.course_id, module_id=alt_module_id, G(models.ProblemsAndTags, course_id=course_id, module_id=alt_module_id,
tag_name='learning_outcome', tag_value=tags['learning_outcome'][2], tag_name='learning_outcome', tag_value=tags['learning_outcome'][2],
total_submissions=4, correct_submissions=0, created=created) total_submissions=4, correct_submissions=0, created=created)
...@@ -700,7 +696,7 @@ class CourseProblemsAndTagsListViewTests(DemoCourseMixin, TestCaseWithAuthentica ...@@ -700,7 +696,7 @@ class CourseProblemsAndTagsListViewTests(DemoCourseMixin, TestCaseWithAuthentica
} }
] ]
response = self._get_data(self.course_id) response = self._get_data(course_id)
self.assertEquals(response.status_code, 200) self.assertEquals(response.status_code, 200)
self.assertListEqual(sorted([dict(d) for d in response.data]), sorted(expected)) self.assertListEqual(sorted([dict(d) for d in response.data]), sorted(expected))
...@@ -713,16 +709,17 @@ class CourseProblemsAndTagsListViewTests(DemoCourseMixin, TestCaseWithAuthentica ...@@ -713,16 +709,17 @@ class CourseProblemsAndTagsListViewTests(DemoCourseMixin, TestCaseWithAuthentica
self.assertEquals(response.status_code, 404) self.assertEquals(response.status_code, 404)
class CourseVideosListViewTests(DemoCourseMixin, TestCaseWithAuthentication): @ddt.ddt
def _get_data(self, course_id=None): class CourseVideosListViewTests(TestCaseWithAuthentication):
def _get_data(self, course_id):
""" """
Retrieve videos for a specified course. Retrieve videos for a specified course.
""" """
course_id = course_id or self.course_id
url = '/api/v0/courses/{}/videos/'.format(course_id) url = '/api/v0/courses/{}/videos/'.format(course_id)
return self.authenticated_get(url) return self.authenticated_get(url)
def test_get(self): @ddt.data(*CourseSamples.course_ids)
def test_get(self, course_id):
# add a blank row, which shouldn't be included in results # add a blank row, which shouldn't be included in results
G(models.Video) G(models.Video)
...@@ -730,14 +727,14 @@ class CourseVideosListViewTests(DemoCourseMixin, TestCaseWithAuthentication): ...@@ -730,14 +727,14 @@ class CourseVideosListViewTests(DemoCourseMixin, TestCaseWithAuthentication):
video_id = 'v1d30' video_id = 'v1d30'
created = datetime.datetime.utcnow() created = datetime.datetime.utcnow()
date_time_format = '%Y-%m-%d %H:%M:%S' date_time_format = '%Y-%m-%d %H:%M:%S'
G(models.Video, course_id=self.course_id, encoded_module_id=module_id, G(models.Video, course_id=course_id, encoded_module_id=module_id,
pipeline_video_id=video_id, duration=100, segment_length=1, users_at_start=50, users_at_end=10, pipeline_video_id=video_id, duration=100, segment_length=1, users_at_start=50, users_at_end=10,
created=created.strftime(date_time_format)) created=created.strftime(date_time_format))
alt_module_id = 'i4x-test-video-2' alt_module_id = 'i4x-test-video-2'
alt_video_id = 'a1d30' alt_video_id = 'a1d30'
alt_created = created + datetime.timedelta(seconds=10) alt_created = created + datetime.timedelta(seconds=10)
G(models.Video, course_id=self.course_id, encoded_module_id=alt_module_id, G(models.Video, course_id=course_id, encoded_module_id=alt_module_id,
pipeline_video_id=alt_video_id, duration=200, segment_length=5, users_at_start=1050, users_at_end=50, pipeline_video_id=alt_video_id, duration=200, segment_length=5, users_at_start=1050, users_at_end=50,
created=alt_created.strftime(date_time_format)) created=alt_created.strftime(date_time_format))
...@@ -762,7 +759,7 @@ class CourseVideosListViewTests(DemoCourseMixin, TestCaseWithAuthentication): ...@@ -762,7 +759,7 @@ class CourseVideosListViewTests(DemoCourseMixin, TestCaseWithAuthentication):
} }
] ]
response = self._get_data(self.course_id) response = self._get_data(course_id)
self.assertEquals(response.status_code, 200) self.assertEquals(response.status_code, 200)
self.assertListEqual(response.data, expected) self.assertListEqual(response.data, expected)
...@@ -771,34 +768,38 @@ class CourseVideosListViewTests(DemoCourseMixin, TestCaseWithAuthentication): ...@@ -771,34 +768,38 @@ class CourseVideosListViewTests(DemoCourseMixin, TestCaseWithAuthentication):
self.assertEquals(response.status_code, 404) self.assertEquals(response.status_code, 404)
class CourseReportDownloadViewTests(DemoCourseMixin, TestCaseWithAuthentication): @ddt.ddt
class CourseReportDownloadViewTests(TestCaseWithAuthentication):
path = '/api/v0/courses/{course_id}/reports/{report_name}' path = '/api/v0/courses/{course_id}/reports/{report_name}'
@patch('django.core.files.storage.default_storage.exists', Mock(return_value=False)) @patch('django.core.files.storage.default_storage.exists', Mock(return_value=False))
def test_report_file_not_found(self): @ddt.data(*CourseSamples.course_ids)
def test_report_file_not_found(self, course_id):
response = self.authenticated_get( response = self.authenticated_get(
self.path.format( self.path.format(
course_id=DEMO_COURSE_ID, course_id=course_id,
report_name='problem_response' report_name='problem_response'
) )
) )
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
def test_report_not_supported(self): @ddt.data(*CourseSamples.course_ids)
def test_report_not_supported(self, course_id):
response = self.authenticated_get( response = self.authenticated_get(
self.path.format( self.path.format(
course_id=DEMO_COURSE_ID, course_id=course_id,
report_name='fake_problem_that_we_dont_support' report_name='fake_problem_that_we_dont_support'
) )
) )
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
@patch('analytics_data_api.utils.default_storage', object()) @patch('analytics_data_api.utils.default_storage', object())
def test_incompatible_storage_provider(self): @ddt.data(*CourseSamples.course_ids)
def test_incompatible_storage_provider(self, course_id):
response = self.authenticated_get( response = self.authenticated_get(
self.path.format( self.path.format(
course_id=DEMO_COURSE_ID, course_id=course_id,
report_name='problem_response' report_name='problem_response'
) )
) )
...@@ -815,16 +816,17 @@ class CourseReportDownloadViewTests(DemoCourseMixin, TestCaseWithAuthentication) ...@@ -815,16 +816,17 @@ class CourseReportDownloadViewTests(DemoCourseMixin, TestCaseWithAuthentication)
'analytics_data_api.utils.get_expiration_date', 'analytics_data_api.utils.get_expiration_date',
Mock(return_value=datetime.datetime(2014, 1, 1, tzinfo=pytz.utc)) Mock(return_value=datetime.datetime(2014, 1, 1, tzinfo=pytz.utc))
) )
def test_make_working_link(self): @ddt.data(*CourseSamples.course_ids)
def test_make_working_link(self, course_id):
response = self.authenticated_get( response = self.authenticated_get(
self.path.format( self.path.format(
course_id=DEMO_COURSE_ID, course_id=course_id,
report_name='problem_response' report_name='problem_response'
) )
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
expected = { expected = {
'course_id': SANITIZED_DEMO_COURSE_ID, 'course_id': get_filename_safe_course_id(course_id),
'report_name': 'problem_response', 'report_name': 'problem_response',
'download_url': 'http://fake', 'download_url': 'http://fake',
'last_modified': datetime.datetime(2014, 1, 1, tzinfo=pytz.utc).strftime(settings.DATETIME_FORMAT), 'last_modified': datetime.datetime(2014, 1, 1, tzinfo=pytz.utc).strftime(settings.DATETIME_FORMAT),
...@@ -844,16 +846,17 @@ class CourseReportDownloadViewTests(DemoCourseMixin, TestCaseWithAuthentication) ...@@ -844,16 +846,17 @@ class CourseReportDownloadViewTests(DemoCourseMixin, TestCaseWithAuthentication)
'analytics_data_api.utils.get_expiration_date', 'analytics_data_api.utils.get_expiration_date',
Mock(return_value=datetime.datetime(2014, 1, 1, tzinfo=pytz.utc)) Mock(return_value=datetime.datetime(2014, 1, 1, tzinfo=pytz.utc))
) )
def test_make_working_link_with_missing_size(self): @ddt.data(*CourseSamples.course_ids)
def test_make_working_link_with_missing_size(self, course_id):
response = self.authenticated_get( response = self.authenticated_get(
self.path.format( self.path.format(
course_id=DEMO_COURSE_ID, course_id=course_id,
report_name='problem_response' report_name='problem_response'
) )
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
expected = { expected = {
'course_id': SANITIZED_DEMO_COURSE_ID, 'course_id': get_filename_safe_course_id(course_id),
'report_name': 'problem_response', 'report_name': 'problem_response',
'download_url': 'http://fake', 'download_url': 'http://fake',
'last_modified': datetime.datetime(2014, 1, 1, tzinfo=pytz.utc).strftime(settings.DATETIME_FORMAT), 'last_modified': datetime.datetime(2014, 1, 1, tzinfo=pytz.utc).strftime(settings.DATETIME_FORMAT),
...@@ -869,16 +872,17 @@ class CourseReportDownloadViewTests(DemoCourseMixin, TestCaseWithAuthentication) ...@@ -869,16 +872,17 @@ class CourseReportDownloadViewTests(DemoCourseMixin, TestCaseWithAuthentication)
'analytics_data_api.utils.get_expiration_date', 'analytics_data_api.utils.get_expiration_date',
Mock(return_value=datetime.datetime(2014, 1, 1, tzinfo=pytz.utc)) Mock(return_value=datetime.datetime(2014, 1, 1, tzinfo=pytz.utc))
) )
def test_make_working_link_with_missing_last_modified_date(self): @ddt.data(*CourseSamples.course_ids)
def test_make_working_link_with_missing_last_modified_date(self, course_id):
response = self.authenticated_get( response = self.authenticated_get(
self.path.format( self.path.format(
course_id=DEMO_COURSE_ID, course_id=course_id,
report_name='problem_response' report_name='problem_response'
) )
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
expected = { expected = {
'course_id': SANITIZED_DEMO_COURSE_ID, 'course_id': get_filename_safe_course_id(course_id),
'report_name': 'problem_response', 'report_name': 'problem_response',
'download_url': 'http://fake', 'download_url': 'http://fake',
'file_size': 1000, 'file_size': 1000,
......
...@@ -12,21 +12,21 @@ from analyticsdataserver.tests import TestCaseWithAuthentication ...@@ -12,21 +12,21 @@ from analyticsdataserver.tests import TestCaseWithAuthentication
from analytics_data_api.constants.engagement_events import (ATTEMPTED, COMPLETED, CONTRIBUTED, DISCUSSION, from analytics_data_api.constants.engagement_events import (ATTEMPTED, COMPLETED, CONTRIBUTED, DISCUSSION,
PROBLEM, VIDEO, VIEWED) PROBLEM, VIDEO, VIEWED)
from analytics_data_api.v0 import models from analytics_data_api.v0 import models
from analytics_data_api.v0.tests.views import DemoCourseMixin, VerifyCourseIdMixin from analytics_data_api.v0.tests.views import CourseSamples, VerifyCourseIdMixin
@ddt.ddt @ddt.ddt
class EngagementTimelineTests(DemoCourseMixin, VerifyCourseIdMixin, TestCaseWithAuthentication): class EngagementTimelineTests(VerifyCourseIdMixin, TestCaseWithAuthentication):
DEFAULT_USERNAME = 'ed_xavier' DEFAULT_USERNAME = 'ed_xavier'
path_template = '/api/v0/engagement_timelines/{}/?course_id={}' path_template = '/api/v0/engagement_timelines/{}/?course_id={}'
def create_engagement(self, entity_type, event_type, entity_id, count, date=None): def create_engagement(self, course_id, entity_type, event_type, entity_id, count, date=None):
"""Create a ModuleEngagement model""" """Create a ModuleEngagement model"""
if date is None: if date is None:
date = datetime.datetime(2015, 1, 1, tzinfo=pytz.utc) date = datetime.datetime(2015, 1, 1, tzinfo=pytz.utc)
G( G(
models.ModuleEngagement, models.ModuleEngagement,
course_id=self.course_id, course_id=course_id,
username=self.DEFAULT_USERNAME, username=self.DEFAULT_USERNAME,
date=date, date=date,
entity_type=entity_type, entity_type=entity_type,
...@@ -36,19 +36,19 @@ class EngagementTimelineTests(DemoCourseMixin, VerifyCourseIdMixin, TestCaseWith ...@@ -36,19 +36,19 @@ class EngagementTimelineTests(DemoCourseMixin, VerifyCourseIdMixin, TestCaseWith
) )
@ddt.data( @ddt.data(
(PROBLEM, ATTEMPTED, 'problems_attempted', True), (CourseSamples.course_ids[0], PROBLEM, ATTEMPTED, 'problems_attempted', True),
(PROBLEM, COMPLETED, 'problems_completed', True), (CourseSamples.course_ids[1], PROBLEM, COMPLETED, 'problems_completed', True),
(VIDEO, VIEWED, 'videos_viewed', True), (CourseSamples.course_ids[2], VIDEO, VIEWED, 'videos_viewed', True),
(DISCUSSION, CONTRIBUTED, 'discussion_contributions', False), (CourseSamples.course_ids[0], DISCUSSION, CONTRIBUTED, 'discussion_contributions', False),
) )
@ddt.unpack @ddt.unpack
def test_metric_aggregation(self, entity_type, event_type, metric_display_name, expect_id_aggregation): def test_metric_aggregation(self, course_id, entity_type, event_type, metric_display_name, expect_id_aggregation):
""" """
Verify that some metrics are counted by unique ID, while some are Verify that some metrics are counted by unique ID, while some are
counted by total interactions. counted by total interactions.
""" """
self.create_engagement(entity_type, event_type, 'entity-id', count=5) self.create_engagement(course_id, entity_type, event_type, 'entity-id', count=5)
self.create_engagement(entity_type, event_type, 'entity-id', count=5) self.create_engagement(course_id, entity_type, event_type, 'entity-id', count=5)
expected_data = { expected_data = {
'days': [ 'days': [
{ {
...@@ -64,7 +64,7 @@ class EngagementTimelineTests(DemoCourseMixin, VerifyCourseIdMixin, TestCaseWith ...@@ -64,7 +64,7 @@ class EngagementTimelineTests(DemoCourseMixin, VerifyCourseIdMixin, TestCaseWith
expected_data['days'][0][metric_display_name] = 1 expected_data['days'][0][metric_display_name] = 1
else: else:
expected_data['days'][0][metric_display_name] = 10 expected_data['days'][0][metric_display_name] = 10
path = self.path_template.format(self.DEFAULT_USERNAME, urlquote(self.course_id)) path = self.path_template.format(self.DEFAULT_USERNAME, urlquote(course_id))
response = self.authenticated_get(path) response = self.authenticated_get(path)
self.assertEquals(response.status_code, 200) self.assertEquals(response.status_code, 200)
self.assertEquals( self.assertEquals(
...@@ -72,20 +72,21 @@ class EngagementTimelineTests(DemoCourseMixin, VerifyCourseIdMixin, TestCaseWith ...@@ -72,20 +72,21 @@ class EngagementTimelineTests(DemoCourseMixin, VerifyCourseIdMixin, TestCaseWith
expected_data expected_data
) )
def test_timeline(self): @ddt.data(*CourseSamples.course_ids)
def test_timeline(self, course_id):
""" """
Smoke test the learner engagement timeline. Smoke test the learner engagement timeline.
""" """
path = self.path_template.format(self.DEFAULT_USERNAME, urlquote(self.course_id)) path = self.path_template.format(self.DEFAULT_USERNAME, urlquote(course_id))
day_one = datetime.datetime(2015, 1, 1, tzinfo=pytz.utc) day_one = datetime.datetime(2015, 1, 1, tzinfo=pytz.utc)
day_two = datetime.datetime(2015, 1, 2, tzinfo=pytz.utc) day_two = datetime.datetime(2015, 1, 2, tzinfo=pytz.utc)
self.create_engagement(PROBLEM, ATTEMPTED, 'id-1', count=100, date=day_one) self.create_engagement(course_id, PROBLEM, ATTEMPTED, 'id-1', count=100, date=day_one)
self.create_engagement(PROBLEM, COMPLETED, 'id-2', count=12, date=day_one) self.create_engagement(course_id, PROBLEM, COMPLETED, 'id-2', count=12, date=day_one)
self.create_engagement(DISCUSSION, CONTRIBUTED, 'id-3', count=6, date=day_one) self.create_engagement(course_id, DISCUSSION, CONTRIBUTED, 'id-3', count=6, date=day_one)
self.create_engagement(DISCUSSION, CONTRIBUTED, 'id-4', count=10, date=day_two) self.create_engagement(course_id, DISCUSSION, CONTRIBUTED, 'id-4', count=10, date=day_two)
self.create_engagement(VIDEO, VIEWED, 'id-5', count=44, date=day_two) self.create_engagement(course_id, VIDEO, VIEWED, 'id-5', count=44, date=day_two)
self.create_engagement(PROBLEM, ATTEMPTED, 'id-6', count=8, date=day_two) self.create_engagement(course_id, PROBLEM, ATTEMPTED, 'id-6', count=8, date=day_two)
self.create_engagement(PROBLEM, ATTEMPTED, 'id-7', count=4, date=day_two) self.create_engagement(course_id, PROBLEM, ATTEMPTED, 'id-7', count=4, date=day_two)
response = self.authenticated_get(path) response = self.authenticated_get(path)
self.assertEquals(response.status_code, 200) self.assertEquals(response.status_code, 200)
expected = { expected = {
...@@ -108,12 +109,13 @@ class EngagementTimelineTests(DemoCourseMixin, VerifyCourseIdMixin, TestCaseWith ...@@ -108,12 +109,13 @@ class EngagementTimelineTests(DemoCourseMixin, VerifyCourseIdMixin, TestCaseWith
} }
self.assertEquals(response.data, expected) self.assertEquals(response.data, expected)
def test_day_gap(self): @ddt.data(*CourseSamples.course_ids)
path = self.path_template.format(self.DEFAULT_USERNAME, urlquote(self.course_id)) def test_day_gap(self, course_id):
path = self.path_template.format(self.DEFAULT_USERNAME, urlquote(course_id))
first_day = datetime.datetime(2015, 5, 26, tzinfo=pytz.utc) first_day = datetime.datetime(2015, 5, 26, tzinfo=pytz.utc)
last_day = datetime.datetime(2015, 5, 28, tzinfo=pytz.utc) last_day = datetime.datetime(2015, 5, 28, tzinfo=pytz.utc)
self.create_engagement(VIDEO, VIEWED, 'id-1', count=1, date=first_day) self.create_engagement(course_id, VIDEO, VIEWED, 'id-1', count=1, date=first_day)
self.create_engagement(PROBLEM, ATTEMPTED, entity_id='id-2', count=1, date=last_day) self.create_engagement(course_id, PROBLEM, ATTEMPTED, entity_id='id-2', count=1, date=last_day)
response = self.authenticated_get(path) response = self.authenticated_get(path)
self.assertEquals(response.status_code, 200) self.assertEquals(response.status_code, 200)
expected = { expected = {
...@@ -143,14 +145,15 @@ class EngagementTimelineTests(DemoCourseMixin, VerifyCourseIdMixin, TestCaseWith ...@@ -143,14 +145,15 @@ class EngagementTimelineTests(DemoCourseMixin, VerifyCourseIdMixin, TestCaseWith
} }
self.assertEquals(response.data, expected) self.assertEquals(response.data, expected)
def test_not_found(self): @ddt.data(*CourseSamples.course_ids)
path = self.path_template.format(self.DEFAULT_USERNAME, urlquote(self.course_id)) def test_not_found(self, course_id):
path = self.path_template.format(self.DEFAULT_USERNAME, urlquote(course_id))
response = self.authenticated_get(path) response = self.authenticated_get(path)
self.assertEquals(response.status_code, status.HTTP_404_NOT_FOUND) self.assertEquals(response.status_code, status.HTTP_404_NOT_FOUND)
expected = { expected = {
u"error_code": u"no_learner_engagement_timeline", u"error_code": u"no_learner_engagement_timeline",
u"developer_message": u"Learner {} engagement timeline not found for course {}.".format( u"developer_message": u"Learner {} engagement timeline not found for course {}.".format(
self.DEFAULT_USERNAME, self.course_id) self.DEFAULT_USERNAME, course_id)
} }
self.assertDictEqual(json.loads(response.content), expected) self.assertDictEqual(json.loads(response.content), expected)
......
...@@ -20,7 +20,7 @@ from analytics_data_api.constants import engagement_events ...@@ -20,7 +20,7 @@ from analytics_data_api.constants import engagement_events
from analytics_data_api.v0.models import ModuleEngagementMetricRanges from analytics_data_api.v0.models import ModuleEngagementMetricRanges
from analytics_data_api.v0.views import CsvViewMixin, PaginatedHeadersMixin from analytics_data_api.v0.views import CsvViewMixin, PaginatedHeadersMixin
from analytics_data_api.v0.tests.views import ( from analytics_data_api.v0.tests.views import (
DemoCourseMixin, VerifyCourseIdMixin, VerifyCsvResponseMixin, CourseSamples, VerifyCourseIdMixin, VerifyCsvResponseMixin,
) )
...@@ -33,6 +33,9 @@ class LearnerAPITestMixin(CsvViewMixin): ...@@ -33,6 +33,9 @@ class LearnerAPITestMixin(CsvViewMixin):
super(LearnerAPITestMixin, self).setUp() super(LearnerAPITestMixin, self).setUp()
self._es = Elasticsearch([settings.ELASTICSEARCH_LEARNERS_HOST]) self._es = Elasticsearch([settings.ELASTICSEARCH_LEARNERS_HOST])
management.call_command('create_elasticsearch_learners_indices') management.call_command('create_elasticsearch_learners_indices')
# ensure that the index is ready
# pylint: disable=unexpected-keyword-arg
self._es.cluster.health(index=settings.ELASTICSEARCH_LEARNERS_INDEX, wait_for_status='yellow')
self.addCleanup(lambda: management.call_command('delete_elasticsearch_learners_indices')) self.addCleanup(lambda: management.call_command('delete_elasticsearch_learners_indices'))
def _create_learner( def _create_learner(
...@@ -641,8 +644,7 @@ class LearnerCsvListTests(LearnerAPITestMixin, VerifyCourseIdMixin, ...@@ -641,8 +644,7 @@ class LearnerCsvListTests(LearnerAPITestMixin, VerifyCourseIdMixin,
@ddt.ddt @ddt.ddt
class CourseLearnerMetadataTests(DemoCourseMixin, VerifyCourseIdMixin, class CourseLearnerMetadataTests(VerifyCourseIdMixin, LearnerAPITestMixin, TestCaseWithAuthentication,):
LearnerAPITestMixin, TestCaseWithAuthentication):
""" """
Tests for the course learner metadata endpoint. Tests for the course learner metadata endpoint.
""" """
...@@ -651,8 +653,8 @@ class CourseLearnerMetadataTests(DemoCourseMixin, VerifyCourseIdMixin, ...@@ -651,8 +653,8 @@ class CourseLearnerMetadataTests(DemoCourseMixin, VerifyCourseIdMixin,
"""Helper to send a GET request to the API.""" """Helper to send a GET request to the API."""
return self.authenticated_get('/api/v0/course_learner_metadata/{}/'.format(course_id)) return self.authenticated_get('/api/v0/course_learner_metadata/{}/'.format(course_id))
def get_expected_json(self, segments, enrollment_modes, cohorts): def get_expected_json(self, course_id, segments, enrollment_modes, cohorts):
expected_json = self._get_full_engagement_ranges() expected_json = self._get_full_engagement_ranges(course_id)
expected_json['segments'] = segments expected_json['segments'] = segments
expected_json['enrollment_modes'] = enrollment_modes expected_json['enrollment_modes'] = enrollment_modes
expected_json['cohorts'] = cohorts expected_json['cohorts'] = cohorts
...@@ -667,22 +669,24 @@ class CourseLearnerMetadataTests(DemoCourseMixin, VerifyCourseIdMixin, ...@@ -667,22 +669,24 @@ class CourseLearnerMetadataTests(DemoCourseMixin, VerifyCourseIdMixin,
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
@ddt.data( @ddt.data(
{}, (CourseSamples.course_ids[0], {}),
{'highly_engaged': 1}, (CourseSamples.course_ids[1], {'highly_engaged': 1}),
{'disengaging': 1}, (CourseSamples.course_ids[2], {'disengaging': 1}),
{'struggling': 1}, (CourseSamples.course_ids[0], {'struggling': 1}),
{'inactive': 1}, (CourseSamples.course_ids[1], {'inactive': 1}),
{'unenrolled': 1}, (CourseSamples.course_ids[2], {'unenrolled': 1}),
{'highly_engaged': 3, 'disengaging': 1}, (CourseSamples.course_ids[0], {'highly_engaged': 3, 'disengaging': 1}),
{'disengaging': 10, 'inactive': 12}, (CourseSamples.course_ids[1], {'disengaging': 10, 'inactive': 12}),
{'highly_engaged': 1, 'disengaging': 2, 'struggling': 3, 'inactive': 4, 'unenrolled': 5}, (CourseSamples.course_ids[2], {'highly_engaged': 1, 'disengaging': 2, 'struggling': 3,
'inactive': 4, 'unenrolled': 5}),
) )
def test_segments_unique_learners(self, segments): @ddt.unpack
def test_segments_unique_learners(self, course_id, segments):
""" """
Tests segment counts when each learner belongs to at most one segment. Tests segment counts when each learner belongs to at most one segment.
""" """
learners = [ learners = [
{'username': '{}_{}'.format(segment, i), 'course_id': self.course_id, 'segments': [segment]} {'username': '{}_{}'.format(segment, i), 'course_id': course_id, 'segments': [segment]}
for segment, count in segments.items() for segment, count in segments.items()
for i in xrange(count) for i in xrange(count)
] ]
...@@ -690,39 +694,43 @@ class CourseLearnerMetadataTests(DemoCourseMixin, VerifyCourseIdMixin, ...@@ -690,39 +694,43 @@ class CourseLearnerMetadataTests(DemoCourseMixin, VerifyCourseIdMixin,
expected_segments = {"highly_engaged": 0, "disengaging": 0, "struggling": 0, "inactive": 0, "unenrolled": 0} expected_segments = {"highly_engaged": 0, "disengaging": 0, "struggling": 0, "inactive": 0, "unenrolled": 0}
expected_segments.update(segments) expected_segments.update(segments)
expected = self.get_expected_json( expected = self.get_expected_json(
course_id=course_id,
segments=expected_segments, segments=expected_segments,
enrollment_modes={'honor': len(learners)} if learners else {}, enrollment_modes={'honor': len(learners)} if learners else {},
cohorts={'Team edX': len(learners)} if learners else {}, cohorts={'Team edX': len(learners)} if learners else {},
) )
self.assert_response_matches(self._get(self.course_id), 200, expected) self.assert_response_matches(self._get(course_id), 200, expected)
def test_segments_same_learner(self): @ddt.data(*CourseSamples.course_ids)
def test_segments_same_learner(self, course_id):
""" """
Tests segment counts when each learner belongs to multiple segments. Tests segment counts when each learner belongs to multiple segments.
""" """
self.create_learners([ self.create_learners([
{'username': 'user_1', 'course_id': self.course_id, 'segments': ['struggling', 'disengaging']}, {'username': 'user_1', 'course_id': course_id, 'segments': ['struggling', 'disengaging']},
{'username': 'user_2', 'course_id': self.course_id, 'segments': ['disengaging']} {'username': 'user_2', 'course_id': course_id, 'segments': ['disengaging']}
]) ])
expected = self.get_expected_json( expected = self.get_expected_json(
course_id=course_id,
segments={'disengaging': 2, 'struggling': 1, 'highly_engaged': 0, 'inactive': 0, 'unenrolled': 0}, segments={'disengaging': 2, 'struggling': 1, 'highly_engaged': 0, 'inactive': 0, 'unenrolled': 0},
enrollment_modes={'honor': 2}, enrollment_modes={'honor': 2},
cohorts={'Team edX': 2}, cohorts={'Team edX': 2},
) )
self.assert_response_matches(self._get(self.course_id), 200, expected) self.assert_response_matches(self._get(course_id), 200, expected)
@ddt.data( @ddt.data(
[], (CourseSamples.course_ids[0], []),
['honor'], (CourseSamples.course_ids[1], ['honor']),
['verified'], (CourseSamples.course_ids[2], ['verified']),
['audit'], (CourseSamples.course_ids[0], ['audit']),
['nonexistent-enrollment-tracks-still-show-up'], (CourseSamples.course_ids[1], ['nonexistent-enrollment-tracks-still-show-up']),
['honor', 'verified', 'audit'], (CourseSamples.course_ids[2], ['honor', 'verified', 'audit']),
['honor', 'honor', 'verified', 'verified', 'audit', 'audit'], (CourseSamples.course_ids[0], ['honor', 'honor', 'verified', 'verified', 'audit', 'audit']),
) )
def test_enrollment_modes(self, enrollment_modes): @ddt.unpack
def test_enrollment_modes(self, course_id, enrollment_modes):
self.create_learners([ self.create_learners([
{'username': 'user_{}'.format(i), 'course_id': self.course_id, 'enrollment_mode': enrollment_mode} {'username': 'user_{}'.format(i), 'course_id': course_id, 'enrollment_mode': enrollment_mode}
for i, enrollment_mode in enumerate(enrollment_modes) for i, enrollment_mode in enumerate(enrollment_modes)
]) ])
expected_enrollment_modes = {} expected_enrollment_modes = {}
...@@ -731,32 +739,35 @@ class CourseLearnerMetadataTests(DemoCourseMixin, VerifyCourseIdMixin, ...@@ -731,32 +739,35 @@ class CourseLearnerMetadataTests(DemoCourseMixin, VerifyCourseIdMixin,
count = len([mode for mode in group]) count = len([mode for mode in group])
expected_enrollment_modes[enrollment_mode] = count expected_enrollment_modes[enrollment_mode] = count
expected = self.get_expected_json( expected = self.get_expected_json(
course_id=course_id,
segments={'disengaging': 0, 'struggling': 0, 'highly_engaged': 0, 'inactive': 0, 'unenrolled': 0}, segments={'disengaging': 0, 'struggling': 0, 'highly_engaged': 0, 'inactive': 0, 'unenrolled': 0},
enrollment_modes=expected_enrollment_modes, enrollment_modes=expected_enrollment_modes,
cohorts={'Team edX': len(enrollment_modes)} if enrollment_modes else {}, cohorts={'Team edX': len(enrollment_modes)} if enrollment_modes else {},
) )
self.assert_response_matches(self._get(self.course_id), 200, expected) self.assert_response_matches(self._get(course_id), 200, expected)
@ddt.data( @ddt.data(
[], (CourseSamples.course_ids[0], []),
['Yellow'], (CourseSamples.course_ids[1], ['Yellow']),
['Blue'], (CourseSamples.course_ids[2], ['Blue']),
['Red', 'Red', 'yellow team', 'yellow team', 'green'], (CourseSamples.course_ids[0], ['Red', 'Red', 'yellow team', 'yellow team', 'green']),
) )
def test_cohorts(self, cohorts): @ddt.unpack
def test_cohorts(self, course_id, cohorts):
self.create_learners([ self.create_learners([
{'username': 'user_{}'.format(i), 'course_id': self.course_id, 'cohort': cohort} {'username': 'user_{}'.format(i), 'course_id': course_id, 'cohort': cohort}
for i, cohort in enumerate(cohorts) for i, cohort in enumerate(cohorts)
]) ])
expected_cohorts = { expected_cohorts = {
cohort: len([mode for mode in group]) for cohort, group in groupby(cohorts) cohort: len([mode for mode in group]) for cohort, group in groupby(cohorts)
} }
expected = self.get_expected_json( expected = self.get_expected_json(
course_id=course_id,
segments={'disengaging': 0, 'struggling': 0, 'highly_engaged': 0, 'inactive': 0, 'unenrolled': 0}, segments={'disengaging': 0, 'struggling': 0, 'highly_engaged': 0, 'inactive': 0, 'unenrolled': 0},
enrollment_modes={'honor': len(cohorts)} if cohorts else {}, enrollment_modes={'honor': len(cohorts)} if cohorts else {},
cohorts=expected_cohorts, cohorts=expected_cohorts,
) )
self.assert_response_matches(self._get(self.course_id), 200, expected) self.assert_response_matches(self._get(course_id), 200, expected)
@property @property
def empty_engagement_ranges(self): def empty_engagement_ranges(self):
...@@ -776,16 +787,18 @@ class CourseLearnerMetadataTests(DemoCourseMixin, VerifyCourseIdMixin, ...@@ -776,16 +787,18 @@ class CourseLearnerMetadataTests(DemoCourseMixin, VerifyCourseIdMixin,
empty_engagement_ranges['engagement_ranges'][metric] = copy.deepcopy(empty_range) empty_engagement_ranges['engagement_ranges'][metric] = copy.deepcopy(empty_range)
return empty_engagement_ranges return empty_engagement_ranges
def test_no_engagement_ranges(self): @ddt.data(*CourseSamples.course_ids)
response = self._get(self.course_id) def test_no_engagement_ranges(self, course_id):
response = self._get(course_id)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertDictContainsSubset(self.empty_engagement_ranges, json.loads(response.content)) self.assertDictContainsSubset(self.empty_engagement_ranges, json.loads(response.content))
def test_one_engagement_range(self): @ddt.data(*CourseSamples.course_ids)
def test_one_engagement_range(self, course_id):
metric_type = 'problems_completed' metric_type = 'problems_completed'
start_date = datetime.date(2015, 7, 1) start_date = datetime.date(2015, 7, 1)
end_date = datetime.date(2015, 7, 21) end_date = datetime.date(2015, 7, 21)
G(ModuleEngagementMetricRanges, course_id=self.course_id, start_date=start_date, end_date=end_date, G(ModuleEngagementMetricRanges, course_id=course_id, start_date=start_date, end_date=end_date,
metric=metric_type, range_type='normal', low_value=90, high_value=6120) metric=metric_type, range_type='normal', low_value=90, high_value=6120)
expected_ranges = self.empty_engagement_ranges expected_ranges = self.empty_engagement_ranges
expected_ranges['engagement_ranges'].update({ expected_ranges['engagement_ranges'].update({
...@@ -800,11 +813,11 @@ class CourseLearnerMetadataTests(DemoCourseMixin, VerifyCourseIdMixin, ...@@ -800,11 +813,11 @@ class CourseLearnerMetadataTests(DemoCourseMixin, VerifyCourseIdMixin,
} }
}) })
response = self._get(self.course_id) response = self._get(course_id)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertDictContainsSubset(expected_ranges, json.loads(response.content)) self.assertDictContainsSubset(expected_ranges, json.loads(response.content))
def _get_full_engagement_ranges(self): def _get_full_engagement_ranges(self, course_id):
""" Populates a full set of engagement ranges and returns the expected engagement ranges. """ """ Populates a full set of engagement ranges and returns the expected engagement ranges. """
start_date = datetime.date(2015, 7, 1) start_date = datetime.date(2015, 7, 1)
end_date = datetime.date(2015, 7, 21) end_date = datetime.date(2015, 7, 21)
...@@ -821,10 +834,10 @@ class CourseLearnerMetadataTests(DemoCourseMixin, VerifyCourseIdMixin, ...@@ -821,10 +834,10 @@ class CourseLearnerMetadataTests(DemoCourseMixin, VerifyCourseIdMixin,
max_value = 1000.0 max_value = 1000.0
for metric_type in engagement_events.EVENTS: for metric_type in engagement_events.EVENTS:
low_ceil = 100.5 low_ceil = 100.5
G(ModuleEngagementMetricRanges, course_id=self.course_id, start_date=start_date, end_date=end_date, G(ModuleEngagementMetricRanges, course_id=course_id, start_date=start_date, end_date=end_date,
metric=metric_type, range_type='low', low_value=0, high_value=low_ceil) metric=metric_type, range_type='low', low_value=0, high_value=low_ceil)
normal_floor = 800.8 normal_floor = 800.8
G(ModuleEngagementMetricRanges, course_id=self.course_id, start_date=start_date, end_date=end_date, G(ModuleEngagementMetricRanges, course_id=course_id, start_date=start_date, end_date=end_date,
metric=metric_type, range_type='normal', low_value=normal_floor, high_value=max_value) metric=metric_type, range_type='normal', low_value=normal_floor, high_value=max_value)
expected['engagement_ranges'][metric_type] = { expected['engagement_ranges'][metric_type] = {
...@@ -843,15 +856,17 @@ class CourseLearnerMetadataTests(DemoCourseMixin, VerifyCourseIdMixin, ...@@ -843,15 +856,17 @@ class CourseLearnerMetadataTests(DemoCourseMixin, VerifyCourseIdMixin,
return expected return expected
def test_engagement_ranges_only(self): @ddt.data(*CourseSamples.course_ids)
expected = self._get_full_engagement_ranges() def test_engagement_ranges_only(self, course_id):
response = self._get(self.course_id) expected = self._get_full_engagement_ranges(course_id)
response = self._get(course_id)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertDictContainsSubset(expected, json.loads(response.content)) self.assertDictContainsSubset(expected, json.loads(response.content))
def test_engagement_ranges_fields(self): @ddt.data(*CourseSamples.course_ids)
def test_engagement_ranges_fields(self, course_id):
expected_events = engagement_events.EVENTS expected_events = engagement_events.EVENTS
response = json.loads(self._get(self.course_id).content) response = json.loads(self._get(course_id).content)
self.assertTrue('engagement_ranges' in response) self.assertTrue('engagement_ranges' in response)
for event in expected_events: for event in expected_events:
self.assertTrue(event in response['engagement_ranges']) self.assertTrue(event in response['engagement_ranges'])
boto==2.42.0 # MIT boto==2.42.0 # MIT
Django==1.9.9 # BSD License Django==1.9.9 # BSD License
django-countries==4.0 # MIT
django-model-utils==2.5.2 # BSD django-model-utils==2.5.2 # BSD
djangorestframework==3.4.6 # BSD djangorestframework==3.4.6 # BSD
django-rest-swagger==0.3.8 # BSD django-rest-swagger==0.3.8 # BSD
djangorestframework-csv==1.4.1 # BSD djangorestframework-csv==1.4.1 # BSD
django-countries==4.0 # MIT django-storages==1.4.1 # BSD
edx-django-release-util==0.1.2
elasticsearch-dsl==0.0.11 # Apache 2.0 elasticsearch-dsl==0.0.11 # Apache 2.0
ordered-set==2.0.1 # MIT ordered-set==2.0.1 # MIT
# markdown is used by swagger for rendering the api docs # markdown is used by swagger for rendering the api docs
Markdown==2.6.6 # BSD Markdown==2.6.6 # BSD
-e git+https://github.com/edx/opaque-keys.git@d45d0bd8d64c69531be69178b9505b5d38806ce0#egg=opaque-keys edx-ccx-keys==0.2.1
django-storages==1.4.1 # BSD edx-django-release-util==0.1.2
edx-opaque-keys==0.4.0
# Test dependencies go here. # Test dependencies go here.
-r base.txt -r base.txt
coverage==4.2 coverage==4.2
ddt==1.1.0 ddt==1.1.1
diff-cover >= 0.9.9 diff-cover >= 0.9.9
django-dynamic-fixture==1.9.0 django-dynamic-fixture==1.9.0
django-nose==1.4.4 django-nose==1.4.4
......
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