Skip to content

Commit

Permalink
Fill in missing dates for engagement timeline
Browse files Browse the repository at this point in the history
AN-6960
  • Loading branch information
dan-f committed Apr 8, 2016
1 parent 56081b9 commit b134e64
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 4 deletions.
9 changes: 9 additions & 0 deletions analytics_data_api/constants/engagement_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ class EngagementType(object):
- The internal question of whether the metric should be counted in terms
of the entity type or the raw number of events.
"""
# Defines the current canonical set of engagement types used in the Learner
# Analytics API.
ALL_TYPES = (
'problems_attempted',
'problems_completed',
'videos_viewed',
'discussion_contributions',
)

def __init__(self, entity_type, event_type):
"""
Initializes an EngagementType for a particular entity and event type.
Expand Down
31 changes: 30 additions & 1 deletion analytics_data_api/tests.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import datetime

from django.contrib.auth.models import User
from django.core.management import call_command, CommandError
from django.test import TestCase
Expand All @@ -6,7 +8,7 @@

from analytics_data_api.constants.country import get_country, UNKNOWN_COUNTRY

from analytics_data_api.utils import delete_user_auth_token, set_user_auth_token
from analytics_data_api.utils import date_range, delete_user_auth_token, set_user_auth_token


class UtilsTests(TestCase):
Expand Down Expand Up @@ -91,3 +93,30 @@ def test_get_country(self):
# Return unknown country if code is invalid
self.assertEqual(get_country('A1'), UNKNOWN_COUNTRY)
self.assertEqual(get_country(None), UNKNOWN_COUNTRY)


class DateRangeTests(TestCase):
def test_empty_range(self):
date = datetime.datetime(2016, 1, 1)
self.assertEqual([date for date in date_range(date, date)], [])

def test_range_exclusive(self):
start_date = datetime.datetime(2016, 1, 1)
end_date = datetime.datetime(2016, 1, 2)
self.assertEqual([date for date in date_range(start_date, end_date)], [start_date])

def test_delta_goes_past_end_date(self):
start_date = datetime.datetime(2016, 1, 1)
end_date = datetime.datetime(2016, 1, 3)
time_delta = datetime.timedelta(days=5)
self.assertEqual([date for date in date_range(start_date, end_date, time_delta)], [start_date])

def test_general_range(self):
start_date = datetime.datetime(2016, 1, 1)
end_date = datetime.datetime(2016, 1, 5)
self.assertEqual([date for date in date_range(start_date, end_date)], [
datetime.datetime(2016, 1, 1),
datetime.datetime(2016, 1, 2),
datetime.datetime(2016, 1, 3),
datetime.datetime(2016, 1, 4),
])
24 changes: 24 additions & 0 deletions analytics_data_api/utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import datetime
from importlib import import_module

from django.db.models import Q
Expand Down Expand Up @@ -60,3 +61,26 @@ def load_fully_qualified_definition(definition):
module_name, class_name = definition.rsplit('.', 1)
module = import_module(module_name)
return getattr(module, class_name)


def date_range(start_date, end_date, delta=datetime.timedelta(days=1)):
"""
Returns a generator that iterates over the date range [start_date, end_date)
(start_date inclusive, end_date exclusive). Each date in the range is
offset from the previous date by a change of `delta`, which defaults
to one day.
Arguments:
start_date (datetime.datetime): The start date of the range, inclusive
end_date (datetime.datetime): The end date of the range, exclusive
delta (datetime.timedelta): The change in time between dates in the
range.
Returns:
Generator: A generator which iterates over all dates in the specified
range.
"""
cur_date = start_date
while cur_date < end_date:
yield cur_date
cur_date += delta
23 changes: 21 additions & 2 deletions analytics_data_api/v0/models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import datetime
from itertools import groupby

from django.conf import settings
Expand All @@ -8,6 +9,7 @@

from analytics_data_api.constants import country, genders, learner
from analytics_data_api.constants.engagement_types import EngagementType
from analytics_data_api.utils import date_range


class CourseActivityWeekly(models.Model):
Expand Down Expand Up @@ -392,7 +394,7 @@ class ModuleEngagementTimelineManager(models.Manager):
Modifies the ModuleEngagement queryset to aggregate engagement data for
the learner engagement timeline.
"""
def get_timelines(self, course_id, username):
def get_timeline(self, course_id, username):
queryset = ModuleEngagement.objects.all().filter(course_id=course_id, username=username) \
.values('date', 'entity_type', 'event') \
.annotate(total_count=Sum('count')) \
Expand All @@ -418,7 +420,24 @@ def get_timelines(self, course_id, username):
day[engagement_type.name] = day.get(engagement_type.name, 0) + count_delta
timelines.append(day)

return timelines
# Fill in dates that may be missing, since the result store doesn't
# store empty engagement entries.
full_timeline = []
default_timeline_entry = {engagement_type: 0 for engagement_type in EngagementType.ALL_TYPES}
for index, current_date in enumerate(timelines):
full_timeline.append(current_date)
try:
next_date = timelines[index + 1]
except IndexError:
continue
one_day = datetime.timedelta(days=1)
if next_date['date'] > current_date['date'] + one_day:
full_timeline += [
dict(date=date, **default_timeline_entry)
for date in date_range(current_date['date'] + one_day, next_date['date'])
]

return full_timeline


class ModuleEngagement(models.Model):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,13 @@ def test_day_gap(self):
'problems_completed': 0,
'videos_viewed': 1
},
{
'date': '2015-05-27',
'discussion_contributions': 0,
'problems_attempted': 0,
'problems_completed': 0,
'videos_viewed': 0
},
{
'date': '2015-05-28',
'discussion_contributions': 0,
Expand Down
2 changes: 1 addition & 1 deletion analytics_data_api/v0/views/learners.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ def get(self, request, *args, **kwargs):
return super(EngagementTimelineView, self).get(request, *args, **kwargs)

def get_queryset(self):
queryset = ModuleEngagement.objects.get_timelines(self.course_id, self.username)
queryset = ModuleEngagement.objects.get_timeline(self.course_id, self.username)
if len(queryset) == 0:
raise LearnerEngagementTimelineNotFoundError(username=self.username, course_id=self.course_id)
return queryset
Expand Down

0 comments on commit b134e64

Please sign in to comment.