Commit acefe9fc by Clinton Blackburn

Updated enrollment resources to support historical data

Change-Id: I88bf4ebefd9742f4647b0e9383fdbac9f97ac805
parent dbde3921
...@@ -88,5 +88,95 @@ ...@@ -88,5 +88,95 @@
"count": 9940, "count": 9940,
"date": "2014-07-01" "date": "2014-07-01"
} }
},
{
"model": "v0.CourseEnrollmentByEducation",
"pk": 14,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"education_level": 6,
"count": 12295,
"date": "2014-07-02"
}
},
{
"model": "v0.CourseEnrollmentByEducation",
"pk": 6,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"education_level": 7,
"count": 70885,
"date": "2014-07-01"
}
},
{
"model": "v0.CourseEnrollmentByEducation",
"pk": 7,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"education_level": 3,
"count": 981,
"date": "2014-07-01"
}
},
{
"model": "v0.CourseEnrollmentByEducation",
"pk": 8,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"education_level": 5,
"count": 51591,
"date": "2014-07-01"
}
},
{
"model": "v0.CourseEnrollmentByEducation",
"pk": 9,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"education_level": 4,
"count": 6051,
"date": "2014-07-01"
}
},
{
"model": "v0.CourseEnrollmentByEducation",
"pk": 10,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"education_level": 8,
"count": 53216,
"date": "2014-07-01"
}
},
{
"model": "v0.CourseEnrollmentByEducation",
"pk": 11,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"education_level": 1,
"count": 667,
"date": "2014-07-01"
}
},
{
"model": "v0.CourseEnrollmentByEducation",
"pk": 12,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"education_level": 2,
"count": 5722,
"date": "2014-07-01"
}
},
{
"model": "v0.CourseEnrollmentByEducation",
"pk": 13,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"education_level": 9,
"count": 9940,
"date": "2014-07-01"
}
} }
] ]
...@@ -28,5 +28,35 @@ ...@@ -28,5 +28,35 @@
"count": 423, "count": 423,
"date": "2014-07-01" "date": "2014-07-01"
} }
},
{
"model": "v0.CourseEnrollmentByGender",
"pk": 4,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"gender": "m",
"count": 1332,
"date": "2014-07-02"
}
},
{
"model": "v0.CourseEnrollmentByGender",
"pk": 5,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"gender": "f",
"count": 77445,
"date": "2014-07-02"
}
},
{
"model": "v0.CourseEnrollmentByGender",
"pk": 6,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"gender": "o",
"count": 34,
"date": "2014-07-02"
}
} }
] ]
from django.conf import settings from django.conf import settings
from rest_framework import serializers from rest_framework import serializers
from analytics_data_api.v0.models import CourseActivityByWeek, ProblemResponseAnswerDistribution, \ from analytics_data_api.v0 import models
CourseEnrollmentDaily, CourseEnrollmentByCountry, Country
class CourseIdMixin(object): class CourseIdMixin(object):
...@@ -24,7 +23,7 @@ class CourseActivityByWeekSerializer(serializers.ModelSerializer, CourseIdMixin) ...@@ -24,7 +23,7 @@ class CourseActivityByWeekSerializer(serializers.ModelSerializer, CourseIdMixin)
course_id = RequiredSerializerMethodField('get_course_id') course_id = RequiredSerializerMethodField('get_course_id')
class Meta(object): class Meta(object):
model = CourseActivityByWeek model = models.CourseActivityByWeek
fields = ('interval_start', 'interval_end', 'activity_type', 'count', 'course_id') fields = ('interval_start', 'interval_end', 'activity_type', 'count', 'course_id')
...@@ -37,7 +36,7 @@ class ProblemResponseAnswerDistributionSerializer(serializers.ModelSerializer): ...@@ -37,7 +36,7 @@ class ProblemResponseAnswerDistributionSerializer(serializers.ModelSerializer):
""" """
class Meta(object): class Meta(object):
model = ProblemResponseAnswerDistribution model = models.ProblemResponseAnswerDistribution
fields = ( fields = (
'course_id', 'course_id',
'module_id', 'module_id',
...@@ -52,30 +51,56 @@ class ProblemResponseAnswerDistributionSerializer(serializers.ModelSerializer): ...@@ -52,30 +51,56 @@ class ProblemResponseAnswerDistributionSerializer(serializers.ModelSerializer):
) )
class CourseEnrollmentDailySerializer(serializers.ModelSerializer, CourseIdMixin): class BaseCourseEnrollmentModelSerializer(serializers.ModelSerializer, CourseIdMixin):
"""
Representation of course enrollment for a single day and course.
"""
course_id = RequiredSerializerMethodField('get_course_id') course_id = RequiredSerializerMethodField('get_course_id')
date = serializers.DateField(format=settings.DATE_FORMAT)
class CourseEnrollmentDailySerializer(BaseCourseEnrollmentModelSerializer):
""" Representation of course enrollment for a single day and course. """
class Meta(object): class Meta(object):
model = CourseEnrollmentDaily model = models.CourseEnrollmentDaily
fields = ('course_id', 'date', 'count') fields = ('course_id', 'date', 'count')
# pylint: disable=no-value-for-parameter # pylint: disable=no-value-for-parameter
class CountrySerializer(serializers.ModelSerializer): class CountrySerializer(serializers.ModelSerializer):
class Meta(object): class Meta(object):
model = Country model = models.Country
fields = ('code', 'name') fields = ('code', 'name')
class CourseEnrollmentByCountrySerializer(serializers.ModelSerializer, CourseIdMixin): # pylint: disable=no-value-for-parameter
course_id = RequiredSerializerMethodField('get_course_id') class EducationLevelSerializer(serializers.ModelSerializer):
class Meta(object):
model = models.EducationLevel
fields = ('name', 'short_name')
class CourseEnrollmentByCountrySerializer(BaseCourseEnrollmentModelSerializer):
country = CountrySerializer() country = CountrySerializer()
date = serializers.DateField(format=settings.DATE_FORMAT)
class Meta(object): class Meta(object):
model = CourseEnrollmentByCountry model = models.CourseEnrollmentByCountry
fields = ('date', 'course_id', 'country', 'count') fields = ('date', 'course_id', 'country', 'count')
class CourseEnrollmentByGenderSerializer(BaseCourseEnrollmentModelSerializer):
class Meta(object):
model = models.CourseEnrollmentByGender
fields = ('course_id', 'date', 'gender', 'count')
class CourseEnrollmentByEducationSerializer(BaseCourseEnrollmentModelSerializer):
education_level = EducationLevelSerializer()
class Meta(object):
model = models.CourseEnrollmentByEducation
fields = ('course_id', 'date', 'education_level', 'count')
class CourseEnrollmentByBirthYearSerializer(BaseCourseEnrollmentModelSerializer):
class Meta(object):
model = models.CourseEnrollmentByBirthYear
fields = ('course_id', 'date', 'birth_year', 'count')
...@@ -7,7 +7,7 @@ from analytics_data_api.v0.views import courses as views ...@@ -7,7 +7,7 @@ from analytics_data_api.v0.views import courses as views
COURSE_URLS = [ COURSE_URLS = [
('recent_activity', views.CourseActivityMostRecentWeekView, 'recent_activity'), ('recent_activity', views.CourseActivityMostRecentWeekView, 'recent_activity'),
('enrollment', views.CourseEnrollmentLatestView, 'enrollment_latest'), ('enrollment', views.CourseEnrollmentView, 'enrollment_latest'),
('enrollment/birth_year', views.CourseEnrollmentByBirthYearView, 'enrollment_by_birth_year'), ('enrollment/birth_year', views.CourseEnrollmentByBirthYearView, 'enrollment_by_birth_year'),
('enrollment/education', views.CourseEnrollmentByEducationView, 'enrollment_by_education'), ('enrollment/education', views.CourseEnrollmentByEducationView, 'enrollment_by_education'),
('enrollment/gender', views.CourseEnrollmentByGenderView, 'enrollment_by_gender'), ('enrollment/gender', views.CourseEnrollmentByGenderView, 'enrollment_by_gender'),
......
import datetime import datetime
from django.conf import settings from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Max from django.db.models import Max
from django.http import Http404 from django.http import Http404
from rest_framework import generics from rest_framework import generics
from rest_framework.generics import RetrieveAPIView, get_object_or_404 from rest_framework.generics import get_object_or_404
from rest_framework.response import Response
from rest_framework.views import APIView
from analytics_data_api.v0.models import CourseActivityByWeek, CourseEnrollmentByBirthYear, \ from analytics_data_api.v0 import models, serializers
CourseEnrollmentByEducation, CourseEnrollmentByGender, CourseEnrollmentByCountry, CourseEnrollmentDaily, Course
from analytics_data_api.v0.serializers import CourseActivityByWeekSerializer, CourseEnrollmentByCountrySerializer, \
CourseEnrollmentDailySerializer
class CourseActivityMostRecentWeekView(generics.RetrieveAPIView): class CourseActivityMostRecentWeekView(generics.RetrieveAPIView):
...@@ -42,7 +38,7 @@ class CourseActivityMostRecentWeekView(generics.RetrieveAPIView): ...@@ -42,7 +38,7 @@ class CourseActivityMostRecentWeekView(generics.RetrieveAPIView):
""" """
serializer_class = CourseActivityByWeekSerializer serializer_class = serializers.CourseActivityByWeekSerializer
def get_object(self, queryset=None): def get_object(self, queryset=None):
"""Select the activity report for the given course and activity type.""" """Select the activity report for the given course and activity type."""
...@@ -51,63 +47,78 @@ class CourseActivityMostRecentWeekView(generics.RetrieveAPIView): ...@@ -51,63 +47,78 @@ class CourseActivityMostRecentWeekView(generics.RetrieveAPIView):
activity_type = activity_type.lower() activity_type = activity_type.lower()
try: try:
return CourseActivityByWeek.get_most_recent(course_id, activity_type) return models.CourseActivityByWeek.get_most_recent(course_id, activity_type)
except ObjectDoesNotExist: except ObjectDoesNotExist:
raise Http404 raise Http404
class AbstractCourseEnrollmentView(APIView): class BaseCourseEnrollmentView(generics.ListAPIView):
model = None def get_course_or_404(self):
return get_object_or_404(models.Course, course_id=self.kwargs.get('course_id'))
def render_data(self, data):
"""
Render view data
"""
raise NotImplementedError('Subclasses must define a render_data method!')
def get(self, request, *args, **kwargs): # pylint: disable=unused-argument
if not self.model:
raise NotImplementedError('Subclasses must specify a model!')
course_id = self.kwargs['course_id'] def apply_date_filtering(self, queryset):
data = self.model.objects.filter(course__course_id=course_id) if 'start_date' in self.request.QUERY_PARAMS or 'end_date' in self.request.QUERY_PARAMS:
# Filter by start/end date
start_date = self.request.QUERY_PARAMS.get('start_date')
if start_date:
start_date = datetime.datetime.strptime(start_date, settings.DATE_FORMAT)
queryset = queryset.filter(date__gte=start_date)
if not data: end_date = self.request.QUERY_PARAMS.get('end_date')
raise Http404 if end_date:
end_date = datetime.datetime.strptime(end_date, settings.DATE_FORMAT)
queryset = queryset.filter(date__lt=end_date)
else:
# No date filter supplied, so only return data for the latest date
latest_date = queryset.aggregate(Max('date'))
if latest_date:
latest_date = latest_date['date__max']
queryset = queryset.filter(date=latest_date)
return queryset
return Response(self.render_data(data)) def get_queryset(self):
course = self.get_course_or_404()
queryset = self.model.objects.filter(course=course)
queryset = self.apply_date_filtering(queryset)
return queryset
class CourseEnrollmentByBirthYearView(AbstractCourseEnrollmentView): class CourseEnrollmentByBirthYearView(BaseCourseEnrollmentView):
""" """
Course enrollment broken down by user birth year Course enrollment broken down by user birth year
Returns the enrollment of a course with users binned by their birth years. Returns the enrollment of a course with users binned by their birth years.
"""
model = CourseEnrollmentByBirthYear If no start or end dates are passed, the data for the latest date is returned. All dates should are in the UTC zone.
Date format: YYYY-mm-dd (e.g. 2014-01-31)
start_date -- Date after which all data should be returned (inclusive)
end_date -- Date before which all data should be returned (exclusive)
"""
def render_data(self, data): serializer_class = serializers.CourseEnrollmentByBirthYearSerializer
return { model = models.CourseEnrollmentByBirthYear
'birth_years': dict(data.values_list('birth_year', 'count'))
}
class CourseEnrollmentByEducationView(AbstractCourseEnrollmentView): class CourseEnrollmentByEducationView(BaseCourseEnrollmentView):
""" """
Course enrollment broken down by user level of education Course enrollment broken down by user level of education
Returns the enrollment of a course with users binned by their education levels. Returns the enrollment of a course with users binned by their education levels.
"""
model = CourseEnrollmentByEducation
def render_data(self, data): If no start or end dates are passed, the data for the latest date is returned. All dates should are in the UTC zone.
return {
'education_levels': dict(data.values_list('education_level__short_name', 'count')) Date format: YYYY-mm-dd (e.g. 2014-01-31)
}
start_date -- Date after which all data should be returned (inclusive)
end_date -- Date before which all data should be returned (exclusive)
"""
serializer_class = serializers.CourseEnrollmentByEducationSerializer
model = models.CourseEnrollmentByEducation
class CourseEnrollmentByGenderView(AbstractCourseEnrollmentView): class CourseEnrollmentByGenderView(BaseCourseEnrollmentView):
""" """
Course enrollment broken down by user gender Course enrollment broken down by user gender
...@@ -117,66 +128,56 @@ class CourseEnrollmentByGenderView(AbstractCourseEnrollmentView): ...@@ -117,66 +128,56 @@ class CourseEnrollmentByGenderView(AbstractCourseEnrollmentView):
m - male m - male
f - female f - female
o - other o - other
If no start or end dates are passed, the data for the latest date is returned. All dates should are in the UTC zone.
Date format: YYYY-mm-dd (e.g. 2014-01-31)
start_date -- Date after which all data should be returned (inclusive)
end_date -- Date before which all data should be returned (exclusive)
""" """
model = CourseEnrollmentByGender serializer_class = serializers.CourseEnrollmentByGenderSerializer
model = models.CourseEnrollmentByGender
def render_data(self, data):
return {
'genders': dict(data.values_list('gender', 'count'))
}
class CourseEnrollmentView(BaseCourseEnrollmentView):
"""
Returns the enrollment count for the specified course.
class CourseEnrollmentLatestView(RetrieveAPIView): If no start or end dates are passed, the data for the latest date is returned. All dates should are in the UTC zone.
""" Returns the latest enrollment count for the specified course. """
model = CourseEnrollmentDaily
serializer_class = CourseEnrollmentDailySerializer
def get_object(self, queryset=None): Date format: YYYY-mm-dd (e.g. 2014-01-31)
try:
course_id = self.kwargs['course_id'] start_date -- Date after which all data should be returned (inclusive)
return CourseEnrollmentDaily.objects.filter(course__course_id=course_id).order_by('-date')[0] end_date -- Date before which all data should be returned (exclusive)
except IndexError: """
raise Http404
serializer_class = serializers.CourseEnrollmentDailySerializer
model = models.CourseEnrollmentDaily
# pylint: disable=line-too-long # pylint: disable=line-too-long
class CourseEnrollmentByLocationView(generics.ListAPIView): class CourseEnrollmentByLocationView(BaseCourseEnrollmentView):
""" """
Course enrollment broken down by user location Course enrollment broken down by user location
Returns the enrollment of a course with users binned by their location. Location is calculated based on the user's Returns the enrollment of a course with users binned by their location. Location is calculated based on the user's
IP address. If no start or end dates are passed, the data for the latest date is returned. IP address.
Countries are denoted by their <a href="http://www.iso.org/iso/country_codes/country_codes" target="_blank">ISO 3166 country code</a>. Countries are denoted by their <a href="http://www.iso.org/iso/country_codes/country_codes" target="_blank">ISO 3166 country code</a>.
If no start or end dates are passed, the data for the latest date is returned. All dates should are in the UTC zone.
Date format: YYYY-mm-dd (e.g. 2014-01-31) Date format: YYYY-mm-dd (e.g. 2014-01-31)
start_date -- Date after which all data should be returned (inclusive) start_date -- Date after which all data should be returned (inclusive)
end_date -- Date before which all data should be returned (exclusive) end_date -- Date before which all data should be returned (exclusive)
""" """
serializer_class = CourseEnrollmentByCountrySerializer serializer_class = serializers.CourseEnrollmentByCountrySerializer
def get_queryset(self): def get_queryset(self):
course = get_object_or_404(Course, course_id=self.kwargs.get('course_id')) course = self.get_course_or_404()
queryset = CourseEnrollmentByCountry.objects.filter(course=course) queryset = models.CourseEnrollmentByCountry.objects.filter(course=course)
queryset = self.apply_date_filtering(queryset)
if 'start_date' in self.request.QUERY_PARAMS or 'end_date' in self.request.QUERY_PARAMS:
# Filter by start/end date
start_date = self.request.QUERY_PARAMS.get('start_date')
if start_date:
start_date = datetime.datetime.strptime(start_date, settings.DATE_FORMAT)
queryset = queryset.filter(date__gte=start_date)
end_date = self.request.QUERY_PARAMS.get('end_date')
if end_date:
end_date = datetime.datetime.strptime(end_date, settings.DATE_FORMAT)
queryset = queryset.filter(date__lt=end_date)
else:
# No date filter supplied, so only return data for the latest date
latest_date = queryset.aggregate(Max('date'))
if latest_date:
latest_date = latest_date['date__max']
queryset = queryset.filter(date=latest_date)
return queryset return queryset
...@@ -83,3 +83,7 @@ ENABLE_ADMIN_SITE = True ...@@ -83,3 +83,7 @@ ENABLE_ADMIN_SITE = True
########## END ANALYTICS DATA API CONFIGURATION ########## END ANALYTICS DATA API CONFIGURATION
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
SWAGGER_SETTINGS = {
'api_key': 'analytics'
}
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