1+ from collections import OrderedDict
12from urlparse import urljoin
23from django .conf import settings
34from rest_framework import pagination , serializers
5+ from rest_framework .response import Response
46
57from analytics_data_api .constants import (
68 engagement_events ,
79 enrollment_modes ,
8- genders ,
10+ learner ,
911)
1012from analytics_data_api .v0 import models
1113
@@ -23,7 +25,7 @@ class CourseActivityByWeekSerializer(serializers.ModelSerializer):
2325 particular record is likely to change unexpectedly so we avoid exposing it.
2426 """
2527
26- activity_type = serializers .SerializerMethodField ('get_activity_type' )
28+ activity_type = serializers .SerializerMethodField ()
2729
2830 def get_activity_type (self , obj ):
2931 """
@@ -45,6 +47,7 @@ class ModelSerializerWithCreatedField(serializers.ModelSerializer):
4547 created = serializers .DateTimeField (format = settings .DATETIME_FORMAT )
4648
4749
50+ # pylint: disable=abstract-method
4851class ProblemSerializer (serializers .Serializer ):
4952 """
5053 Serializer for problems.
@@ -57,6 +60,7 @@ class ProblemSerializer(serializers.Serializer):
5760 created = serializers .DateTimeField (format = settings .DATETIME_FORMAT )
5861
5962
63+ # pylint: disable=abstract-method
6064class ProblemsAndTagsSerializer (serializers .Serializer ):
6165 """
6266 Serializer for problems and tags.
@@ -187,13 +191,7 @@ class Meta(object):
187191 )
188192
189193
190- class DefaultIfNoneMixin (object ):
191-
192- def default_if_none (self , value , default = 0 ):
193- return value if value is not None else default
194-
195-
196- class BaseCourseEnrollmentModelSerializer (DefaultIfNoneMixin , ModelSerializerWithCreatedField ):
194+ class BaseCourseEnrollmentModelSerializer (ModelSerializerWithCreatedField ):
197195 date = serializers .DateField (format = settings .DATE_FORMAT )
198196
199197
@@ -207,32 +205,35 @@ class Meta(object):
207205
208206class CourseEnrollmentModeDailySerializer (BaseCourseEnrollmentModelSerializer ):
209207 """ Representation of course enrollment, broken down by mode, for a single day and course. """
208+ audit = serializers .SerializerMethodField ()
209+ credit = serializers .SerializerMethodField ()
210+ honor = serializers .SerializerMethodField ()
211+ professional = serializers .SerializerMethodField ()
212+ verified = serializers .SerializerMethodField ()
210213
211- def get_default_fields (self ):
212- # pylint: disable=super-on-old-class
213- fields = super (CourseEnrollmentModeDailySerializer , self ).get_default_fields ()
214+ def get_audit (self , obj ):
215+ return obj .get ('audit' , 0 )
214216
215- # Create a field for each enrollment mode
216- for mode in ENROLLMENT_MODES :
217- fields [mode ] = serializers .IntegerField (required = True , default = 0 )
217+ def get_honor (self , obj ):
218+ return obj .get ('honor' , 0 )
218219
219- # Create a transform method for each field
220- setattr ( self , 'transform_%s' % mode , self . _transform_mode )
220+ def get_credit ( self , obj ):
221+ return obj . get ( 'credit' , 0 )
221222
222- fields ['cumulative_count' ] = serializers .IntegerField (required = True , default = 0 )
223+ def get_professional (self , obj ):
224+ return obj .get ('professional' , 0 )
223225
224- return fields
225-
226- def _transform_mode (self , _obj , value ):
227- return self .default_if_none (value , 0 )
226+ def get_verified (self , obj ):
227+ return obj .get ('verified' , 0 )
228228
229229 class Meta (object ):
230- model = models .CourseEnrollmentDaily
230+ model = models .CourseEnrollmentModeDaily
231231
232232 # Declare the dynamically-created fields here as well so that they will be picked up by Swagger.
233233 fields = ['course_id' , 'date' , 'count' , 'cumulative_count' , 'created' ] + ENROLLMENT_MODES
234234
235235
236+ # pylint: disable=abstract-method
236237class CountrySerializer (serializers .Serializer ):
237238 """
238239 Serialize country to an object with fields for the complete country name
@@ -256,21 +257,23 @@ class Meta(object):
256257
257258
258259class CourseEnrollmentByGenderSerializer (BaseCourseEnrollmentModelSerializer ):
259- def get_default_fields (self ):
260- # pylint: disable=super-on-old-class
261- fields = super (CourseEnrollmentByGenderSerializer , self ).get_default_fields ()
262260
263- # Create a field for each gender
264- for gender in genders .ALL :
265- fields [gender ] = serializers .IntegerField (required = True , default = 0 )
261+ female = serializers .ReadOnlyField ()
262+ male = serializers .ReadOnlyField ()
263+ other = serializers .ReadOnlyField ()
264+ unknown = serializers .ReadOnlyField ()
265+
266+ def get_female (self , obj ):
267+ return obj .get ('female' , None )
266268
267- # Create a transform method for each field
268- setattr ( self , 'transform_%s' % gender , self . _transform_gender )
269+ def get_male ( self , obj ):
270+ return obj . get ( 'male' , None )
269271
270- return fields
272+ def get_other (self , obj ):
273+ return obj .get ('other' , None )
271274
272- def _transform_gender (self , _obj , value ):
273- return self . default_if_none ( value , 0 )
275+ def get_unknown (self , obj ):
276+ return obj . get ( 'unknown' , None )
274277
275278 class Meta (object ):
276279 model = models .CourseEnrollmentByGender
@@ -329,35 +332,47 @@ class Meta(object):
329332 )
330333
331334
335+ # pylint: disable=abstract-method
332336class LastUpdatedSerializer (serializers .Serializer ):
333- last_updated = serializers .DateField (source = 'date' , format = settings .DATE_FORMAT )
337+ last_updated = serializers .DateTimeField (source = 'date' , format = settings .DATE_FORMAT )
334338
335339
336- class LearnerSerializer (serializers .Serializer , DefaultIfNoneMixin ):
337- username = serializers .CharField (source = 'username' )
338- enrollment_mode = serializers .CharField (source = 'enrollment_mode' )
339- name = serializers .CharField (source = 'name' )
340- account_url = serializers .SerializerMethodField ('get_account_url' )
341- email = serializers .CharField (source = 'email' )
342- segments = serializers .Field (source = 'segments' )
343- engagements = serializers .SerializerMethodField ('get_engagements' )
344- enrollment_date = serializers .DateField (source = 'enrollment_date' , format = settings .DATE_FORMAT )
345- cohort = serializers .CharField (source = 'cohort' )
346-
347- def transform_segments (self , _obj , value ):
348- # returns null instead of empty strings
349- return value or []
340+ # pylint: disable=abstract-method
341+ class LearnerSerializer (serializers .Serializer ):
342+ username = serializers .CharField ()
343+ enrollment_mode = serializers .CharField ()
344+ name = serializers .CharField ()
345+ account_url = serializers .SerializerMethodField ()
346+ email = serializers .CharField ()
347+ segments = serializers .SerializerMethodField ()
348+ engagements = serializers .SerializerMethodField ()
349+ enrollment_date = serializers .DateTimeField (format = settings .DATE_FORMAT )
350+ cohort = serializers .SerializerMethodField ()
351+
352+ def get_segments (self , obj ):
353+ # using hasattr() instead because DocType.get() is overloaded and makes a request
354+ if hasattr (obj , 'segments' ):
355+ # json parsing will fail unless in unicode
356+ return [unicode (segment ) for segment in obj .segments ]
357+ else :
358+ return []
350359
351- def transform_cohort (self , _obj , value ):
352- # returns null instead of empty strings
353- return value or None
360+ def get_cohort (self , obj ):
361+ # using hasattr() instead because DocType.get() is overloaded and makes a request
362+ if hasattr (obj , 'cohort' ) and len (obj .cohort ) > 0 :
363+ return obj .cohort
364+ else :
365+ return None
354366
355367 def get_account_url (self , obj ):
356368 if settings .LMS_USER_ACCOUNT_BASE_URL :
357369 return urljoin (settings .LMS_USER_ACCOUNT_BASE_URL , obj .username )
358370 else :
359371 return None
360372
373+ def default_if_none (self , value , default = 0 ):
374+ return value if value is not None else default
375+
361376 def get_engagements (self , obj ):
362377 """
363378 Add the engagement totals.
@@ -376,62 +391,65 @@ def get_engagements(self, obj):
376391 return engagements
377392
378393
379- class EdxPaginationSerializer (pagination .PaginationSerializer ):
394+ class EdxPaginationSerializer (pagination .PageNumberPagination ):
380395 """
381396 Adds values to the response according to edX REST API Conventions.
382397 """
383- count = serializers .Field (source = 'paginator.count' )
384- num_pages = serializers .Field (source = 'paginator.num_pages' )
385-
386-
387- class ElasticsearchDSLSearchSerializer (EdxPaginationSerializer ):
388- def __init__ (self , * args , ** kwargs ):
389- """Make sure that the elasticsearch query is executed."""
390- # Because the elasticsearch-dsl search object has a different
391- # API from the queryset object that's expected by the django
392- # Paginator object, we have to manually execute the query.
393- # Note that the `kwargs['instance']` is the Page object, and
394- # `kwargs['instance'].object_list` is actually an
395- # elasticsearch-dsl search object.
396- kwargs ['instance' ].object_list = kwargs ['instance' ].object_list .execute ()
397- super (ElasticsearchDSLSearchSerializer , self ).__init__ (* args , ** kwargs )
398-
399-
400- class EngagementDaySerializer (DefaultIfNoneMixin , serializers .Serializer ):
398+ page_size_query_param = 'page_size'
399+ page_size = learner .LEARNER_API_DEFAULT_LIST_PAGE_SIZE
400+ max_page_size = 100 # TODO -- tweak during load testing
401+
402+ def get_paginated_response (self , data ):
403+ # The output is more readable with num_pages included not at the end, but
404+ # inefficient to insert into an OrderedDict, so the response is copied from
405+ # rest_framework.pagination with the addition of "num_pages".
406+ return Response (OrderedDict ([
407+ ('count' , self .page .paginator .count ),
408+ ('num_pages' , self .page .paginator .num_pages ),
409+ ('next' , self .get_next_link ()),
410+ ('previous' , self .get_previous_link ()),
411+ ('results' , data )
412+ ]))
413+
414+
415+ # pylint: disable=abstract-method
416+ class EngagementDaySerializer (serializers .Serializer ):
401417 date = serializers .DateField (format = settings .DATE_FORMAT )
402- problems_attempted = serializers .IntegerField ( required = True , default = 0 )
403- problems_completed = serializers .IntegerField ( required = True , default = 0 )
404- discussion_contributions = serializers .IntegerField ( required = True , default = 0 )
405- videos_viewed = serializers .IntegerField ( required = True , default = 0 )
418+ problems_attempted = serializers .SerializerMethodField ( )
419+ problems_completed = serializers .SerializerMethodField ( )
420+ discussion_contributions = serializers .SerializerMethodField ( )
421+ videos_viewed = serializers .SerializerMethodField ( )
406422
407- def transform_problems_attempted (self , _obj , value ):
408- return self . default_if_none ( value , 0 )
423+ def get_problems_attempted (self , obj ):
424+ return obj . get ( 'problems_attempted' , 0 )
409425
410- def transform_problems_completed (self , _obj , value ):
411- return self . default_if_none ( value , 0 )
426+ def get_problems_completed (self , obj ):
427+ return obj . get ( 'problems_completed' , 0 )
412428
413- def transform_discussion_contributions (self , _obj , value ):
414- return self . default_if_none ( value , 0 )
429+ def get_discussion_contributions (self , obj ):
430+ return obj . get ( 'discussion_contributions' , 0 )
415431
416- def transform_videos_viewed (self , _obj , value ):
417- return self . default_if_none ( value , 0 )
432+ def get_videos_viewed (self , obj ):
433+ return obj . get ( 'videos_viewed' , 0 )
418434
419435
436+ # pylint: disable=abstract-method
420437class DateRangeSerializer (serializers .Serializer ):
421438 start = serializers .DateTimeField (source = 'start_date' , format = settings .DATE_FORMAT )
422439 end = serializers .DateTimeField (source = 'end_date' , format = settings .DATE_FORMAT )
423440
424441
442+ # pylint: disable=abstract-method
425443class EnagementRangeMetricSerializer (serializers .Serializer ):
426444 """
427445 Serializes ModuleEngagementMetricRanges ('bottom', 'average', and 'top') into
428446 the class_rank_bottom, class_rank_average, and class_rank_top ranges
429447 represented as arrays. If any one of the ranges is not defined, it is not
430448 included in the serialized output.
431449 """
432- class_rank_bottom = serializers .SerializerMethodField ('get_class_rank_bottom' )
433- class_rank_average = serializers .SerializerMethodField ('get_class_rank_average' )
434- class_rank_top = serializers .SerializerMethodField ('get_class_rank_top' )
450+ class_rank_bottom = serializers .SerializerMethodField ()
451+ class_rank_average = serializers .SerializerMethodField ()
452+ class_rank_top = serializers .SerializerMethodField ()
435453
436454 def get_class_rank_average (self , obj ):
437455 return self ._transform_range (obj ['average' ])
@@ -446,11 +464,12 @@ def _transform_range(self, metric_range):
446464 return [metric_range .low_value , metric_range .high_value ] if metric_range else None
447465
448466
467+ # pylint: disable=abstract-method
449468class CourseLearnerMetadataSerializer (serializers .Serializer ):
450- enrollment_modes = serializers .Field (source = 'es_data.enrollment_modes' )
451- segments = serializers .Field (source = 'es_data.segments' )
452- cohorts = serializers .Field (source = 'es_data.cohorts' )
453- engagement_ranges = serializers .SerializerMethodField ('get_engagement_ranges' )
469+ enrollment_modes = serializers .ReadOnlyField (source = 'es_data.enrollment_modes' )
470+ segments = serializers .ReadOnlyField (source = 'es_data.segments' )
471+ cohorts = serializers .ReadOnlyField (source = 'es_data.cohorts' )
472+ engagement_ranges = serializers .SerializerMethodField ()
454473
455474 def get_engagement_ranges (self , obj ):
456475 query_set = obj ['engagement_ranges' ]
0 commit comments