diff --git a/analytics_data_api/v0/tests/views/__init__.py b/analytics_data_api/v0/tests/views/__init__.py index 6c816b8e..cb8e1aff 100644 --- a/analytics_data_api/v0/tests/views/__init__.py +++ b/analytics_data_api/v0/tests/views/__init__.py @@ -2,26 +2,18 @@ import StringIO import csv -from opaque_keys.edx.keys import CourseKey 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 -DEMO_COURSE_ID = u'course-v1:edX+DemoX+Demo_2014' -SANITIZED_DEMO_COURSE_ID = get_filename_safe_course_id(DEMO_COURSE_ID) +class CourseSamples(object): - -class DemoCourseMixin(object): - course_key = None - course_id = None - - @classmethod - def setUpClass(cls): - cls.course_id = DEMO_COURSE_ID - cls.course_key = CourseKey.from_string(cls.course_id) - super(DemoCourseMixin, cls).setUpClass() + course_ids = [ + 'edX/DemoX/Demo_Course', + 'course-v1:edX+DemoX+Demo_2014', + 'ccx-v1:edx+1.005x-CCX+rerun+ccx@15' + ] class VerifyCourseIdMixin(object): diff --git a/analytics_data_api/v0/tests/views/test_courses.py b/analytics_data_api/v0/tests/views/test_courses.py index fe9a56b8..f2f35091 100644 --- a/analytics_data_api/v0/tests/views/test_courses.py +++ b/analytics_data_api/v0/tests/views/test_courses.py @@ -7,21 +7,22 @@ from itertools import groupby import urllib +import ddt from django.conf import settings from django_dynamic_fixture import G import pytz +from opaque_keys.edx.keys import CourseKey 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.v0 import models -from analytics_data_api.constants import country, enrollment_modes, genders -from analytics_data_api.v0.models import CourseActivityWeekly -from analytics_data_api.v0.tests.views import ( - DemoCourseMixin, VerifyCsvResponseMixin, DEMO_COURSE_ID, SANITIZED_DEMO_COURSE_ID, -) +from analytics_data_api.v0.tests.views import CourseSamples, VerifyCsvResponseMixin +from analytics_data_api.utils import get_filename_safe_course_id from analyticsdataserver.tests import TestCaseWithAuthentication +@ddt.ddt class DefaultFillTestMixin(object): """ Test that the view fills in missing data with a default value. @@ -32,19 +33,21 @@ class DefaultFillTestMixin(object): def destroy_data(self): self.model.objects.all().delete() - def test_default_fill(self): + @ddt.data(*CourseSamples.course_ids) + def test_default_fill(self, course_id): raise NotImplementedError # pylint: disable=no-member -class CourseViewTestCaseMixin(DemoCourseMixin, VerifyCsvResponseMixin): +@ddt.ddt +class CourseViewTestCaseMixin(VerifyCsvResponseMixin): model = None api_root_path = '/api/v0/' path = None order_by = [] csv_filename_slug = None - def generate_data(self, course_id=None): + def generate_data(self, course_id): raise NotImplementedError def format_as_response(self, *args): @@ -56,7 +59,7 @@ def format_as_response(self, *args): """ 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 to the endpoint with no date filtering. @@ -65,9 +68,10 @@ def get_latest_data(self, course_id=None): """ raise NotImplementedError - @property - def csv_filename(self): - return u'edX-DemoX-Demo_2014--{0}.csv'.format(self.csv_filename_slug) + def csv_filename(self, course_id): + course_key = CourseKey.from_string(course_id) + 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): """ Requests made against non-existent courses should return a 404 """ @@ -75,62 +79,58 @@ def test_get_not_found(self): response = self.authenticated_get(u'%scourses/%s%s' % (self.api_root_path, course_id, self.path)) self.assertEquals(response.status_code, 404) - def assertViewReturnsExpectedData(self, expected): + def assertViewReturnsExpectedData(self, expected, course_id): # 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) # Validate the data is correct and sorted chronologically 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. """ - expected = self.format_as_response(*self.get_latest_data()) - self.assertViewReturnsExpectedData(expected) + self.generate_data(course_id) + expected = self.format_as_response(*self.get_latest_data(course_id)) + self.assertViewReturnsExpectedData(expected, course_id) def assertCSVIsValid(self, course_id, filename): path = u'{0}courses/{1}{2}'.format(self.api_root_path, course_id, self.path) csv_content_type = 'text/csv' response = self.authenticated_get(path, HTTP_ACCEPT=csv_content_type) - 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) - 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. """ - 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) - filename = u'{0}--{1}.csv'.format(u'edX-DemoX-Demo_Course', self.csv_filename_slug) - self.assertCSVIsValid(course_id, filename) + self.assertCSVIsValid(course_id, self.csv_filename(course_id)) - 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. """ 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 date = (start_date + datetime.timedelta(days=30)).strftime(settings.DATETIME_FORMAT) 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) # If end date is before date of existing data, return a 404 date = (start_date - datetime.timedelta(days=30)).strftime(settings.DATETIME_FORMAT) 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) # If data falls in date range, data should be returned start = start_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' % ( - 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.assertListEqual(response.data, expected_response) @@ -138,31 +138,34 @@ def assertIntervalFilteringWorks(self, expected_response, start_date, end_date): start = start_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' % ( - 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.assertListEqual(response.data, expected_response) # pylint: disable=abstract-method +@ddt.ddt class CourseEnrollmentViewTestCaseMixin(CourseViewTestCaseMixin): date = None - def setUp(self): - super(CourseEnrollmentViewTestCaseMixin, self).setUp() - self.date = datetime.date(2014, 1, 1) + @classmethod + def setUpClass(cls): + super(CourseEnrollmentViewTestCaseMixin, cls).setUpClass() + cls.date = datetime.date(2014, 1, 1) - def get_latest_data(self, course_id=None): - course_id = course_id or self.course_id + def get_latest_data(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): + @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)) - 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): - def generate_data(self, course_id=None): - course_id = course_id or self.course_id +@ddt.ddt +class CourseActivityLastWeekTest(TestCaseWithAuthentication): + def generate_data(self, course_id): interval_start = datetime.datetime(2014, 1, 1, tzinfo=pytz.utc) interval_end = interval_start + datetime.timedelta(weeks=1) G(models.CourseActivityWeekly, course_id=course_id, interval_start=interval_start, @@ -177,26 +180,25 @@ def generate_data(self, course_id=None): interval_end=interval_end, activity_type='PLAYED_VIDEO', count=400) - def setUp(self): - super(CourseActivityLastWeekTest, self).setUp() - self.generate_data() - - def test_activity(self): - response = self.authenticated_get(u'/api/v0/courses/{0}/recent_activity'.format(self.course_id)) + @ddt.data(*CourseSamples.course_ids) + def test_activity(self, course_id): + self.generate_data(course_id) + response = self.authenticated_get(u'/api/v0/courses/{0}/recent_activity'.format(course_id)) 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( - self.course_id, activity_type)) + course_id, activity_type)) 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 def get_activity_record(**kwargs): datetime_format = "%Y-%m-%dT%H:%M:%SZ" 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_end': datetime.datetime(2014, 1, 8, 0, 0, tzinfo=pytz.utc).strftime(datetime_format), 'activity_type': 'any', @@ -206,27 +208,37 @@ def get_activity_record(**kwargs): default['activity_type'] = default['activity_type'].lower() return default - def test_activity_auth(self): - response = self.client.get(u'/api/v0/courses/{0}/recent_activity'.format(self.course_id), follow=True) + @ddt.data(*CourseSamples.course_ids) + 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) - def test_url_encoded_course_id(self): - url_encoded_course_id = urllib.quote_plus(self.course_id) + @ddt.data(*CourseSamples.course_ids) + 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)) 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): - self.assertValidActivityResponse('ANY', 300) - self.assertValidActivityResponse('any', 300) + @ddt.data(*CourseSamples.course_ids) + def test_any_activity(self, course_id): + self.generate_data(course_id) + self.assertValidActivityResponse(course_id, 'ANY', 300) + self.assertValidActivityResponse(course_id, 'any', 300) - def test_video_activity(self): - self.assertValidActivityResponse('played_video', 400) + @ddt.data(*CourseSamples.course_ids) + 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' 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) def test_unknown_course_id(self): @@ -237,38 +249,39 @@ def test_missing_course_id(self): response = self.authenticated_get(u'/api/v0/courses/recent_activity') 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' 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.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): path = '/enrollment/birth_year' model = models.CourseEnrollmentByBirthYear order_by = ['birth_year'] csv_filename_slug = u'enrollment-age' - def generate_data(self, course_id=None): - course_id = course_id or self.course_id + def generate_data(self, course_id): G(self.model, course_id=course_id, date=self.date, birth_year=1956) G(self.model, course_id=course_id, date=self.date, birth_year=1986) G(self.model, course_id=course_id, date=self.date - datetime.timedelta(days=10), birth_year=1956) G(self.model, course_id=course_id, date=self.date - datetime.timedelta(days=10), birth_year=1986) - def setUp(self): - super(CourseEnrollmentByBirthYearViewTests, self).setUp() - self.generate_data() - def format_as_response(self, *args): return [ {'course_id': unicode(ce.course_id), 'count': ce.count, 'date': ce.date.strftime(settings.DATE_FORMAT), 'birth_year': ce.birth_year, 'created': ce.created.strftime(settings.DATETIME_FORMAT)} for ce in args] - def test_get(self): - response = self.authenticated_get('/api/v0/courses/%s%s' % (self.course_id, self.path,)) + @ddt.data(*CourseSamples.course_ids) + 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) expected = self.format_as_response(*self.model.objects.filter(date=self.date)) @@ -281,17 +294,16 @@ class CourseEnrollmentByEducationViewTests(CourseEnrollmentViewTestCaseMixin, Te 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 + def generate_data(self, course_id): G(self.model, course_id=course_id, date=self.date, education_level=self.el1) G(self.model, course_id=course_id, date=self.date, education_level=self.el2) G(self.model, course_id=course_id, date=self.date - datetime.timedelta(days=2), education_level=self.el2) - def setUp(self): - super(CourseEnrollmentByEducationViewTests, self).setUp() - self.el1 = 'doctorate' - self.el2 = 'top_secret' - self.generate_data() + @classmethod + def setUpClass(cls): + super(CourseEnrollmentByEducationViewTests, cls).setUpClass() + cls.el1 = 'doctorate' + cls.el2 = 'top_secret' def format_as_response(self, *args): return [ @@ -300,6 +312,7 @@ def format_as_response(self, *args): ce in args] +@ddt.ddt class CourseEnrollmentByGenderViewTests(CourseEnrollmentViewTestCaseMixin, DefaultFillTestMixin, TestCaseWithAuthentication): path = '/enrollment/gender/' @@ -307,8 +320,7 @@ class CourseEnrollmentByGenderViewTests(CourseEnrollmentViewTestCaseMixin, Defau order_by = ['gender'] csv_filename_slug = u'enrollment-gender' - def generate_data(self, course_id=None): - course_id = course_id or self.course_id + def generate_data(self, course_id): _genders = ['f', 'm', 'o', None] days = 2 @@ -320,10 +332,6 @@ def generate_data(self, course_id=None): gender=gender, count=100 + day) - def setUp(self): - super(CourseEnrollmentByGenderViewTests, self).setUp() - self.generate_data() - def tearDown(self): self.destroy_data() @@ -350,11 +358,10 @@ def format_as_response(self, *args): return response - def test_default_fill(self): - self.destroy_data() - + @ddt.data(*CourseSamples.course_ids) + def test_default_fill(self, course_id): # 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 _genders = list(genders.ALL) @@ -365,7 +372,7 @@ def test_default_fill(self): for gender in _genders: expected[gender] = 0 - self.assertViewReturnsExpectedData([expected]) + self.assertViewReturnsExpectedData([expected], course_id) class CourseEnrollmentViewTests(CourseEnrollmentViewTestCaseMixin, TestCaseWithAuthentication): @@ -373,15 +380,10 @@ class CourseEnrollmentViewTests(CourseEnrollmentViewTestCaseMixin, TestCaseWithA path = '/enrollment' csv_filename_slug = u'enrollment' - def generate_data(self, course_id=None): - course_id = course_id or self.course_id + def generate_data(self, course_id): G(self.model, course_id=course_id, date=self.date, count=203) G(self.model, course_id=course_id, date=self.date - datetime.timedelta(days=5), count=203) - def setUp(self): - super(CourseEnrollmentViewTests, self).setUp() - self.generate_data() - def format_as_response(self, *args): return [ {'course_id': unicode(ce.course_id), 'count': ce.count, 'date': ce.date.strftime(settings.DATE_FORMAT), @@ -389,19 +391,14 @@ def format_as_response(self, *args): for ce in args] +@ddt.ddt class CourseEnrollmentModeViewTests(CourseEnrollmentViewTestCaseMixin, DefaultFillTestMixin, TestCaseWithAuthentication): model = models.CourseEnrollmentModeDaily path = '/enrollment/mode' csv_filename_slug = u'enrollment_mode' - def setUp(self): - super(CourseEnrollmentModeViewTests, self).setUp() - self.generate_data() - - def generate_data(self, course_id=None): - course_id = course_id or self.course_id - + def generate_data(self, course_id): for mode in enrollment_modes.ALL: G(self.model, course_id=course_id, date=self.date, mode=mode) @@ -432,11 +429,12 @@ def format_as_response(self, *args): return [response] - def test_default_fill(self): + @ddt.data(*CourseSamples.course_ids) + def test_default_fill(self, course_id): self.destroy_data() # 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) # Create the expected data @@ -451,7 +449,7 @@ def test_default_fill(self): expected[u'count'] = 1 expected[u'cumulative_count'] = 100 - self.assertViewReturnsExpectedData([expected]) + self.assertViewReturnsExpectedData([expected], course_id) class CourseEnrollmentByLocationViewTests(CourseEnrollmentViewTestCaseMixin, TestCaseWithAuthentication): @@ -482,8 +480,7 @@ def format_as_response(self, *args): return response - def generate_data(self, course_id=None): - course_id = course_id or self.course_id + def generate_data(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)) @@ -494,40 +491,37 @@ def generate_data(self, course_id=None): G(self.model, course_id=course_id, country_code='EU', count=4, date=self.date) G(self.model, course_id=course_id, country_code='O1', count=7, date=self.date) - def setUp(self): - super(CourseEnrollmentByLocationViewTests, self).setUp() - self.country = get_country('US') - self.generate_data() + @classmethod + def setUpClass(cls): + super(CourseEnrollmentByLocationViewTests, cls).setUpClass() + cls.country = get_country('US') +@ddt.ddt class CourseActivityWeeklyViewTests(CourseViewTestCaseMixin, TestCaseWithAuthentication): path = '/activity/' 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'] csv_filename_slug = u'engagement-activity' - def generate_data(self, course_id=None): - course_id = course_id or self.course_id - + def generate_data(self, course_id): for activity_type in self.activity_types: - G(CourseActivityWeekly, + G(models.CourseActivityWeekly, course_id=course_id, interval_start=self.interval_start, interval_end=self.interval_end, activity_type=activity_type, count=100) - def setUp(self): - super(CourseActivityWeeklyViewTests, self).setUp() - self.interval_start = datetime.datetime(2014, 1, 1, tzinfo=pytz.utc) - self.interval_end = self.interval_start + datetime.timedelta(weeks=1) + @classmethod + def setUpClass(cls): + super(CourseActivityWeeklyViewTests, cls).setUpClass() + 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=None): - course_id = course_id or self.course_id + def get_latest_data(self, course_id): return self.model.objects.filter(course_id=course_id, interval_end=self.interval_end) def format_as_response(self, *args): @@ -555,15 +549,16 @@ def format_as_response(self, *args): 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. """ - # Create additional data + self.generate_data(course_id) interval_start = self.interval_start + datetime.timedelta(weeks=1) interval_end = self.interval_end + datetime.timedelta(weeks=1) for activity_type in self.activity_types: - G(CourseActivityWeekly, - course_id=self.course_id, + G(models.CourseActivityWeekly, + course_id=course_id, interval_start=interval_start, interval_end=interval_end, activity_type=activity_type, @@ -571,20 +566,21 @@ def test_get_with_intervals(self): expected = self.format_as_response(*self.model.objects.all()) 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): - def _get_data(self, course_id=None): +@ddt.ddt +class CourseProblemsListViewTests(TestCaseWithAuthentication): + def _get_data(self, course_id): """ Retrieve data for the specified course. """ - - course_id = course_id or self.course_id url = '/api/v0/courses/{}/problems/'.format(course_id) 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. """ @@ -600,11 +596,11 @@ def test_get(self): alt_created = created + datetime.timedelta(seconds=2) 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)) - 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)) - 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)) expected = [ @@ -624,7 +620,7 @@ def test_get(self): } ] - response = self._get_data(self.course_id) + response = self._get_data(course_id) self.assertEquals(response.status_code, 200) self.assertListEqual([dict(d) for d in response.data], expected) @@ -637,17 +633,17 @@ def test_get_404(self): self.assertEquals(response.status_code, 404) -class CourseProblemsAndTagsListViewTests(DemoCourseMixin, TestCaseWithAuthentication): - def _get_data(self, course_id=None): +@ddt.ddt +class CourseProblemsAndTagsListViewTests(TestCaseWithAuthentication): + def _get_data(self, course_id): """ Retrieve data for the specified course. """ - - course_id = course_id or self.course_id url = '/api/v0/courses/{}/problems_and_tags/'.format(course_id) 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. """ @@ -668,13 +664,13 @@ def test_get(self): created = datetime.datetime.utcnow() 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], 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], 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], total_submissions=4, correct_submissions=0, created=created) @@ -700,7 +696,7 @@ def test_get(self): } ] - response = self._get_data(self.course_id) + response = self._get_data(course_id) self.assertEquals(response.status_code, 200) self.assertListEqual(sorted([dict(d) for d in response.data]), sorted(expected)) @@ -713,16 +709,17 @@ def test_get_404(self): self.assertEquals(response.status_code, 404) -class CourseVideosListViewTests(DemoCourseMixin, TestCaseWithAuthentication): - def _get_data(self, course_id=None): +@ddt.ddt +class CourseVideosListViewTests(TestCaseWithAuthentication): + def _get_data(self, course_id): """ Retrieve videos for a specified course. """ - course_id = course_id or self.course_id url = '/api/v0/courses/{}/videos/'.format(course_id) 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 G(models.Video) @@ -730,14 +727,14 @@ def test_get(self): video_id = 'v1d30' created = datetime.datetime.utcnow() 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, created=created.strftime(date_time_format)) alt_module_id = 'i4x-test-video-2' alt_video_id = 'a1d30' 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, created=alt_created.strftime(date_time_format)) @@ -762,7 +759,7 @@ def test_get(self): } ] - response = self._get_data(self.course_id) + response = self._get_data(course_id) self.assertEquals(response.status_code, 200) self.assertListEqual(response.data, expected) @@ -771,34 +768,38 @@ def test_get_404(self): self.assertEquals(response.status_code, 404) -class CourseReportDownloadViewTests(DemoCourseMixin, TestCaseWithAuthentication): +@ddt.ddt +class CourseReportDownloadViewTests(TestCaseWithAuthentication): path = '/api/v0/courses/{course_id}/reports/{report_name}' @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( self.path.format( - course_id=DEMO_COURSE_ID, + course_id=course_id, report_name='problem_response' ) ) 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( self.path.format( - course_id=DEMO_COURSE_ID, + course_id=course_id, report_name='fake_problem_that_we_dont_support' ) ) self.assertEqual(response.status_code, 404) @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( self.path.format( - course_id=DEMO_COURSE_ID, + course_id=course_id, report_name='problem_response' ) ) @@ -815,16 +816,17 @@ def test_incompatible_storage_provider(self): 'analytics_data_api.utils.get_expiration_date', 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( self.path.format( - course_id=DEMO_COURSE_ID, + course_id=course_id, report_name='problem_response' ) ) self.assertEqual(response.status_code, 200) expected = { - 'course_id': SANITIZED_DEMO_COURSE_ID, + 'course_id': get_filename_safe_course_id(course_id), 'report_name': 'problem_response', 'download_url': 'http://fake', 'last_modified': datetime.datetime(2014, 1, 1, tzinfo=pytz.utc).strftime(settings.DATETIME_FORMAT), @@ -844,16 +846,17 @@ def test_make_working_link(self): 'analytics_data_api.utils.get_expiration_date', 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( self.path.format( - course_id=DEMO_COURSE_ID, + course_id=course_id, report_name='problem_response' ) ) self.assertEqual(response.status_code, 200) expected = { - 'course_id': SANITIZED_DEMO_COURSE_ID, + 'course_id': get_filename_safe_course_id(course_id), 'report_name': 'problem_response', 'download_url': 'http://fake', 'last_modified': datetime.datetime(2014, 1, 1, tzinfo=pytz.utc).strftime(settings.DATETIME_FORMAT), @@ -869,16 +872,17 @@ def test_make_working_link_with_missing_size(self): 'analytics_data_api.utils.get_expiration_date', 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( self.path.format( - course_id=DEMO_COURSE_ID, + course_id=course_id, report_name='problem_response' ) ) self.assertEqual(response.status_code, 200) expected = { - 'course_id': SANITIZED_DEMO_COURSE_ID, + 'course_id': get_filename_safe_course_id(course_id), 'report_name': 'problem_response', 'download_url': 'http://fake', 'file_size': 1000, diff --git a/analytics_data_api/v0/tests/views/test_engagement_timelines.py b/analytics_data_api/v0/tests/views/test_engagement_timelines.py index 7f7f8595..50618e2f 100644 --- a/analytics_data_api/v0/tests/views/test_engagement_timelines.py +++ b/analytics_data_api/v0/tests/views/test_engagement_timelines.py @@ -12,21 +12,21 @@ from analytics_data_api.constants.engagement_events import (ATTEMPTED, COMPLETED, CONTRIBUTED, DISCUSSION, PROBLEM, VIDEO, VIEWED) 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 -class EngagementTimelineTests(DemoCourseMixin, VerifyCourseIdMixin, TestCaseWithAuthentication): +class EngagementTimelineTests(VerifyCourseIdMixin, TestCaseWithAuthentication): DEFAULT_USERNAME = 'ed_xavier' 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""" if date is None: date = datetime.datetime(2015, 1, 1, tzinfo=pytz.utc) G( models.ModuleEngagement, - course_id=self.course_id, + course_id=course_id, username=self.DEFAULT_USERNAME, date=date, entity_type=entity_type, @@ -36,19 +36,19 @@ def create_engagement(self, entity_type, event_type, entity_id, count, date=None ) @ddt.data( - (PROBLEM, ATTEMPTED, 'problems_attempted', True), - (PROBLEM, COMPLETED, 'problems_completed', True), - (VIDEO, VIEWED, 'videos_viewed', True), - (DISCUSSION, CONTRIBUTED, 'discussion_contributions', False), + (CourseSamples.course_ids[0], PROBLEM, ATTEMPTED, 'problems_attempted', True), + (CourseSamples.course_ids[1], PROBLEM, COMPLETED, 'problems_completed', True), + (CourseSamples.course_ids[2], VIDEO, VIEWED, 'videos_viewed', True), + (CourseSamples.course_ids[0], DISCUSSION, CONTRIBUTED, 'discussion_contributions', False), ) @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 counted by total interactions. """ - self.create_engagement(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) + self.create_engagement(course_id, entity_type, event_type, 'entity-id', count=5) expected_data = { 'days': [ { @@ -64,7 +64,7 @@ def test_metric_aggregation(self, entity_type, event_type, metric_display_name, expected_data['days'][0][metric_display_name] = 1 else: 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) self.assertEquals(response.status_code, 200) self.assertEquals( @@ -72,20 +72,21 @@ def test_metric_aggregation(self, entity_type, event_type, metric_display_name, expected_data ) - def test_timeline(self): + @ddt.data(*CourseSamples.course_ids) + def test_timeline(self, course_id): """ 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_two = datetime.datetime(2015, 1, 2, tzinfo=pytz.utc) - self.create_engagement(PROBLEM, ATTEMPTED, 'id-1', count=100, date=day_one) - self.create_engagement(PROBLEM, COMPLETED, 'id-2', count=12, date=day_one) - self.create_engagement(DISCUSSION, CONTRIBUTED, 'id-3', count=6, date=day_one) - self.create_engagement(DISCUSSION, CONTRIBUTED, 'id-4', count=10, date=day_two) - self.create_engagement(VIDEO, VIEWED, 'id-5', count=44, date=day_two) - self.create_engagement(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-1', count=100, date=day_one) + self.create_engagement(course_id, PROBLEM, COMPLETED, 'id-2', count=12, date=day_one) + self.create_engagement(course_id, DISCUSSION, CONTRIBUTED, 'id-3', count=6, date=day_one) + self.create_engagement(course_id, DISCUSSION, CONTRIBUTED, 'id-4', count=10, date=day_two) + self.create_engagement(course_id, VIDEO, VIEWED, 'id-5', count=44, date=day_two) + self.create_engagement(course_id, PROBLEM, ATTEMPTED, 'id-6', count=8, date=day_two) + self.create_engagement(course_id, PROBLEM, ATTEMPTED, 'id-7', count=4, date=day_two) response = self.authenticated_get(path) self.assertEquals(response.status_code, 200) expected = { @@ -108,12 +109,13 @@ def test_timeline(self): } self.assertEquals(response.data, expected) - def test_day_gap(self): - path = self.path_template.format(self.DEFAULT_USERNAME, urlquote(self.course_id)) + @ddt.data(*CourseSamples.course_ids) + 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) 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(PROBLEM, ATTEMPTED, entity_id='id-2', count=1, date=last_day) + self.create_engagement(course_id, VIDEO, VIEWED, 'id-1', count=1, date=first_day) + self.create_engagement(course_id, PROBLEM, ATTEMPTED, entity_id='id-2', count=1, date=last_day) response = self.authenticated_get(path) self.assertEquals(response.status_code, 200) expected = { @@ -143,14 +145,15 @@ def test_day_gap(self): } self.assertEquals(response.data, expected) - def test_not_found(self): - path = self.path_template.format(self.DEFAULT_USERNAME, urlquote(self.course_id)) + @ddt.data(*CourseSamples.course_ids) + def test_not_found(self, course_id): + path = self.path_template.format(self.DEFAULT_USERNAME, urlquote(course_id)) response = self.authenticated_get(path) self.assertEquals(response.status_code, status.HTTP_404_NOT_FOUND) expected = { u"error_code": u"no_learner_engagement_timeline", 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) diff --git a/analytics_data_api/v0/tests/views/test_learners.py b/analytics_data_api/v0/tests/views/test_learners.py index cba18fe3..5200d8d0 100644 --- a/analytics_data_api/v0/tests/views/test_learners.py +++ b/analytics_data_api/v0/tests/views/test_learners.py @@ -20,7 +20,7 @@ from analytics_data_api.v0.models import ModuleEngagementMetricRanges from analytics_data_api.v0.views import CsvViewMixin, PaginatedHeadersMixin from analytics_data_api.v0.tests.views import ( - DemoCourseMixin, VerifyCourseIdMixin, VerifyCsvResponseMixin, + CourseSamples, VerifyCourseIdMixin, VerifyCsvResponseMixin, ) @@ -33,6 +33,9 @@ def setUp(self): super(LearnerAPITestMixin, self).setUp() self._es = Elasticsearch([settings.ELASTICSEARCH_LEARNERS_HOST]) 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')) def _create_learner( @@ -641,8 +644,7 @@ def test_csv_fields(self, fields, valid_fields): @ddt.ddt -class CourseLearnerMetadataTests(DemoCourseMixin, VerifyCourseIdMixin, - LearnerAPITestMixin, TestCaseWithAuthentication): +class CourseLearnerMetadataTests(VerifyCourseIdMixin, LearnerAPITestMixin, TestCaseWithAuthentication,): """ Tests for the course learner metadata endpoint. """ @@ -651,8 +653,8 @@ def _get(self, course_id): """Helper to send a GET request to the API.""" return self.authenticated_get('/api/v0/course_learner_metadata/{}/'.format(course_id)) - def get_expected_json(self, segments, enrollment_modes, cohorts): - expected_json = self._get_full_engagement_ranges() + def get_expected_json(self, course_id, segments, enrollment_modes, cohorts): + expected_json = self._get_full_engagement_ranges(course_id) expected_json['segments'] = segments expected_json['enrollment_modes'] = enrollment_modes expected_json['cohorts'] = cohorts @@ -667,22 +669,24 @@ def test_no_course_id(self): self.assertEqual(response.status_code, 404) @ddt.data( - {}, - {'highly_engaged': 1}, - {'disengaging': 1}, - {'struggling': 1}, - {'inactive': 1}, - {'unenrolled': 1}, - {'highly_engaged': 3, 'disengaging': 1}, - {'disengaging': 10, 'inactive': 12}, - {'highly_engaged': 1, 'disengaging': 2, 'struggling': 3, 'inactive': 4, 'unenrolled': 5}, + (CourseSamples.course_ids[0], {}), + (CourseSamples.course_ids[1], {'highly_engaged': 1}), + (CourseSamples.course_ids[2], {'disengaging': 1}), + (CourseSamples.course_ids[0], {'struggling': 1}), + (CourseSamples.course_ids[1], {'inactive': 1}), + (CourseSamples.course_ids[2], {'unenrolled': 1}), + (CourseSamples.course_ids[0], {'highly_engaged': 3, 'disengaging': 1}), + (CourseSamples.course_ids[1], {'disengaging': 10, 'inactive': 12}), + (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. """ 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 i in xrange(count) ] @@ -690,39 +694,43 @@ def test_segments_unique_learners(self, segments): expected_segments = {"highly_engaged": 0, "disengaging": 0, "struggling": 0, "inactive": 0, "unenrolled": 0} expected_segments.update(segments) expected = self.get_expected_json( + course_id=course_id, segments=expected_segments, enrollment_modes={'honor': 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. """ self.create_learners([ - {'username': 'user_1', 'course_id': self.course_id, 'segments': ['struggling', 'disengaging']}, - {'username': 'user_2', 'course_id': self.course_id, 'segments': ['disengaging']} + {'username': 'user_1', 'course_id': course_id, 'segments': ['struggling', 'disengaging']}, + {'username': 'user_2', 'course_id': course_id, 'segments': ['disengaging']} ]) expected = self.get_expected_json( + course_id=course_id, segments={'disengaging': 2, 'struggling': 1, 'highly_engaged': 0, 'inactive': 0, 'unenrolled': 0}, enrollment_modes={'honor': 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( - [], - ['honor'], - ['verified'], - ['audit'], - ['nonexistent-enrollment-tracks-still-show-up'], - ['honor', 'verified', 'audit'], - ['honor', 'honor', 'verified', 'verified', 'audit', 'audit'], + (CourseSamples.course_ids[0], []), + (CourseSamples.course_ids[1], ['honor']), + (CourseSamples.course_ids[2], ['verified']), + (CourseSamples.course_ids[0], ['audit']), + (CourseSamples.course_ids[1], ['nonexistent-enrollment-tracks-still-show-up']), + (CourseSamples.course_ids[2], ['honor', 'verified', '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([ - {'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) ]) expected_enrollment_modes = {} @@ -731,32 +739,35 @@ def test_enrollment_modes(self, enrollment_modes): count = len([mode for mode in group]) expected_enrollment_modes[enrollment_mode] = count expected = self.get_expected_json( + course_id=course_id, segments={'disengaging': 0, 'struggling': 0, 'highly_engaged': 0, 'inactive': 0, 'unenrolled': 0}, enrollment_modes=expected_enrollment_modes, 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( - [], - ['Yellow'], - ['Blue'], - ['Red', 'Red', 'yellow team', 'yellow team', 'green'], + (CourseSamples.course_ids[0], []), + (CourseSamples.course_ids[1], ['Yellow']), + (CourseSamples.course_ids[2], ['Blue']), + (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([ - {'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) ]) expected_cohorts = { cohort: len([mode for mode in group]) for cohort, group in groupby(cohorts) } expected = self.get_expected_json( + course_id=course_id, segments={'disengaging': 0, 'struggling': 0, 'highly_engaged': 0, 'inactive': 0, 'unenrolled': 0}, enrollment_modes={'honor': len(cohorts)} if cohorts else {}, 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 def empty_engagement_ranges(self): @@ -776,16 +787,18 @@ def empty_engagement_ranges(self): empty_engagement_ranges['engagement_ranges'][metric] = copy.deepcopy(empty_range) return empty_engagement_ranges - def test_no_engagement_ranges(self): - response = self._get(self.course_id) + @ddt.data(*CourseSamples.course_ids) + def test_no_engagement_ranges(self, course_id): + response = self._get(course_id) self.assertEqual(response.status_code, 200) 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' start_date = datetime.date(2015, 7, 1) 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) expected_ranges = self.empty_engagement_ranges expected_ranges['engagement_ranges'].update({ @@ -800,11 +813,11 @@ def test_one_engagement_range(self): } }) - response = self._get(self.course_id) + response = self._get(course_id) self.assertEqual(response.status_code, 200) 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. """ start_date = datetime.date(2015, 7, 1) end_date = datetime.date(2015, 7, 21) @@ -821,10 +834,10 @@ def _get_full_engagement_ranges(self): max_value = 1000.0 for metric_type in engagement_events.EVENTS: 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) 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) expected['engagement_ranges'][metric_type] = { @@ -843,15 +856,17 @@ def _get_full_engagement_ranges(self): return expected - def test_engagement_ranges_only(self): - expected = self._get_full_engagement_ranges() - response = self._get(self.course_id) + @ddt.data(*CourseSamples.course_ids) + def test_engagement_ranges_only(self, course_id): + expected = self._get_full_engagement_ranges(course_id) + response = self._get(course_id) self.assertEqual(response.status_code, 200) 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 - response = json.loads(self._get(self.course_id).content) + response = json.loads(self._get(course_id).content) self.assertTrue('engagement_ranges' in response) for event in expected_events: self.assertTrue(event in response['engagement_ranges']) diff --git a/requirements/base.txt b/requirements/base.txt index 1ae0467f..50b23012 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,16 +1,17 @@ boto==2.42.0 # MIT Django==1.9.9 # BSD License +django-countries==4.0 # MIT django-model-utils==2.5.2 # BSD djangorestframework==3.4.6 # BSD django-rest-swagger==0.3.8 # BSD djangorestframework-csv==1.4.1 # BSD -django-countries==4.0 # MIT -edx-django-release-util==0.1.2 +django-storages==1.4.1 # BSD elasticsearch-dsl==0.0.11 # Apache 2.0 ordered-set==2.0.1 # MIT # markdown is used by swagger for rendering the api docs Markdown==2.6.6 # BSD --e git+https://github.com/edx/opaque-keys.git@d45d0bd8d64c69531be69178b9505b5d38806ce0#egg=opaque-keys -django-storages==1.4.1 # BSD +edx-ccx-keys==0.2.1 +edx-django-release-util==0.1.2 +edx-opaque-keys==0.4.0 diff --git a/requirements/test.txt b/requirements/test.txt index 05edff78..0429ed28 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -1,7 +1,7 @@ # Test dependencies go here. -r base.txt coverage==4.2 -ddt==1.1.0 +ddt==1.1.1 diff-cover >= 0.9.9 django-dynamic-fixture==1.9.0 django-nose==1.4.4