Commit e95f2178 by Dennis Jen Committed by GitHub

Merge pull request #135 from edx/dsjen/updgrade-drf

Update DRF to 3.4.6
parents 1e8aa546 87c4a49c
...@@ -234,7 +234,7 @@ class Video(BaseVideo): ...@@ -234,7 +234,7 @@ class Video(BaseVideo):
class RosterUpdate(DocType): class RosterUpdate(DocType):
date = Date() date = Date(format=settings.DATE_FORMAT)
# pylint: disable=old-style-class # pylint: disable=old-style-class
class Meta: class Meta:
...@@ -265,8 +265,8 @@ class RosterEntry(DocType): ...@@ -265,8 +265,8 @@ class RosterEntry(DocType):
attempt_ratio_order = Integer() attempt_ratio_order = Integer()
discussion_contributions = Integer() discussion_contributions = Integer()
videos_watched = Integer() videos_watched = Integer()
enrollment_date = Date() enrollment_date = Date(format=settings.DATE_FORMAT)
last_updated = Date() last_updated = Date(format=settings.DATE_FORMAT)
# pylint: disable=old-style-class # pylint: disable=old-style-class
class Meta: class Meta:
...@@ -460,7 +460,7 @@ class ModuleEngagement(models.Model): ...@@ -460,7 +460,7 @@ class ModuleEngagement(models.Model):
course_id = models.CharField(db_index=True, max_length=255) course_id = models.CharField(db_index=True, max_length=255)
username = models.CharField(max_length=255) username = models.CharField(max_length=255)
date = models.DateTimeField() date = models.DateField()
# This will be one of "problem", "video" or "discussion" # This will be one of "problem", "video" or "discussion"
entity_type = models.CharField(max_length=255) entity_type = models.CharField(max_length=255)
# For problems this will be the usage key, for videos it will be the html encoded module ID, # For problems this will be the usage key, for videos it will be the html encoded module ID,
......
from collections import OrderedDict
from urlparse import urljoin from urlparse import urljoin
from django.conf import settings from django.conf import settings
from rest_framework import pagination, serializers from rest_framework import pagination, serializers
from rest_framework.response import Response
from analytics_data_api.constants import ( from analytics_data_api.constants import (
engagement_events, engagement_events,
enrollment_modes, enrollment_modes,
genders, learner,
) )
from analytics_data_api.v0 import models from analytics_data_api.v0 import models
...@@ -23,7 +25,7 @@ class CourseActivityByWeekSerializer(serializers.ModelSerializer): ...@@ -23,7 +25,7 @@ class CourseActivityByWeekSerializer(serializers.ModelSerializer):
particular record is likely to change unexpectedly so we avoid exposing it. particular record is likely to change unexpectedly so we avoid exposing it.
""" """
activity_type = serializers.SerializerMethodField('get_activity_type') activity_type = serializers.SerializerMethodField()
def get_activity_type(self, obj): def get_activity_type(self, obj):
""" """
...@@ -45,6 +47,7 @@ class ModelSerializerWithCreatedField(serializers.ModelSerializer): ...@@ -45,6 +47,7 @@ class ModelSerializerWithCreatedField(serializers.ModelSerializer):
created = serializers.DateTimeField(format=settings.DATETIME_FORMAT) created = serializers.DateTimeField(format=settings.DATETIME_FORMAT)
# pylint: disable=abstract-method
class ProblemSerializer(serializers.Serializer): class ProblemSerializer(serializers.Serializer):
""" """
Serializer for problems. Serializer for problems.
...@@ -57,6 +60,7 @@ class ProblemSerializer(serializers.Serializer): ...@@ -57,6 +60,7 @@ class ProblemSerializer(serializers.Serializer):
created = serializers.DateTimeField(format=settings.DATETIME_FORMAT) created = serializers.DateTimeField(format=settings.DATETIME_FORMAT)
# pylint: disable=abstract-method
class ProblemsAndTagsSerializer(serializers.Serializer): class ProblemsAndTagsSerializer(serializers.Serializer):
""" """
Serializer for problems and tags. Serializer for problems and tags.
...@@ -187,13 +191,7 @@ class SequentialOpenDistributionSerializer(ModelSerializerWithCreatedField): ...@@ -187,13 +191,7 @@ class SequentialOpenDistributionSerializer(ModelSerializerWithCreatedField):
) )
class DefaultIfNoneMixin(object): class BaseCourseEnrollmentModelSerializer(ModelSerializerWithCreatedField):
def default_if_none(self, value, default=0):
return value if value is not None else default
class BaseCourseEnrollmentModelSerializer(DefaultIfNoneMixin, ModelSerializerWithCreatedField):
date = serializers.DateField(format=settings.DATE_FORMAT) date = serializers.DateField(format=settings.DATE_FORMAT)
...@@ -207,32 +205,35 @@ class CourseEnrollmentDailySerializer(BaseCourseEnrollmentModelSerializer): ...@@ -207,32 +205,35 @@ class CourseEnrollmentDailySerializer(BaseCourseEnrollmentModelSerializer):
class CourseEnrollmentModeDailySerializer(BaseCourseEnrollmentModelSerializer): class CourseEnrollmentModeDailySerializer(BaseCourseEnrollmentModelSerializer):
""" Representation of course enrollment, broken down by mode, for a single day and course. """ """ Representation of course enrollment, broken down by mode, for a single day and course. """
audit = serializers.SerializerMethodField()
credit = serializers.SerializerMethodField()
honor = serializers.SerializerMethodField()
professional = serializers.SerializerMethodField()
verified = serializers.SerializerMethodField()
def get_default_fields(self): def get_audit(self, obj):
# pylint: disable=super-on-old-class return obj.get('audit', 0)
fields = super(CourseEnrollmentModeDailySerializer, self).get_default_fields()
# Create a field for each enrollment mode def get_honor(self, obj):
for mode in ENROLLMENT_MODES: return obj.get('honor', 0)
fields[mode] = serializers.IntegerField(required=True, default=0)
# Create a transform method for each field def get_credit(self, obj):
setattr(self, 'transform_%s' % mode, self._transform_mode) return obj.get('credit', 0)
fields['cumulative_count'] = serializers.IntegerField(required=True, default=0) def get_professional(self, obj):
return obj.get('professional', 0)
return fields def get_verified(self, obj):
return obj.get('verified', 0)
def _transform_mode(self, _obj, value):
return self.default_if_none(value, 0)
class Meta(object): class Meta(object):
model = models.CourseEnrollmentDaily model = models.CourseEnrollmentModeDaily
# Declare the dynamically-created fields here as well so that they will be picked up by Swagger. # Declare the dynamically-created fields here as well so that they will be picked up by Swagger.
fields = ['course_id', 'date', 'count', 'cumulative_count', 'created'] + ENROLLMENT_MODES fields = ['course_id', 'date', 'count', 'cumulative_count', 'created'] + ENROLLMENT_MODES
# pylint: disable=abstract-method
class CountrySerializer(serializers.Serializer): class CountrySerializer(serializers.Serializer):
""" """
Serialize country to an object with fields for the complete country name Serialize country to an object with fields for the complete country name
...@@ -256,21 +257,23 @@ class CourseEnrollmentByCountrySerializer(BaseCourseEnrollmentModelSerializer): ...@@ -256,21 +257,23 @@ class CourseEnrollmentByCountrySerializer(BaseCourseEnrollmentModelSerializer):
class CourseEnrollmentByGenderSerializer(BaseCourseEnrollmentModelSerializer): class CourseEnrollmentByGenderSerializer(BaseCourseEnrollmentModelSerializer):
def get_default_fields(self):
# pylint: disable=super-on-old-class
fields = super(CourseEnrollmentByGenderSerializer, self).get_default_fields()
# Create a field for each gender female = serializers.ReadOnlyField()
for gender in genders.ALL: male = serializers.ReadOnlyField()
fields[gender] = serializers.IntegerField(required=True, default=0) other = serializers.ReadOnlyField()
unknown = serializers.ReadOnlyField()
def get_female(self, obj):
return obj.get('female', None)
# Create a transform method for each field def get_male(self, obj):
setattr(self, 'transform_%s' % gender, self._transform_gender) return obj.get('male', None)
return fields def get_other(self, obj):
return obj.get('other', None)
def _transform_gender(self, _obj, value): def get_unknown(self, obj):
return self.default_if_none(value, 0) return obj.get('unknown', None)
class Meta(object): class Meta(object):
model = models.CourseEnrollmentByGender model = models.CourseEnrollmentByGender
...@@ -329,28 +332,37 @@ class VideoTimelineSerializer(ModelSerializerWithCreatedField): ...@@ -329,28 +332,37 @@ class VideoTimelineSerializer(ModelSerializerWithCreatedField):
) )
# pylint: disable=abstract-method
class LastUpdatedSerializer(serializers.Serializer): class LastUpdatedSerializer(serializers.Serializer):
last_updated = serializers.DateField(source='date', format=settings.DATE_FORMAT) last_updated = serializers.DateTimeField(source='date', format=settings.DATE_FORMAT)
class LearnerSerializer(serializers.Serializer, DefaultIfNoneMixin): # pylint: disable=abstract-method
username = serializers.CharField(source='username') class LearnerSerializer(serializers.Serializer):
enrollment_mode = serializers.CharField(source='enrollment_mode') username = serializers.CharField()
name = serializers.CharField(source='name') enrollment_mode = serializers.CharField()
account_url = serializers.SerializerMethodField('get_account_url') name = serializers.CharField()
email = serializers.CharField(source='email') account_url = serializers.SerializerMethodField()
segments = serializers.Field(source='segments') email = serializers.CharField()
engagements = serializers.SerializerMethodField('get_engagements') segments = serializers.SerializerMethodField()
enrollment_date = serializers.DateField(source='enrollment_date', format=settings.DATE_FORMAT) engagements = serializers.SerializerMethodField()
cohort = serializers.CharField(source='cohort') enrollment_date = serializers.DateTimeField(format=settings.DATE_FORMAT)
cohort = serializers.SerializerMethodField()
def transform_segments(self, _obj, value):
# returns null instead of empty strings def get_segments(self, obj):
return value or [] # using hasattr() instead because DocType.get() is overloaded and makes a request
if hasattr(obj, 'segments'):
# json parsing will fail unless in unicode
return [unicode(segment) for segment in obj.segments]
else:
return []
def transform_cohort(self, _obj, value): def get_cohort(self, obj):
# returns null instead of empty strings # using hasattr() instead because DocType.get() is overloaded and makes a request
return value or None if hasattr(obj, 'cohort') and len(obj.cohort) > 0:
return obj.cohort
else:
return None
def get_account_url(self, obj): def get_account_url(self, obj):
if settings.LMS_USER_ACCOUNT_BASE_URL: if settings.LMS_USER_ACCOUNT_BASE_URL:
...@@ -358,6 +370,9 @@ class LearnerSerializer(serializers.Serializer, DefaultIfNoneMixin): ...@@ -358,6 +370,9 @@ class LearnerSerializer(serializers.Serializer, DefaultIfNoneMixin):
else: else:
return None return None
def default_if_none(self, value, default=0):
return value if value is not None else default
def get_engagements(self, obj): def get_engagements(self, obj):
""" """
Add the engagement totals. Add the engagement totals.
...@@ -376,52 +391,55 @@ class LearnerSerializer(serializers.Serializer, DefaultIfNoneMixin): ...@@ -376,52 +391,55 @@ class LearnerSerializer(serializers.Serializer, DefaultIfNoneMixin):
return engagements return engagements
class EdxPaginationSerializer(pagination.PaginationSerializer): class EdxPaginationSerializer(pagination.PageNumberPagination):
""" """
Adds values to the response according to edX REST API Conventions. Adds values to the response according to edX REST API Conventions.
""" """
count = serializers.Field(source='paginator.count') page_size_query_param = 'page_size'
num_pages = serializers.Field(source='paginator.num_pages') page_size = learner.LEARNER_API_DEFAULT_LIST_PAGE_SIZE
max_page_size = 100 # TODO -- tweak during load testing
class ElasticsearchDSLSearchSerializer(EdxPaginationSerializer): def get_paginated_response(self, data):
def __init__(self, *args, **kwargs): # The output is more readable with num_pages included not at the end, but
"""Make sure that the elasticsearch query is executed.""" # inefficient to insert into an OrderedDict, so the response is copied from
# Because the elasticsearch-dsl search object has a different # rest_framework.pagination with the addition of "num_pages".
# API from the queryset object that's expected by the django return Response(OrderedDict([
# Paginator object, we have to manually execute the query. ('count', self.page.paginator.count),
# Note that the `kwargs['instance']` is the Page object, and ('num_pages', self.page.paginator.num_pages),
# `kwargs['instance'].object_list` is actually an ('next', self.get_next_link()),
# elasticsearch-dsl search object. ('previous', self.get_previous_link()),
kwargs['instance'].object_list = kwargs['instance'].object_list.execute() ('results', data)
super(ElasticsearchDSLSearchSerializer, self).__init__(*args, **kwargs) ]))
class EngagementDaySerializer(DefaultIfNoneMixin, serializers.Serializer): # pylint: disable=abstract-method
class EngagementDaySerializer(serializers.Serializer):
date = serializers.DateField(format=settings.DATE_FORMAT) date = serializers.DateField(format=settings.DATE_FORMAT)
problems_attempted = serializers.IntegerField(required=True, default=0) problems_attempted = serializers.SerializerMethodField()
problems_completed = serializers.IntegerField(required=True, default=0) problems_completed = serializers.SerializerMethodField()
discussion_contributions = serializers.IntegerField(required=True, default=0) discussion_contributions = serializers.SerializerMethodField()
videos_viewed = serializers.IntegerField(required=True, default=0) videos_viewed = serializers.SerializerMethodField()
def transform_problems_attempted(self, _obj, value): def get_problems_attempted(self, obj):
return self.default_if_none(value, 0) return obj.get('problems_attempted', 0)
def transform_problems_completed(self, _obj, value): def get_problems_completed(self, obj):
return self.default_if_none(value, 0) return obj.get('problems_completed', 0)
def transform_discussion_contributions(self, _obj, value): def get_discussion_contributions(self, obj):
return self.default_if_none(value, 0) return obj.get('discussion_contributions', 0)
def transform_videos_viewed(self, _obj, value): def get_videos_viewed(self, obj):
return self.default_if_none(value, 0) return obj.get('videos_viewed', 0)
# pylint: disable=abstract-method
class DateRangeSerializer(serializers.Serializer): class DateRangeSerializer(serializers.Serializer):
start = serializers.DateTimeField(source='start_date', format=settings.DATE_FORMAT) start = serializers.DateTimeField(source='start_date', format=settings.DATE_FORMAT)
end = serializers.DateTimeField(source='end_date', format=settings.DATE_FORMAT) end = serializers.DateTimeField(source='end_date', format=settings.DATE_FORMAT)
# pylint: disable=abstract-method
class EnagementRangeMetricSerializer(serializers.Serializer): class EnagementRangeMetricSerializer(serializers.Serializer):
""" """
Serializes ModuleEngagementMetricRanges ('bottom', 'average', and 'top') into Serializes ModuleEngagementMetricRanges ('bottom', 'average', and 'top') into
...@@ -429,9 +447,9 @@ class EnagementRangeMetricSerializer(serializers.Serializer): ...@@ -429,9 +447,9 @@ class EnagementRangeMetricSerializer(serializers.Serializer):
represented as arrays. If any one of the ranges is not defined, it is not represented as arrays. If any one of the ranges is not defined, it is not
included in the serialized output. included in the serialized output.
""" """
class_rank_bottom = serializers.SerializerMethodField('get_class_rank_bottom') class_rank_bottom = serializers.SerializerMethodField()
class_rank_average = serializers.SerializerMethodField('get_class_rank_average') class_rank_average = serializers.SerializerMethodField()
class_rank_top = serializers.SerializerMethodField('get_class_rank_top') class_rank_top = serializers.SerializerMethodField()
def get_class_rank_average(self, obj): def get_class_rank_average(self, obj):
return self._transform_range(obj['average']) return self._transform_range(obj['average'])
...@@ -446,11 +464,12 @@ class EnagementRangeMetricSerializer(serializers.Serializer): ...@@ -446,11 +464,12 @@ class EnagementRangeMetricSerializer(serializers.Serializer):
return [metric_range.low_value, metric_range.high_value] if metric_range else None return [metric_range.low_value, metric_range.high_value] if metric_range else None
# pylint: disable=abstract-method
class CourseLearnerMetadataSerializer(serializers.Serializer): class CourseLearnerMetadataSerializer(serializers.Serializer):
enrollment_modes = serializers.Field(source='es_data.enrollment_modes') enrollment_modes = serializers.ReadOnlyField(source='es_data.enrollment_modes')
segments = serializers.Field(source='es_data.segments') segments = serializers.ReadOnlyField(source='es_data.segments')
cohorts = serializers.Field(source='es_data.cohorts') cohorts = serializers.ReadOnlyField(source='es_data.cohorts')
engagement_ranges = serializers.SerializerMethodField('get_engagement_ranges') engagement_ranges = serializers.SerializerMethodField()
def get_engagement_ranges(self, obj): def get_engagement_ranges(self, obj):
query_set = obj['engagement_ranges'] query_set = obj['engagement_ranges']
......
...@@ -210,10 +210,11 @@ class CourseActivityLastWeekTest(DemoCourseMixin, TestCaseWithAuthentication): ...@@ -210,10 +210,11 @@ class CourseActivityLastWeekTest(DemoCourseMixin, TestCaseWithAuthentication):
@staticmethod @staticmethod
def get_activity_record(**kwargs): def get_activity_record(**kwargs):
datetime_format = "%Y-%m-%dT%H:%M:%SZ"
default = { default = {
'course_id': DEMO_COURSE_ID, 'course_id': DEMO_COURSE_ID,
'interval_start': datetime.datetime(2014, 1, 1, 0, 0, tzinfo=pytz.utc), '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), 'interval_end': datetime.datetime(2014, 1, 8, 0, 0, tzinfo=pytz.utc).strftime(datetime_format),
'activity_type': 'any', 'activity_type': 'any',
'count': 300, 'count': 300,
} }
...@@ -339,6 +340,9 @@ class CourseEnrollmentByGenderViewTests(CourseEnrollmentViewTestCaseMixin, Defau ...@@ -339,6 +340,9 @@ class CourseEnrollmentByGenderViewTests(CourseEnrollmentViewTestCaseMixin, Defau
super(CourseEnrollmentByGenderViewTests, self).setUp() super(CourseEnrollmentByGenderViewTests, self).setUp()
self.generate_data() self.generate_data()
def tearDown(self):
self.destroy_data()
def serialize_enrollment(self, enrollment): def serialize_enrollment(self, enrollment):
return { return {
'created': enrollment.created.strftime(settings.DATETIME_FORMAT), 'created': enrollment.created.strftime(settings.DATETIME_FORMAT),
...@@ -606,8 +610,8 @@ class CourseProblemsListViewTests(DemoCourseMixin, TestCaseWithAuthentication): ...@@ -606,8 +610,8 @@ class CourseProblemsListViewTests(DemoCourseMixin, TestCaseWithAuthentication):
# Create multiple objects here to test the grouping. Add a model with a different module_id to break up the # Create multiple objects here to test the grouping. Add a model with a different module_id to break up the
# natural order and ensure the view properly sorts the objects before grouping. # natural order and ensure the view properly sorts the objects before grouping.
module_id = 'i4x://test/problem/1' module_id = u'i4x://test/problem/1'
alt_module_id = 'i4x://test/problem/2' alt_module_id = u'i4x://test/problem/2'
created = datetime.datetime.utcnow() created = datetime.datetime.utcnow()
alt_created = created + datetime.timedelta(seconds=2) alt_created = created + datetime.timedelta(seconds=2)
date_time_format = '%Y-%m-%d %H:%M:%S' date_time_format = '%Y-%m-%d %H:%M:%S'
...@@ -624,21 +628,21 @@ class CourseProblemsListViewTests(DemoCourseMixin, TestCaseWithAuthentication): ...@@ -624,21 +628,21 @@ class CourseProblemsListViewTests(DemoCourseMixin, TestCaseWithAuthentication):
'module_id': module_id, 'module_id': module_id,
'total_submissions': 150, 'total_submissions': 150,
'correct_submissions': 50, 'correct_submissions': 50,
'part_ids': [o1.part_id, o3.part_id], 'part_ids': unicode([o1.part_id, o3.part_id]),
'created': alt_created.strftime(settings.DATETIME_FORMAT) 'created': alt_created.strftime(settings.DATETIME_FORMAT)
}, },
{ {
'module_id': alt_module_id, 'module_id': alt_module_id,
'total_submissions': 100, 'total_submissions': 100,
'correct_submissions': 100, 'correct_submissions': 100,
'part_ids': [o2.part_id], 'part_ids': unicode([o2.part_id]),
'created': created.strftime(settings.DATETIME_FORMAT) 'created': unicode(created.strftime(settings.DATETIME_FORMAT))
} }
] ]
response = self._get_data(self.course_id) response = self._get_data(self.course_id)
self.assertEquals(response.status_code, 200) self.assertEquals(response.status_code, 200)
self.assertListEqual(response.data, expected) self.assertListEqual([dict(d) for d in response.data], expected)
def test_get_404(self): def test_get_404(self):
""" """
...@@ -669,8 +673,8 @@ class CourseProblemsAndTagsListViewTests(DemoCourseMixin, TestCaseWithAuthentica ...@@ -669,8 +673,8 @@ class CourseProblemsAndTagsListViewTests(DemoCourseMixin, TestCaseWithAuthentica
# Create multiple objects here to test the grouping. Add a model with a different module_id to break up the # Create multiple objects here to test the grouping. Add a model with a different module_id to break up the
# natural order and ensure the view properly sorts the objects before grouping. # natural order and ensure the view properly sorts the objects before grouping.
module_id = 'i4x://test/problem/1' module_id = u'i4x://test/problem/1'
alt_module_id = 'i4x://test/problem/2' alt_module_id = u'i4x://test/problem/2'
tags = { tags = {
'difficulty': ['Easy', 'Medium', 'Hard'], 'difficulty': ['Easy', 'Medium', 'Hard'],
...@@ -695,26 +699,26 @@ class CourseProblemsAndTagsListViewTests(DemoCourseMixin, TestCaseWithAuthentica ...@@ -695,26 +699,26 @@ class CourseProblemsAndTagsListViewTests(DemoCourseMixin, TestCaseWithAuthentica
'module_id': module_id, 'module_id': module_id,
'total_submissions': 11, 'total_submissions': 11,
'correct_submissions': 4, 'correct_submissions': 4,
'tags': { 'tags': unicode({
'difficulty': 'Easy', u'difficulty': u'Easy',
'learning_outcome': 'Learned a few things', u'learning_outcome': u'Learned a few things',
}, }),
'created': alt_created.strftime(settings.DATETIME_FORMAT) 'created': alt_created.strftime(settings.DATETIME_FORMAT)
}, },
{ {
'module_id': alt_module_id, 'module_id': alt_module_id,
'total_submissions': 4, 'total_submissions': 4,
'correct_submissions': 0, 'correct_submissions': 0,
'tags': { 'tags': unicode({
'learning_outcome': 'Learned everything', u'learning_outcome': u'Learned everything',
}, }),
'created': created.strftime(settings.DATETIME_FORMAT) 'created': created.strftime(settings.DATETIME_FORMAT)
} }
] ]
response = self._get_data(self.course_id) response = self._get_data(self.course_id)
self.assertEquals(response.status_code, 200) self.assertEquals(response.status_code, 200)
self.assertListEqual(sorted(response.data), sorted(expected)) self.assertListEqual(sorted([dict(d) for d in response.data]), sorted(expected))
def test_get_404(self): def test_get_404(self):
""" """
......
...@@ -205,8 +205,7 @@ class LearnerListTests(LearnerAPITestMixin, VerifyCourseIdMixin, TestCaseWithAut ...@@ -205,8 +205,7 @@ class LearnerListTests(LearnerAPITestMixin, VerifyCourseIdMixin, TestCaseWithAut
returned. returned.
""" """
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
payload = json.loads(response.content) returned_learners = json.loads(response.content)['results']
returned_learners = payload['results']
if expected_learners is None: if expected_learners is None:
self.assertEqual(returned_learners, list()) self.assertEqual(returned_learners, list())
else: else:
......
...@@ -12,7 +12,7 @@ class CourseViewMixin(object): ...@@ -12,7 +12,7 @@ class CourseViewMixin(object):
course_id = None course_id = None
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
self.course_id = self.kwargs.get('course_id', request.QUERY_PARAMS.get('course_id', None)) self.course_id = self.kwargs.get('course_id', request.query_params.get('course_id', None))
if not self.course_id: if not self.course_id:
raise CourseNotSpecifiedError() raise CourseNotSpecifiedError()
......
...@@ -15,6 +15,8 @@ from analytics_data_api.constants import enrollment_modes ...@@ -15,6 +15,8 @@ from analytics_data_api.constants import enrollment_modes
from analytics_data_api.utils import dictfetchall from analytics_data_api.utils import dictfetchall
from analytics_data_api.v0 import models, serializers from analytics_data_api.v0 import models, serializers
from analytics_data_api.v0.views.utils import raise_404_if_none
class BaseCourseView(generics.ListAPIView): class BaseCourseView(generics.ListAPIView):
start_date = None start_date = None
...@@ -25,8 +27,8 @@ class BaseCourseView(generics.ListAPIView): ...@@ -25,8 +27,8 @@ class BaseCourseView(generics.ListAPIView):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
self.course_id = self.kwargs.get('course_id') self.course_id = self.kwargs.get('course_id')
start_date = request.QUERY_PARAMS.get('start_date') start_date = request.query_params.get('start_date')
end_date = request.QUERY_PARAMS.get('end_date') end_date = request.query_params.get('end_date')
timezone = utc timezone = utc
self.start_date = self.parse_date(start_date, timezone) self.start_date = self.parse_date(start_date, timezone)
...@@ -46,6 +48,7 @@ class BaseCourseView(generics.ListAPIView): ...@@ -46,6 +48,7 @@ class BaseCourseView(generics.ListAPIView):
def apply_date_filtering(self, queryset): def apply_date_filtering(self, queryset):
raise NotImplementedError raise NotImplementedError
@raise_404_if_none
def get_queryset(self): def get_queryset(self):
queryset = self.model.objects.filter(course_id=self.course_id) queryset = self.model.objects.filter(course_id=self.course_id)
queryset = self.apply_date_filtering(queryset) queryset = self.apply_date_filtering(queryset)
...@@ -232,14 +235,14 @@ class CourseActivityMostRecentWeekView(generics.RetrieveAPIView): ...@@ -232,14 +235,14 @@ class CourseActivityMostRecentWeekView(generics.RetrieveAPIView):
""" Retrieve the activity type from the query string. """ """ Retrieve the activity type from the query string. """
# Support the old label param # Support the old label param
activity_type = self.request.QUERY_PARAMS.get('label', None) activity_type = self.request.query_params.get('label', None)
activity_type = activity_type or self.request.QUERY_PARAMS.get('activity_type', self.DEFAULT_ACTIVITY_TYPE) activity_type = activity_type or self.request.query_params.get('activity_type', self.DEFAULT_ACTIVITY_TYPE)
activity_type = self._format_activity_type(activity_type) activity_type = self._format_activity_type(activity_type)
return activity_type return activity_type
def get_object(self, queryset=None): def get_object(self):
"""Select the activity report for the given course and activity type.""" """Select the activity report for the given course and activity type."""
warnings.warn('CourseActivityMostRecentWeekView has been deprecated! Use CourseActivityWeeklyView instead.', warnings.warn('CourseActivityMostRecentWeekView has been deprecated! Use CourseActivityWeeklyView instead.',
...@@ -400,7 +403,11 @@ class CourseEnrollmentByGenderView(BaseCourseEnrollmentView): ...@@ -400,7 +403,11 @@ class CourseEnrollmentByGenderView(BaseCourseEnrollmentView):
item = { item = {
u'course_id': key[0], u'course_id': key[0],
u'date': key[1], u'date': key[1],
u'created': None u'created': None,
u'male': 0,
u'female': 0,
u'other': 0,
u'unknown': 0
} }
for enrollment in group: for enrollment in group:
...@@ -633,6 +640,7 @@ class ProblemsListView(BaseCourseView): ...@@ -633,6 +640,7 @@ class ProblemsListView(BaseCourseView):
serializer_class = serializers.ProblemSerializer serializer_class = serializers.ProblemSerializer
allow_empty = False allow_empty = False
@raise_404_if_none
def get_queryset(self): def get_queryset(self):
# last_response_count is the number of submissions for the problem part and must # last_response_count is the number of submissions for the problem part and must
# be divided by the number of problem parts to get the problem submission rather # be divided by the number of problem parts to get the problem submission rather
...@@ -709,6 +717,7 @@ class ProblemsAndTagsListView(BaseCourseView): ...@@ -709,6 +717,7 @@ class ProblemsAndTagsListView(BaseCourseView):
allow_empty = False allow_empty = False
model = models.ProblemsAndTags model = models.ProblemsAndTags
@raise_404_if_none
def get_queryset(self): def get_queryset(self):
queryset = self.model.objects.filter(course_id=self.course_id) queryset = self.model.objects.filter(course_id=self.course_id)
items = queryset.all() items = queryset.all()
......
...@@ -5,9 +5,6 @@ import logging ...@@ -5,9 +5,6 @@ import logging
from rest_framework import generics, status from rest_framework import generics, status
from analytics_data_api.constants import (
learner
)
from analytics_data_api.v0.exceptions import ( from analytics_data_api.v0.exceptions import (
LearnerEngagementTimelineNotFoundError, LearnerEngagementTimelineNotFoundError,
LearnerNotFoundError, LearnerNotFoundError,
...@@ -21,7 +18,7 @@ from analytics_data_api.v0.models import ( ...@@ -21,7 +18,7 @@ from analytics_data_api.v0.models import (
) )
from analytics_data_api.v0.serializers import ( from analytics_data_api.v0.serializers import (
CourseLearnerMetadataSerializer, CourseLearnerMetadataSerializer,
ElasticsearchDSLSearchSerializer, EdxPaginationSerializer,
EngagementDaySerializer, EngagementDaySerializer,
LastUpdatedSerializer, LastUpdatedSerializer,
LearnerSerializer, LearnerSerializer,
...@@ -106,7 +103,7 @@ class LearnerView(LastUpdateMixin, CourseViewMixin, generics.RetrieveAPIView): ...@@ -106,7 +103,7 @@ class LearnerView(LastUpdateMixin, CourseViewMixin, generics.RetrieveAPIView):
def get_queryset(self): def get_queryset(self):
return RosterEntry.get_course_user(self.course_id, self.username) return RosterEntry.get_course_user(self.course_id, self.username)
def get_object(self, queryset=None): def get_object(self):
queryset = self.get_queryset() queryset = self.get_queryset()
if len(queryset) == 1: if len(queryset) == 1:
return queryset[0] return queryset[0]
...@@ -187,14 +184,12 @@ class LearnerListView(LastUpdateMixin, CourseViewMixin, generics.ListAPIView): ...@@ -187,14 +184,12 @@ class LearnerListView(LastUpdateMixin, CourseViewMixin, generics.ListAPIView):
""" """
serializer_class = LearnerSerializer serializer_class = LearnerSerializer
pagination_serializer_class = ElasticsearchDSLSearchSerializer pagination_class = EdxPaginationSerializer
paginate_by_param = 'page_size'
paginate_by = learner.LEARNER_API_DEFAULT_LIST_PAGE_SIZE
max_paginate_by = 100 # TODO -- tweak during load testing max_paginate_by = 100 # TODO -- tweak during load testing
def _validate_query_params(self): def _validate_query_params(self):
"""Validates various querystring parameters.""" """Validates various querystring parameters."""
query_params = self.request.QUERY_PARAMS query_params = self.request.query_params
page = query_params.get('page') page = query_params.get('page')
if page: if page:
try: try:
...@@ -222,8 +217,9 @@ class LearnerListView(LastUpdateMixin, CourseViewMixin, generics.ListAPIView): ...@@ -222,8 +217,9 @@ class LearnerListView(LastUpdateMixin, CourseViewMixin, generics.ListAPIView):
""" """
response = super(LearnerListView, self).list(request, args, kwargs) response = super(LearnerListView, self).list(request, args, kwargs)
last_updated = self.get_last_updated() last_updated = self.get_last_updated()
for result in response.data['results']: if response.data['results'] is not None:
result.update(last_updated) for result in response.data['results']:
result.update(last_updated)
return response return response
def get_queryset(self): def get_queryset(self):
...@@ -232,7 +228,7 @@ class LearnerListView(LastUpdateMixin, CourseViewMixin, generics.ListAPIView): ...@@ -232,7 +228,7 @@ class LearnerListView(LastUpdateMixin, CourseViewMixin, generics.ListAPIView):
as a an array of dicts with fields "learner" and "last_updated". as a an array of dicts with fields "learner" and "last_updated".
""" """
self._validate_query_params() self._validate_query_params()
query_params = self.request.QUERY_PARAMS query_params = self.request.query_params
order_by = query_params.get('order_by') order_by = query_params.get('order_by')
sort_order = query_params.get('sort_order') sort_order = query_params.get('sort_order')
...@@ -366,7 +362,7 @@ class CourseLearnerMetadata(CourseViewMixin, generics.RetrieveAPIView): ...@@ -366,7 +362,7 @@ class CourseLearnerMetadata(CourseViewMixin, generics.RetrieveAPIView):
""" """
serializer_class = CourseLearnerMetadataSerializer serializer_class = CourseLearnerMetadataSerializer
def get_object(self, queryset=None): def get_object(self):
# Because we're serializing data from both Elasticsearch and MySQL into # Because we're serializing data from both Elasticsearch and MySQL into
# the same JSON object, we have to pass both sources of data in a dict # the same JSON object, we have to pass both sources of data in a dict
# to our custom course metadata serializer. # to our custom course metadata serializer.
......
...@@ -22,6 +22,8 @@ from analytics_data_api.v0.serializers import ( ...@@ -22,6 +22,8 @@ from analytics_data_api.v0.serializers import (
) )
from analytics_data_api.utils import matching_tuple from analytics_data_api.utils import matching_tuple
from analytics_data_api.v0.views.utils import raise_404_if_none
class ProblemResponseAnswerDistributionView(generics.ListAPIView): class ProblemResponseAnswerDistributionView(generics.ListAPIView):
""" """
...@@ -98,6 +100,7 @@ class ProblemResponseAnswerDistributionView(generics.ListAPIView): ...@@ -98,6 +100,7 @@ class ProblemResponseAnswerDistributionView(generics.ListAPIView):
return consolidated_answers return consolidated_answers
@raise_404_if_none
def get_queryset(self): def get_queryset(self):
"""Select all the answer distribution response having to do with this usage of the problem.""" """Select all the answer distribution response having to do with this usage of the problem."""
problem_id = self.kwargs.get('problem_id') problem_id = self.kwargs.get('problem_id')
...@@ -142,6 +145,7 @@ class GradeDistributionView(generics.ListAPIView): ...@@ -142,6 +145,7 @@ class GradeDistributionView(generics.ListAPIView):
serializer_class = GradeDistributionSerializer serializer_class = GradeDistributionSerializer
allow_empty = False allow_empty = False
@raise_404_if_none
def get_queryset(self): def get_queryset(self):
"""Select all grade distributions for a particular module""" """Select all grade distributions for a particular module"""
problem_id = self.kwargs.get('problem_id') problem_id = self.kwargs.get('problem_id')
...@@ -170,6 +174,7 @@ class SequentialOpenDistributionView(generics.ListAPIView): ...@@ -170,6 +174,7 @@ class SequentialOpenDistributionView(generics.ListAPIView):
serializer_class = SequentialOpenDistributionSerializer serializer_class = SequentialOpenDistributionSerializer
allow_empty = False allow_empty = False
@raise_404_if_none
def get_queryset(self): def get_queryset(self):
"""Select the view count for a specific module""" """Select the view count for a specific module"""
module_id = self.kwargs.get('module_id') module_id = self.kwargs.get('module_id')
......
"""Utilities for view-level API logic.""" """Utilities for view-level API logic."""
from django.http import Http404
def split_query_argument(argument): def split_query_argument(argument):
...@@ -10,3 +11,16 @@ def split_query_argument(argument): ...@@ -10,3 +11,16 @@ def split_query_argument(argument):
return argument.split(',') return argument.split(',')
else: else:
return None return None
def raise_404_if_none(func):
"""
Decorator for raiseing Http404 if function evaulation is falsey (e.g. empty queryset).
"""
def func_wrapper(self):
queryset = func(self)
if queryset:
return queryset
else:
raise Http404
return func_wrapper
...@@ -7,6 +7,8 @@ from rest_framework import generics ...@@ -7,6 +7,8 @@ from rest_framework import generics
from analytics_data_api.v0.models import VideoTimeline from analytics_data_api.v0.models import VideoTimeline
from analytics_data_api.v0.serializers import VideoTimelineSerializer from analytics_data_api.v0.serializers import VideoTimelineSerializer
from analytics_data_api.v0.views.utils import raise_404_if_none
class VideoTimelineView(generics.ListAPIView): class VideoTimelineView(generics.ListAPIView):
""" """
...@@ -30,6 +32,7 @@ class VideoTimelineView(generics.ListAPIView): ...@@ -30,6 +32,7 @@ class VideoTimelineView(generics.ListAPIView):
serializer_class = VideoTimelineSerializer serializer_class = VideoTimelineSerializer
allow_empty = False allow_empty = False
@raise_404_if_none
def get_queryset(self): def get_queryset(self):
"""Select the view count for a specific module""" """Select the view count for a specific module"""
video_id = self.kwargs.get('video_id') video_id = self.kwargs.get('video_id')
......
boto==2.22.1 # MIT boto==2.22.1 # MIT
Django==1.8.14 # BSD License Django==1.8.14 # BSD License
django-model-utils==2.2 # BSD django-model-utils==2.2 # BSD
djangorestframework==2.4.4 # BSD djangorestframework==3.4.6 # BSD
django-rest-swagger==0.2.8 # BSD django-rest-swagger==0.2.8 # BSD
djangorestframework-csv==1.3.3 # BSD djangorestframework-csv==1.3.3 # BSD
django-countries==3.2 # MIT django-countries==3.2 # MIT
......
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