Commit e9c0c069 by Dennis Jen Committed by Daniel Friedman

Added fields for learner endpoints.

parent 9fb50c73
......@@ -3,7 +3,9 @@ from itertools import groupby
from django.conf import settings
from django.db import models
from django.db.models import Sum
from elasticsearch_dsl import DocType, Q
# some fields (e.g. Float, Integer) are dynamic and your IDE may highlight them as unavailable
from elasticsearch_dsl import Date, DocType, Float, Integer, Q, String
from analytics_data_api.constants import country, engagement_entity_types, genders, learner
......@@ -214,6 +216,27 @@ class Video(BaseVideo):
class RosterEntry(DocType):
course_id = String()
username = String()
name = String()
email = String()
enrollment_mode = String()
cohort = String()
segments = String(fields={'raw': String()})
problems_attempted = Integer()
problems_completed = Integer()
problem_attempts_per_completed = Float()
# Useful for ordering problem_attempts_per_completed (because results can include null, which is
# different from zero). attempt_ratio_order is equal to the number of problem attempts if
# problem_attempts_per_completed is > 1 and set to -problem_attempts if
# problem_attempts_per_completed = 1.
attempt_ratio_order = Integer()
discussions_contributed = Integer()
videos_watched = Integer()
enrollment_date = Date()
last_updated = Date()
# pylint: disable=old-style-class
class Meta:
index = settings.ELASTICSEARCH_LEARNERS_INDEX
......@@ -230,8 +253,7 @@ class RosterEntry(DocType):
course_id,
segments=None,
ignore_segments=None,
# TODO: enable during https://openedx.atlassian.net/browse/AN-6319
# cohort=None,
cohort=None,
enrollment_mode=None,
text_search=None,
order_by='username',
......@@ -251,13 +273,14 @@ class RosterEntry(DocType):
segment=segment, segments=', '.join(learner.SEGMENTS)
))
order_by_options = (
'username', 'email', 'discussions_contributed', 'problems_attempted', 'problems_completed', 'videos_viewed'
'username', 'email', 'discussions_contributed', 'problems_attempted', 'problems_completed',
'attempt_ratio_order', 'videos_viewed'
)
sort_order_options = ('asc', 'desc')
if order_by not in order_by_options:
raise ValueError("order_by value '{order_by}' must be one of: ({order_by_options})".format(
order_by=order_by, order_by_options=', '.join(order_by_options)
))
sort_order_options = ('asc', 'desc')
if sort_order not in sort_order_options:
raise ValueError("sort_order value '{sort_order}' must be one of: ({sort_order_options})".format(
sort_order=sort_order, sort_order_options=', '.join(sort_order_options)
......@@ -272,9 +295,8 @@ class RosterEntry(DocType):
elif ignore_segments:
for segment in ignore_segments:
search = search.query(~Q('term', segments=segment))
# TODO: enable during https://openedx.atlassian.net/browse/AN-6319
# if cohort:
# search = search.query('term', cohort=cohort)
if cohort:
search = search.query('term', cohort=cohort)
if enrollment_mode:
search = search.query('term', enrollment_mode=enrollment_mode)
if text_search:
......@@ -309,8 +331,7 @@ class RosterEntry(DocType):
search.query = Q('bool', must=[Q('term', course_id=course_id)])
search.aggs.bucket('enrollment_modes', 'terms', field='enrollment_mode')
search.aggs.bucket('segments', 'terms', field='segments')
# TODO: enable during https://openedx.atlassian.net/browse/AN-6319
# search.aggs.bucket('group_by_cohorts', 'terms', field='cohort')
search.aggs.bucket('cohorts', 'terms', field='cohort')
response = search.execute()
# Build up the map of aggregation name to count
aggregations = {
......
......@@ -317,7 +317,7 @@ class VideoTimelineSerializer(ModelSerializerWithCreatedField):
)
class LearnerSerializer(serializers.Serializer):
class LearnerSerializer(serializers.Serializer, DefaultIfNoneMixin):
username = serializers.CharField()
enrollment_mode = serializers.CharField()
name = serializers.CharField()
......@@ -325,11 +325,9 @@ class LearnerSerializer(serializers.Serializer):
email = serializers.CharField()
segments = serializers.Field(source='segments')
engagements = serializers.SerializerMethodField('get_engagements')
# TODO: add these back in when the index returns them
# enrollment_date = serializers.DateField(format=settings.DATE_FORMAT, allow_empty=True)
# last_updated = serializers.DateField(format=settings.DATE_FORMAT)
# cohort = serializers.CharField(allow_none=True)
enrollment_date = serializers.DateField(format=settings.DATE_FORMAT)
last_updated = serializers.DateField(format=settings.DATE_FORMAT)
cohort = serializers.CharField()
def get_account_url(self, obj):
if settings.LMS_USER_ACCOUNT_BASE_URL:
......@@ -342,10 +340,17 @@ class LearnerSerializer(serializers.Serializer):
Add the engagement totals.
"""
engagements = {}
for entity_type in engagement_entity_types.AGGREGATE_TYPES:
for event in engagement_events.EVENTS[entity_type]:
metric = '{0}_{1}'.format(entity_type, event)
engagements[metric] = getattr(obj, metric, 0)
# fill in these fields will 0 if values not returned/found
default_if_none_fields = ['discussions_contributed', 'problems_attempted',
'problems_completed', 'videos_viewed']
for field in default_if_none_fields:
engagements[field] = self.default_if_none(getattr(obj, field, None), 0)
# preserve null values for problem attempts per completed
no_default_field = 'problem_attempts_per_completed'
engagements[no_default_field] = getattr(obj, no_default_field, None)
return engagements
......@@ -424,8 +429,7 @@ class EnagementRangeMetricSerializer(serializers.Serializer):
class CourseLearnerMetadataSerializer(serializers.Serializer):
enrollment_modes = serializers.SerializerMethodField('get_enrollment_modes')
segments = serializers.SerializerMethodField('get_segments')
# TODO: enable during https://openedx.atlassian.net/browse/AN-6319
# cohorts = serializers.SerializerMethodField('get_cohorts')
cohorts = serializers.SerializerMethodField('get_cohorts')
engagement_ranges = serializers.SerializerMethodField('get_engagement_ranges')
def get_enrollment_modes(self, obj):
......@@ -434,9 +438,8 @@ class CourseLearnerMetadataSerializer(serializers.Serializer):
def get_segments(self, obj):
return obj['es_data']['segments']
# TODO: enable during https://openedx.atlassian.net/browse/AN-6319
# def get_cohorts(self, obj):
# return obj['es_data']['cohorts']
def get_cohorts(self, obj):
return obj['es_data']['cohorts']
def get_engagement_ranges(self, obj):
query_set = obj['engagement_ranges']
......
......@@ -174,14 +174,21 @@ class LearnerListView(CourseViewMixin, generics.ListAPIView):
"""
self._validate_query_params()
query_params = self.request.QUERY_PARAMS
# Ordering by problem_attempts_per_completed can be ambiguous because
# it's a ratio values could be infinite (e.g. divide by zero) if no problems
# were completed. Instead, sorting by attempt_ratio_order will produce
# a sensible ordering
order_by = query_params.get('order_by')
order_by = 'attempt_ratio_order' if order_by == 'problem_attempts_per_completed' else order_by
params = {
'segments': split_query_argument(query_params.get('segments')),
'ignore_segments': split_query_argument(query_params.get('ignore_segments')),
# TODO: enable during https://openedx.atlassian.net/browse/AN-6319
# 'cohort': query_params.get('cohort'),
'cohort': query_params.get('cohort'),
'enrollment_mode': query_params.get('enrollment_mode'),
'text_search': query_params.get('text_search'),
'order_by': query_params.get('order_by'),
'order_by': order_by,
'sort_order': query_params.get('sort_order')
}
# Remove None values from `params` so that we don't overwrite default
......@@ -211,6 +218,7 @@ class EngagementTimelineView(CourseViewMixin, generics.ListAPIView):
* problems_completed: Unique number of problems completed.
* discussions_contributed: Number of discussions participated in (e.g. forum posts)
* videos_viewed: Number of videos watched.
* problem_attempts_per_completed: TBD
**Parameters**
......
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