Commit 6596d90f by Dennis Jen

Added course meta summary enrollment

parent d2fcb7b3
......@@ -12,21 +12,27 @@ from analytics_data_api.constants.engagement_types import EngagementType
from analytics_data_api.utils import date_range
class CourseActivityWeekly(models.Model):
"""A count of unique users who performed a particular action during a week."""
class BaseCourseModel(models.Model):
course_id = models.CharField(db_index=True, max_length=255)
created = models.DateTimeField(auto_now_add=True)
class Meta(object):
abstract = True
class CourseActivityWeekly(BaseCourseModel):
"""A count of unique users who performed a particular action during a week."""
class Meta(BaseCourseModel.Meta):
db_table = 'course_activity'
index_together = [['course_id', 'activity_type']]
ordering = ('interval_end', 'interval_start', 'course_id')
get_latest_by = 'interval_end'
course_id = models.CharField(db_index=True, max_length=255)
interval_start = models.DateTimeField()
interval_end = models.DateTimeField(db_index=True)
activity_type = models.CharField(db_index=True, max_length=255, db_column='label')
count = models.IntegerField()
created = models.DateTimeField(auto_now_add=True)
@classmethod
def get_most_recent(cls, course_id, activity_type):
......@@ -34,13 +40,11 @@ class CourseActivityWeekly(models.Model):
return cls.objects.filter(course_id=course_id, activity_type=activity_type).latest('interval_end')
class BaseCourseEnrollment(models.Model):
course_id = models.CharField(max_length=255)
class BaseCourseEnrollment(BaseCourseModel):
date = models.DateField(null=False, db_index=True)
count = models.IntegerField(null=False)
created = models.DateTimeField(auto_now_add=True)
class Meta(object):
class Meta(BaseCourseModel.Meta):
abstract = True
get_latest_by = 'date'
index_together = [('course_id', 'date',)]
......@@ -63,6 +67,23 @@ class CourseEnrollmentModeDaily(BaseCourseEnrollment):
unique_together = [('course_id', 'date', 'mode')]
class CourseMetaSummaryEnrollment(BaseCourseModel):
catalog_course_title = models.CharField(db_index=True, max_length=255)
catalog_course = models.CharField(db_index=True, max_length=255)
start_date = models.DateTimeField()
end_date = models.DateTimeField()
pacing_type = models.CharField(db_index=True, max_length=255)
availability = models.CharField(db_index=True, max_length=255)
mode = models.CharField(max_length=255)
cumulative_count = models.IntegerField(null=False)
count_change_7_days = models.IntegerField(default=0)
class Meta(BaseCourseModel.Meta):
db_table = 'course_meta_summary_enrollment'
ordering = ('course_id',)
unique_together = [('course_id', 'mode',)]
class CourseEnrollmentByBirthYear(BaseCourseEnrollment):
birth_year = models.IntegerField(null=False)
......@@ -103,14 +124,13 @@ class CourseEnrollmentByGender(BaseCourseEnrollment):
unique_together = [('course_id', 'date', 'gender')]
class BaseProblemResponseAnswerDistribution(models.Model):
class BaseProblemResponseAnswerDistribution(BaseCourseModel):
""" Base model for the answer_distribution table. """
class Meta(object):
class Meta(BaseCourseModel.Meta):
db_table = 'answer_distribution'
abstract = True
course_id = models.CharField(db_index=True, max_length=255)
module_id = models.CharField(db_index=True, max_length=255)
part_id = models.CharField(db_index=True, max_length=255)
correct = models.NullBooleanField()
......@@ -119,7 +139,6 @@ class BaseProblemResponseAnswerDistribution(models.Model):
variant = models.IntegerField(null=True)
problem_display_name = models.TextField(null=True)
question_text = models.TextField(null=True)
created = models.DateTimeField(auto_now_add=True)
class ProblemResponseAnswerDistribution(BaseProblemResponseAnswerDistribution):
......@@ -131,19 +150,17 @@ class ProblemResponseAnswerDistribution(BaseProblemResponseAnswerDistribution):
count = models.IntegerField()
class ProblemsAndTags(models.Model):
class ProblemsAndTags(BaseCourseModel):
""" Model for the tags_distribution table """
class Meta(object):
class Meta(BaseCourseModel.Meta):
db_table = 'tags_distribution'
course_id = models.CharField(db_index=True, max_length=255)
module_id = models.CharField(db_index=True, max_length=255)
tag_name = models.CharField(max_length=255)
tag_value = models.CharField(max_length=255)
total_submissions = models.IntegerField(default=0)
correct_submissions = models.IntegerField(default=0)
created = models.DateTimeField(auto_now_add=True)
class ProblemFirstLastResponseAnswerDistribution(BaseProblemResponseAnswerDistribution):
......@@ -172,30 +189,26 @@ class CourseEnrollmentByCountry(BaseCourseEnrollment):
unique_together = [('course_id', 'date', 'country_code')]
class GradeDistribution(models.Model):
class GradeDistribution(BaseCourseModel):
""" Each row stores the count of a particular grade on a module for a given course. """
class Meta(object):
class Meta(BaseCourseModel.Meta):
db_table = 'grade_distribution'
module_id = models.CharField(db_index=True, max_length=255)
course_id = models.CharField(db_index=True, max_length=255)
grade = models.IntegerField()
max_grade = models.IntegerField()
count = models.IntegerField()
created = models.DateTimeField(auto_now_add=True)
class SequentialOpenDistribution(models.Model):
class SequentialOpenDistribution(BaseCourseModel):
""" Each row stores the count of views a particular module has had in a given course. """
class Meta(object):
class Meta(BaseCourseModel.Meta):
db_table = 'sequential_open_distribution'
module_id = models.CharField(db_index=True, max_length=255)
course_id = models.CharField(db_index=True, max_length=255)
count = models.IntegerField()
created = models.DateTimeField(auto_now_add=True)
class BaseVideo(models.Model):
......@@ -465,10 +478,9 @@ class ModuleEngagementTimelineManager(models.Manager):
return full_timeline
class ModuleEngagement(models.Model):
class ModuleEngagement(BaseCourseModel):
"""User interactions with entities within the courseware."""
course_id = models.CharField(db_index=True, max_length=255)
username = models.CharField(max_length=255)
date = models.DateField()
# This will be one of "problem", "video" or "discussion"
......@@ -483,18 +495,17 @@ class ModuleEngagement(models.Model):
objects = ModuleEngagementTimelineManager()
class Meta(object):
class Meta(BaseCourseModel.Meta):
db_table = 'module_engagement'
class ModuleEngagementMetricRanges(models.Model):
class ModuleEngagementMetricRanges(BaseCourseModel):
"""
Represents the low and high values for a module engagement entity and event
pair, known as the metric. The range_type will either be low, normal, or
high, bounded by low_value and high_value.
"""
course_id = models.CharField(db_index=True, max_length=255)
start_date = models.DateField()
# This is a left-closed interval. No data from the end_date is included in the analysis.
end_date = models.DateField()
......@@ -505,5 +516,5 @@ class ModuleEngagementMetricRanges(models.Model):
high_value = models.FloatField()
low_value = models.FloatField()
class Meta(object):
class Meta(BaseCourseModel.Meta):
db_table = 'module_engagement_metric_ranges'
......@@ -507,3 +507,22 @@ class CourseLearnerMetadataSerializer(serializers.Serializer):
})
return engagement_ranges
class CourseMetaSummaryEnrollmentSerializer(ModelSerializerWithCreatedField):
"""
Serializer for problems.
"""
course_id = serializers.CharField()
catalog_course_title = serializers.CharField()
catalog_course = serializers.CharField()
start_date = serializers.DateTimeField()
end_date = serializers.DateTimeField()
pacing_type = serializers.CharField()
availability = serializers.CharField()
mode = serializers.CharField()
cumulative_count = serializers.IntegerField(default=0)
count_change_7_days = serializers.IntegerField(default=0) # TODO: 0 as default?
class Meta(object):
model = models.CourseMetaSummaryEnrollment
import ddt
from mock import Mock
from django.http import Http404
from django.test import TestCase
from analytics_data_api.v0.exceptions import CourseKeyMalformedError
import analytics_data_api.v0.views.utils as utils
@ddt.ddt
class UtilsTest(TestCase):
@ddt.data(
None,
'not-a-key',
)
def test_invalid_course_id(self, course_id):
with self.assertRaises(CourseKeyMalformedError):
utils.validate_course_id(course_id)
# TODO: DDT w/ the refactored CourseSamples once https://github.com/edx/edx-analytics-data-api/pull/143 merges
@ddt.data(
'edX/DemoX/Demo_Course',
'course-v1:edX+DemoX+Demo_2014',
)
def test_valid_course_id(self, course_id):
try:
utils.validate_course_id(course_id)
except CourseKeyMalformedError:
self.fail('Unexpected CourseKeyMalformedError!')
def test_split_query_argument_none(self):
self.assertIsNone(utils.split_query_argument(None))
@ddt.data(
('one', ['one']),
('one,two', ['one', 'two']),
)
@ddt.unpack
def test_split_query_argument(self, input, expected):
self.assertListEqual(utils.split_query_argument(input), expected)
def test_raise_404_if_none_raises_error(self):
decorated_func = utils.raise_404_if_none(Mock(return_value=None))
with self.assertRaises(Http404):
decorated_func(self)
def test_raise_404_if_none_passes_through(self):
decorated_func = utils.raise_404_if_none(Mock(return_value='Not a 404'))
self.assertEqual(decorated_func(self), 'Not a 404')
......@@ -9,6 +9,7 @@ urlpatterns = [
url(r'^problems/', include('analytics_data_api.v0.urls.problems', 'problems')),
url(r'^videos/', include('analytics_data_api.v0.urls.videos', 'videos')),
url('^', include('analytics_data_api.v0.urls.learners', 'learners')),
url('^', include('analytics_data_api.v0.urls.course_summaries', 'course_summaries')),
# pylint: disable=no-value-for-parameter
url(r'^authenticated/$', RedirectView.as_view(url=reverse_lazy('authenticated')), name='authenticated'),
......
from django.conf.urls import url
from analytics_data_api.v0.views import course_summaries as views
USERNAME_PATTERN = r'(?P<username>[\w.+-]+)'
urlpatterns = [
url(r'^course_summaries/$', views.CourseSummariesView.as_view(), name='course_summaries'),
]
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from django.utils import timezone
from rest_framework.response import Response
from analytics_data_api.v0.exceptions import (CourseNotSpecifiedError, CourseKeyMalformedError)
from analytics_data_api.v0.exceptions import CourseNotSpecifiedError
import analytics_data_api.utils as utils
class CourseViewMixin(object):
"""
......@@ -18,10 +16,7 @@ class CourseViewMixin(object):
if not self.course_id:
raise CourseNotSpecifiedError()
try:
CourseKey.from_string(self.course_id)
except InvalidKeyError:
raise CourseKeyMalformedError(course_id=self.course_id)
utils.validate_course_id(self.course_id)
return super(CourseViewMixin, self).get(request, *args, **kwargs)
......
"""
TODO: TBD
"""
from django.db.models import Q
from rest_framework import generics
from analytics_data_api.v0 import models, serializers
from analytics_data_api.v0.views.utils import (
raise_404_if_none,
split_query_argument,
validate_course_id,
)
class CourseSummariesView(generics.ListAPIView):
"""
TBD.
**Example Request**
GET /api/v0/course_summaries/?course_ids={course_id},{course_id}
**Response Values**
TBD
**Parameters**
You can specify the course IDs for which you want data.
course_ids -- The comma-separated course identifiers for which user data is requested.
For example, edX/DemoX/Demo_Course,course-v1:edX+DemoX+Demo_2016
"""
course_ids = None
fields = None
serializer_class = serializers.CourseMetaSummaryEnrollmentSerializer
model = models.CourseMetaSummaryEnrollment
def get(self, request, *args, **kwargs):
query_params = self.request.query_params
self.fields = split_query_argument(query_params.get('fields'))
self.course_ids = split_query_argument(query_params.get('course_ids'))
if self.course_ids is not None:
for course_id in self.course_ids:
validate_course_id(course_id)
return super(CourseSummariesView, self).get(request, *args, **kwargs)
@raise_404_if_none
def get_queryset(self):
if self.course_ids:
# create an OR query for course IDs that match
query = reduce(lambda q, course_id: q|Q(course_id=course_id), self.course_ids, Q())
queryset = self.model.objects.filter(query)
else:
queryset = self.model.objects.all()
return queryset
"""Utilities for view-level API logic."""
from django.http import Http404
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from analytics_data_api.v0.exceptions import CourseKeyMalformedError
def split_query_argument(argument):
"""
......@@ -15,7 +20,7 @@ def split_query_argument(argument):
def raise_404_if_none(func):
"""
Decorator for raiseing Http404 if function evaulation is falsey (e.g. empty queryset).
Decorator for raising Http404 if function evaluation is falsey (e.g. empty queryset).
"""
def func_wrapper(self):
queryset = func(self)
......@@ -24,3 +29,11 @@ def raise_404_if_none(func):
else:
raise Http404
return func_wrapper
def validate_course_id(course_id):
"""Raises CourseKeyMalformedError if course ID is invalid."""
try:
CourseKey.from_string(course_id)
except InvalidKeyError:
raise CourseKeyMalformedError(course_id=course_id)
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