Commit 376dce83 by Clinton Blackburn

Updated Demographics and Enrollment Mode Serializers with Defaults

Both serializers will not return 0 if a demographic or mode field value is set to None.
parent 1e0124df
...@@ -2,3 +2,4 @@ FEMALE = u'female' ...@@ -2,3 +2,4 @@ FEMALE = u'female'
MALE = u'male' MALE = u'male'
OTHER = u'other' OTHER = u'other'
UNKNOWN = u'unknown' UNKNOWN = u'unknown'
ALL = [FEMALE, MALE, OTHER, UNKNOWN]
from django.conf import settings from django.conf import settings
from rest_framework import serializers from rest_framework import serializers
from analytics_data_api.constants import enrollment_modes from analytics_data_api.constants import enrollment_modes, genders
from analytics_data_api.v0 import models from analytics_data_api.v0 import models
...@@ -90,6 +90,9 @@ class BaseCourseEnrollmentModelSerializer(serializers.ModelSerializer): ...@@ -90,6 +90,9 @@ class BaseCourseEnrollmentModelSerializer(serializers.ModelSerializer):
date = serializers.DateField(format=settings.DATE_FORMAT) date = serializers.DateField(format=settings.DATE_FORMAT)
created = serializers.DateTimeField(format=settings.DATETIME_FORMAT) created = serializers.DateTimeField(format=settings.DATETIME_FORMAT)
def default_if_none(self, value, default=0):
return value if value is not None else default
class CourseEnrollmentDailySerializer(BaseCourseEnrollmentModelSerializer): class CourseEnrollmentDailySerializer(BaseCourseEnrollmentModelSerializer):
""" Representation of course enrollment for a single day and course. """ """ Representation of course enrollment for a single day and course. """
...@@ -110,8 +113,14 @@ class CourseEnrollmentModeDailySerializer(BaseCourseEnrollmentModelSerializer): ...@@ -110,8 +113,14 @@ class CourseEnrollmentModeDailySerializer(BaseCourseEnrollmentModelSerializer):
for mode in enrollment_modes.ALL: for mode in enrollment_modes.ALL:
fields[mode] = serializers.IntegerField(required=True, default=0) fields[mode] = serializers.IntegerField(required=True, default=0)
# Create a transform method for each field
setattr(self, 'transform_%s' % mode, self._transform_mode)
return fields return fields
def _transform_mode(self, _obj, value):
return self.default_if_none(value, 0)
class Meta(object): class Meta(object):
model = models.CourseEnrollmentDaily model = models.CourseEnrollmentDaily
...@@ -142,10 +151,21 @@ class CourseEnrollmentByCountrySerializer(BaseCourseEnrollmentModelSerializer): ...@@ -142,10 +151,21 @@ class CourseEnrollmentByCountrySerializer(BaseCourseEnrollmentModelSerializer):
class CourseEnrollmentByGenderSerializer(BaseCourseEnrollmentModelSerializer): class CourseEnrollmentByGenderSerializer(BaseCourseEnrollmentModelSerializer):
female = serializers.IntegerField(required=False) def get_default_fields(self):
male = serializers.IntegerField(required=False) # pylint: disable=super-on-old-class
other = serializers.IntegerField(required=False) fields = super(CourseEnrollmentByGenderSerializer, self).get_default_fields()
unknown = serializers.IntegerField(required=False)
# Create a field for each gender
for gender in genders.ALL:
fields[gender] = serializers.IntegerField(required=True, default=0)
# Create a transform method for each field
setattr(self, 'transform_%s' % gender, self._transform_gender)
return fields
def _transform_gender(self, _obj, value):
return self.default_if_none(value, 0)
class Meta(object): class Meta(object):
model = models.CourseEnrollmentByGender model = models.CourseEnrollmentByGender
......
...@@ -15,7 +15,7 @@ import pytz ...@@ -15,7 +15,7 @@ import pytz
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from analytics_data_api.v0 import models from analytics_data_api.v0 import models
from analytics_data_api.constants import country, enrollment_modes from analytics_data_api.constants import country, enrollment_modes, genders
from analytics_data_api.v0.models import CourseActivityWeekly from analytics_data_api.v0.models import CourseActivityWeekly
from analytics_data_api.v0.serializers import ProblemResponseAnswerDistributionSerializer from analytics_data_api.v0.serializers import ProblemResponseAnswerDistributionSerializer
from analytics_data_api.v0.serializers import GradeDistributionSerializer from analytics_data_api.v0.serializers import GradeDistributionSerializer
...@@ -37,6 +37,20 @@ class DemoCourseMixin(object): ...@@ -37,6 +37,20 @@ class DemoCourseMixin(object):
super(DemoCourseMixin, self).setUp() super(DemoCourseMixin, self).setUp()
class DefaultFillTestMixin(object):
"""
Test that the view fills in missing data with a default value.
"""
model = None
def destroy_data(self):
self.model.objects.all().delete()
def test_default_fill(self):
raise NotImplementedError
# pylint: disable=no-member # pylint: disable=no-member
class CourseViewTestCaseMixin(DemoCourseMixin): class CourseViewTestCaseMixin(DemoCourseMixin):
model = None model = None
...@@ -75,16 +89,19 @@ class CourseViewTestCaseMixin(DemoCourseMixin): ...@@ -75,16 +89,19 @@ class CourseViewTestCaseMixin(DemoCourseMixin):
response = self.authenticated_get(u'%scourses/%s%s' % (self.api_root_path, course_id, self.path)) response = self.authenticated_get(u'%scourses/%s%s' % (self.api_root_path, course_id, self.path))
self.assertEquals(response.status_code, 404) self.assertEquals(response.status_code, 404)
def test_get(self): def assertViewReturnsExpectedData(self, expected):
""" Verify the endpoint returns an HTTP 200 status and the correct data. """
# Validate the basic response status # Validate the basic response status
response = self.authenticated_get(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, self.course_id, self.path))
self.assertEquals(response.status_code, 200) self.assertEquals(response.status_code, 200)
# Validate the data is correct and sorted chronologically # Validate the data is correct and sorted chronologically
expected = self.format_as_response(*self.get_latest_data())
self.assertEquals(response.data, expected) self.assertEquals(response.data, expected)
def test_get(self):
""" Verify the endpoint returns an HTTP 200 status and the correct data. """
expected = self.format_as_response(*self.get_latest_data())
self.assertViewReturnsExpectedData(expected)
def assertCSVIsValid(self, course_id, filename): def assertCSVIsValid(self, course_id, filename):
path = u'{0}courses/{1}{2}'.format(self.api_root_path, course_id, self.path) path = u'{0}courses/{1}{2}'.format(self.api_root_path, course_id, self.path)
csv_content_type = 'text/csv' csv_content_type = 'text/csv'
...@@ -307,7 +324,8 @@ class CourseEnrollmentByEducationViewTests(CourseEnrollmentViewTestCaseMixin, Te ...@@ -307,7 +324,8 @@ class CourseEnrollmentByEducationViewTests(CourseEnrollmentViewTestCaseMixin, Te
ce in args] ce in args]
class CourseEnrollmentByGenderViewTests(CourseEnrollmentViewTestCaseMixin, TestCaseWithAuthentication): class CourseEnrollmentByGenderViewTests(CourseEnrollmentViewTestCaseMixin, DefaultFillTestMixin,
TestCaseWithAuthentication):
path = '/enrollment/gender/' path = '/enrollment/gender/'
model = models.CourseEnrollmentByGender model = models.CourseEnrollmentByGender
order_by = ['gender'] order_by = ['gender']
...@@ -315,11 +333,11 @@ class CourseEnrollmentByGenderViewTests(CourseEnrollmentViewTestCaseMixin, TestC ...@@ -315,11 +333,11 @@ class CourseEnrollmentByGenderViewTests(CourseEnrollmentViewTestCaseMixin, TestC
def generate_data(self, course_id=None): def generate_data(self, course_id=None):
course_id = course_id or self.course_id course_id = course_id or self.course_id
genders = ['f', 'm', 'o', None] _genders = ['f', 'm', 'o', None]
days = 2 days = 2
for day in range(days): for day in range(days):
for gender in genders: for gender in _genders:
G(self.model, G(self.model,
course_id=course_id, course_id=course_id,
date=self.date - datetime.timedelta(days=day), date=self.date - datetime.timedelta(days=day),
...@@ -330,6 +348,14 @@ class CourseEnrollmentByGenderViewTests(CourseEnrollmentViewTestCaseMixin, TestC ...@@ -330,6 +348,14 @@ class CourseEnrollmentByGenderViewTests(CourseEnrollmentViewTestCaseMixin, TestC
super(CourseEnrollmentByGenderViewTests, self).setUp() super(CourseEnrollmentByGenderViewTests, self).setUp()
self.generate_data() self.generate_data()
def serialize_enrollment(self, enrollment):
return {
'created': enrollment.created.strftime(settings.DATETIME_FORMAT),
'course_id': unicode(enrollment.course_id),
'date': enrollment.date.strftime(settings.DATE_FORMAT),
enrollment.cleaned_gender: enrollment.count
}
def format_as_response(self, *args): def format_as_response(self, *args):
response = [] response = []
...@@ -339,17 +365,30 @@ class CourseEnrollmentByGenderViewTests(CourseEnrollmentViewTestCaseMixin, TestC ...@@ -339,17 +365,30 @@ class CourseEnrollmentByGenderViewTests(CourseEnrollmentViewTestCaseMixin, TestC
item = {} item = {}
for enrollment in group: for enrollment in group:
item.update({ item.update(self.serialize_enrollment(enrollment))
'created': enrollment.created.strftime(settings.DATETIME_FORMAT),
'course_id': unicode(enrollment.course_id),
'date': enrollment.date.strftime(settings.DATE_FORMAT),
enrollment.cleaned_gender: enrollment.count
})
response.append(item) response.append(item)
return response return response
def test_default_fill(self):
self.destroy_data()
# Create a single entry for a single gender
enrollment = G(self.model, course_id=self.course_id, date=self.date, gender='f', count=1)
# Create the expected data
_genders = list(genders.ALL)
_genders.remove(genders.FEMALE)
expected = self.serialize_enrollment(enrollment)
for gender in _genders:
expected[gender] = 0
expected = [expected]
self.assertViewReturnsExpectedData(expected)
# pylint: disable=no-member,no-value-for-parameter # pylint: disable=no-member,no-value-for-parameter
class AnswerDistributionTests(TestCaseWithAuthentication): class AnswerDistributionTests(TestCaseWithAuthentication):
...@@ -403,7 +442,8 @@ class CourseEnrollmentViewTests(CourseEnrollmentViewTestCaseMixin, TestCaseWithA ...@@ -403,7 +442,8 @@ class CourseEnrollmentViewTests(CourseEnrollmentViewTestCaseMixin, TestCaseWithA
for ce in args] for ce in args]
class CourseEnrollmentModeViewTests(CourseEnrollmentViewTestCaseMixin, TestCaseWithAuthentication): class CourseEnrollmentModeViewTests(CourseEnrollmentViewTestCaseMixin, DefaultFillTestMixin,
TestCaseWithAuthentication):
model = models.CourseEnrollmentModeDaily model = models.CourseEnrollmentModeDaily
path = '/enrollment/mode' path = '/enrollment/mode'
csv_filename_slug = u'enrollment_mode' csv_filename_slug = u'enrollment_mode'
...@@ -418,13 +458,17 @@ class CourseEnrollmentModeViewTests(CourseEnrollmentViewTestCaseMixin, TestCaseW ...@@ -418,13 +458,17 @@ class CourseEnrollmentModeViewTests(CourseEnrollmentViewTestCaseMixin, TestCaseW
for mode in enrollment_modes.ALL: for mode in enrollment_modes.ALL:
G(self.model, course_id=course_id, date=self.date, mode=mode) G(self.model, course_id=course_id, date=self.date, mode=mode)
def serialize_enrollment(self, enrollment):
return {
u'course_id': enrollment.course_id,
u'date': enrollment.date.strftime(settings.DATE_FORMAT),
u'created': enrollment.created.strftime(settings.DATETIME_FORMAT),
enrollment.mode: enrollment.count
}
def format_as_response(self, *args): def format_as_response(self, *args):
arg = args[0] arg = args[0]
response = { response = self.serialize_enrollment(arg)
u'course_id': arg.course_id,
u'date': arg.date.strftime(settings.DATE_FORMAT),
u'created': arg.created.strftime(settings.DATETIME_FORMAT)
}
total = 0 total = 0
for ce in args: for ce in args:
...@@ -435,6 +479,25 @@ class CourseEnrollmentModeViewTests(CourseEnrollmentViewTestCaseMixin, TestCaseW ...@@ -435,6 +479,25 @@ class CourseEnrollmentModeViewTests(CourseEnrollmentViewTestCaseMixin, TestCaseW
return [response] return [response]
def test_default_fill(self):
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, count=1)
# Create the expected data
modes = list(enrollment_modes.ALL)
modes.remove(enrollment_modes.AUDIT)
expected = self.serialize_enrollment(enrollment)
expected[u'count'] = 1
for mode in modes:
expected[mode] = 0
expected = [expected]
self.assertViewReturnsExpectedData(expected)
class CourseEnrollmentByLocationViewTests(CourseEnrollmentViewTestCaseMixin, TestCaseWithAuthentication): class CourseEnrollmentByLocationViewTests(CourseEnrollmentViewTestCaseMixin, TestCaseWithAuthentication):
path = '/enrollment/location/' path = '/enrollment/location/'
......
...@@ -2,7 +2,7 @@ Django==1.6.6 # BSD License ...@@ -2,7 +2,7 @@ Django==1.6.6 # BSD License
Markdown==2.4.1 # BSD Markdown==2.4.1 # BSD
South==1.0 # Apache License South==1.0 # Apache License
django-model-utils==1.4.0 # BSD django-model-utils==1.4.0 # BSD
djangorestframework==2.3.5 # BSD djangorestframework==2.4.4 # BSD
ipython==2.1.0 # BSD ipython==2.1.0 # BSD
django-rest-swagger==0.1.14 # BSD django-rest-swagger==0.1.14 # BSD
djangorestframework-csv==1.3.3 # BSD djangorestframework-csv==1.3.3 # BSD
......
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