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
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):
......
......@@ -7,21 +7,22 @@ import datetime
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 @@ class CourseViewTestCaseMixin(DemoCourseMixin, VerifyCsvResponseMixin):
"""
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 @@ class CourseViewTestCaseMixin(DemoCourseMixin, VerifyCsvResponseMixin):
"""
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 @@ class CourseViewTestCaseMixin(DemoCourseMixin, VerifyCsvResponseMixin):
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 @@ class CourseViewTestCaseMixin(DemoCourseMixin, VerifyCsvResponseMixin):
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 @@ class CourseActivityLastWeekTest(DemoCourseMixin, TestCaseWithAuthentication):
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 @@ class CourseActivityLastWeekTest(DemoCourseMixin, TestCaseWithAuthentication):
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 @@ class CourseActivityLastWeekTest(DemoCourseMixin, TestCaseWithAuthentication):
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 @@ class CourseEnrollmentByEducationViewTests(CourseEnrollmentViewTestCaseMixin, Te
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 @@ class CourseEnrollmentByGenderViewTests(CourseEnrollmentViewTestCaseMixin, Defau
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 @@ class CourseEnrollmentByGenderViewTests(CourseEnrollmentViewTestCaseMixin, Defau
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 @@ class CourseEnrollmentByGenderViewTests(CourseEnrollmentViewTestCaseMixin, Defau
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 @@ class CourseEnrollmentViewTests(CourseEnrollmentViewTestCaseMixin, TestCaseWithA
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 @@ class CourseEnrollmentModeViewTests(CourseEnrollmentViewTestCaseMixin, DefaultFi
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 @@ class CourseEnrollmentModeViewTests(CourseEnrollmentViewTestCaseMixin, DefaultFi
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 @@ class CourseEnrollmentByLocationViewTests(CourseEnrollmentViewTestCaseMixin, Tes
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 @@ 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='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 @@ class CourseActivityWeeklyViewTests(CourseViewTestCaseMixin, TestCaseWithAuthent
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 @@ class CourseActivityWeeklyViewTests(CourseViewTestCaseMixin, TestCaseWithAuthent
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 @@ class CourseProblemsListViewTests(DemoCourseMixin, TestCaseWithAuthentication):
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 @@ class CourseProblemsListViewTests(DemoCourseMixin, TestCaseWithAuthentication):
}
]
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 @@ class CourseProblemsListViewTests(DemoCourseMixin, TestCaseWithAuthentication):
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 @@ class CourseProblemsAndTagsListViewTests(DemoCourseMixin, TestCaseWithAuthentica
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 @@ class CourseProblemsAndTagsListViewTests(DemoCourseMixin, TestCaseWithAuthentica
}
]
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 @@ class CourseProblemsAndTagsListViewTests(DemoCourseMixin, TestCaseWithAuthentica
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 @@ class CourseVideosListViewTests(DemoCourseMixin, TestCaseWithAuthentication):
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 @@ class CourseVideosListViewTests(DemoCourseMixin, TestCaseWithAuthentication):
}
]
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 @@ class CourseVideosListViewTests(DemoCourseMixin, TestCaseWithAuthentication):
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 @@ class CourseReportDownloadViewTests(DemoCourseMixin, TestCaseWithAuthentication)
'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 @@ class CourseReportDownloadViewTests(DemoCourseMixin, TestCaseWithAuthentication)
'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 @@ class CourseReportDownloadViewTests(DemoCourseMixin, TestCaseWithAuthentication)
'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,
......
......@@ -12,21 +12,21 @@ from analyticsdataserver.tests import TestCaseWithAuthentication
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 @@ class EngagementTimelineTests(DemoCourseMixin, VerifyCourseIdMixin, TestCaseWith
)
@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 @@ class EngagementTimelineTests(DemoCourseMixin, VerifyCourseIdMixin, TestCaseWith
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 @@ class EngagementTimelineTests(DemoCourseMixin, VerifyCourseIdMixin, TestCaseWith
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 @@ class EngagementTimelineTests(DemoCourseMixin, VerifyCourseIdMixin, TestCaseWith
}
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 @@ class EngagementTimelineTests(DemoCourseMixin, VerifyCourseIdMixin, TestCaseWith
}
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)
......
......@@ -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.views import CsvViewMixin, PaginatedHeadersMixin
from analytics_data_api.v0.tests.views import (
DemoCourseMixin, VerifyCourseIdMixin, VerifyCsvResponseMixin,
CourseSamples, VerifyCourseIdMixin, VerifyCsvResponseMixin,
)
......@@ -33,6 +33,9 @@ class LearnerAPITestMixin(CsvViewMixin):
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 @@ class LearnerCsvListTests(LearnerAPITestMixin, VerifyCourseIdMixin,
@ddt.ddt
class CourseLearnerMetadataTests(DemoCourseMixin, VerifyCourseIdMixin,
LearnerAPITestMixin, TestCaseWithAuthentication):
class CourseLearnerMetadataTests(VerifyCourseIdMixin, LearnerAPITestMixin, TestCaseWithAuthentication,):
"""
Tests for the course learner metadata endpoint.
"""
......@@ -651,8 +653,8 @@ class CourseLearnerMetadataTests(DemoCourseMixin, VerifyCourseIdMixin,
"""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 @@ class CourseLearnerMetadataTests(DemoCourseMixin, VerifyCourseIdMixin,
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 @@ class CourseLearnerMetadataTests(DemoCourseMixin, VerifyCourseIdMixin,
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 @@ class CourseLearnerMetadataTests(DemoCourseMixin, VerifyCourseIdMixin,
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 @@ class CourseLearnerMetadataTests(DemoCourseMixin, VerifyCourseIdMixin,
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 @@ class CourseLearnerMetadataTests(DemoCourseMixin, VerifyCourseIdMixin,
}
})
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 @@ class CourseLearnerMetadataTests(DemoCourseMixin, VerifyCourseIdMixin,
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 @@ class CourseLearnerMetadataTests(DemoCourseMixin, VerifyCourseIdMixin,
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'])
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
# 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
......
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