Commit 55e9c00d by Anjali Pal Committed by GitHub

Merge pull request #122 from edx/ajpal/fix-attempts-per-completed

Ajpal/fix attempts per completed
parents 1ba61f24 c05732b5
DISCUSSION = 'discussion'
PROBLEM = 'problem'
VIDEO = 'video'
INDIVIDUAL_TYPES = [DISCUSSION, PROBLEM, VIDEO]
PROBLEMS = 'problems'
VIDEOS = 'videos'
AGGREGATE_TYPES = [DISCUSSION, PROBLEMS, VIDEOS]
from analytics_data_api.constants import engagement_entity_types
ATTEMPTED = 'attempted' ATTEMPTED = 'attempted'
ATTEMPTS_PER_COMPLETED = 'attempts_per_completed' ATTEMPTS_PER_COMPLETED = 'attempts_per_completed'
COMPLETED = 'completed' COMPLETED = 'completed'
CONTRIBUTED = 'contributed' CONTRIBUTED = 'contributed'
VIEWED = 'viewed' VIEWED = 'viewed'
# map entity types to events DISCUSSION = 'discussion'
EVENTS = { PROBLEM = 'problem'
engagement_entity_types.DISCUSSION: [CONTRIBUTED], VIDEO = 'video'
engagement_entity_types.PROBLEM: [ATTEMPTED, ATTEMPTS_PER_COMPLETED, COMPLETED], PROBLEMS = 'problems'
engagement_entity_types.PROBLEMS: [ATTEMPTED, COMPLETED], VIDEOS = 'videos'
engagement_entity_types.VIDEO: [VIEWED],
engagement_entity_types.VIDEOS: [VIEWED], INDIVIDUAL_EVENTS = [
} 'problem_attempts_per_completed',
'problem_attempted',
'problem_completed',
'discussion_contributed',
'video_viewed'
]
EVENTS = [
'problem_attempts_per_completed',
'problems_attempted',
'problems_completed',
'discussion_contributions',
'videos_viewed'
]
from analytics_data_api.constants.engagement_entity_types import DISCUSSION, PROBLEM, VIDEO from analytics_data_api.constants.engagement_events import (ATTEMPTED, ATTEMPTS_PER_COMPLETED, COMPLETED,
from analytics_data_api.constants.engagement_events import ATTEMPTED, COMPLETED, CONTRIBUTED, VIEWED CONTRIBUTED, DISCUSSION, PROBLEM, VIDEO, VIEWED)
class EngagementType(object): class EngagementType(object):
...@@ -12,6 +12,7 @@ class EngagementType(object): ...@@ -12,6 +12,7 @@ class EngagementType(object):
# Defines the current canonical set of engagement types used in the Learner # Defines the current canonical set of engagement types used in the Learner
# Analytics API. # Analytics API.
ALL_TYPES = ( ALL_TYPES = (
'problem_attempts_per_completed',
'problems_attempted', 'problems_attempted',
'problems_completed', 'problems_completed',
'videos_viewed', 'videos_viewed',
...@@ -30,6 +31,9 @@ class EngagementType(object): ...@@ -30,6 +31,9 @@ class EngagementType(object):
if event_type == ATTEMPTED: if event_type == ATTEMPTED:
self.name = 'problems_attempted' self.name = 'problems_attempted'
self.is_counted_by_entity = True self.is_counted_by_entity = True
if event_type == ATTEMPTS_PER_COMPLETED:
self.name = 'problem_attempts_per_completed'
self.is_counted_by_entity = True
if event_type == COMPLETED: if event_type == COMPLETED:
self.name = 'problems_completed' self.name = 'problems_completed'
self.is_counted_by_entity = True self.is_counted_by_entity = True
......
...@@ -8,7 +8,7 @@ import random ...@@ -8,7 +8,7 @@ import random
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.utils import timezone from django.utils import timezone
from analytics_data_api.v0 import models from analytics_data_api.v0 import models
from analytics_data_api.constants import engagement_entity_types, engagement_events from analytics_data_api.constants import engagement_events
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -194,16 +194,17 @@ class Command(BaseCommand): ...@@ -194,16 +194,17 @@ class Command(BaseCommand):
current = start_date current = start_date
while current < end_date: while current < end_date:
current = current + datetime.timedelta(days=1) current = current + datetime.timedelta(days=1)
for entity_type in engagement_entity_types.INDIVIDUAL_TYPES: for metric in engagement_events.INDIVIDUAL_EVENTS:
for event in engagement_events.EVENTS[entity_type]: num_events = random.randint(0, max_value)
num_events = random.randint(0, max_value) if num_events:
if num_events: for _ in xrange(num_events):
for _ in xrange(num_events): count = random.randint(0, max_value / 20)
count = random.randint(0, max_value / 20) entity_type = metric.split('_', 1)[0]
entity_id = 'an-id-{}-{}'.format(entity_type, event) event = metric.split('_', 1)[1]
models.ModuleEngagement.objects.create( entity_id = 'an-id-{}-{}'.format(entity_type, event)
course_id=course_id, username=username, date=current, models.ModuleEngagement.objects.create(
entity_type=entity_type, entity_id=entity_id, event=event, count=count) course_id=course_id, username=username, date=current,
entity_type=entity_type, entity_id=entity_id, event=event, count=count)
logger.info("Done!") logger.info("Done!")
def generate_learner_engagement_range_data(self, course_id, start_date, end_date, max_value=100): def generate_learner_engagement_range_data(self, course_id, start_date, end_date, max_value=100):
...@@ -211,19 +212,15 @@ class Command(BaseCommand): ...@@ -211,19 +212,15 @@ class Command(BaseCommand):
models.ModuleEngagementMetricRanges.objects.all().delete() models.ModuleEngagementMetricRanges.objects.all().delete()
logger.info("Generating engagement range data...") logger.info("Generating engagement range data...")
for entity_type in engagement_entity_types.AGGREGATE_TYPES: for event in engagement_events.EVENTS:
for event in engagement_events.EVENTS[entity_type]: low_ceil = random.random() * max_value * 0.5
metric = '{0}_{1}'.format(entity_type, event) models.ModuleEngagementMetricRanges.objects.create(
course_id=course_id, start_date=start_date, end_date=end_date, metric=event,
low_ceil = random.random() * max_value * 0.5 range_type='low', low_value=0, high_value=low_ceil)
models.ModuleEngagementMetricRanges.objects.create( high_floor = random.random() * max_value * 0.5 + low_ceil
course_id=course_id, start_date=start_date, end_date=end_date, metric=metric, models.ModuleEngagementMetricRanges.objects.create(
range_type='low', low_value=0, high_value=low_ceil) course_id=course_id, start_date=start_date, end_date=end_date, metric=event,
range_type='high', low_value=high_floor, high_value=max_value)
high_floor = random.random() * max_value * 0.5 + low_ceil
models.ModuleEngagementMetricRanges.objects.create(
course_id=course_id, start_date=start_date, end_date=end_date, metric=metric,
range_type='high', low_value=high_floor, high_value=max_value)
def generate_tags_distribution_data(self, course_id): def generate_tags_distribution_data(self, course_id):
logger.info("Deleting existed tags distribution data...") logger.info("Deleting existed tags distribution data...")
......
...@@ -3,7 +3,6 @@ from django.conf import settings ...@@ -3,7 +3,6 @@ from django.conf import settings
from rest_framework import pagination, serializers from rest_framework import pagination, serializers
from analytics_data_api.constants import ( from analytics_data_api.constants import (
engagement_entity_types,
engagement_events, engagement_events,
enrollment_modes, enrollment_modes,
genders, genders,
...@@ -458,23 +457,16 @@ class CourseLearnerMetadataSerializer(serializers.Serializer): ...@@ -458,23 +457,16 @@ class CourseLearnerMetadataSerializer(serializers.Serializer):
'date_range': DateRangeSerializer(query_set[0] if len(query_set) else None).data 'date_range': DateRangeSerializer(query_set[0] if len(query_set) else None).data
} }
# go through each entity and event type combination and fill in the ranges for metric in engagement_events.EVENTS:
for entity_type in engagement_entity_types.AGGREGATE_TYPES: low_range_queryset = query_set.filter(metric=metric, range_type='low')
for event in engagement_events.EVENTS[entity_type]: normal_range_queryset = query_set.filter(metric=metric, range_type='normal')
metric = '{0}_{1}'.format(entity_type, event) high_range_queryset = query_set.filter(metric=metric, range_type='high')
# It's assumed that there may be any combination of low, normal, engagement_ranges.update({
# and high ranges in the database for the given course. Some metric: EnagementRangeMetricSerializer({
# edge cases result from a lack of available data; in such 'low_range': low_range_queryset[0] if len(low_range_queryset) else None,
# cases, only some ranges may be returned. 'normal_range': normal_range_queryset[0] if len(normal_range_queryset) else None,
low_range_queryset = query_set.filter(metric=metric, range_type='low') 'high_range': high_range_queryset[0] if len(high_range_queryset) else None,
normal_range_queryset = query_set.filter(metric=metric, range_type='normal') }).data
high_range_queryset = query_set.filter(metric=metric, range_type='high') })
engagement_ranges.update({
metric: EnagementRangeMetricSerializer({
'low_range': low_range_queryset[0] if len(low_range_queryset) else None,
'normal_range': normal_range_queryset[0] if len(normal_range_queryset) else None,
'high_range': high_range_queryset[0] if len(high_range_queryset) else None,
}).data
})
return engagement_ranges return engagement_ranges
...@@ -9,8 +9,8 @@ import pytz ...@@ -9,8 +9,8 @@ import pytz
from rest_framework import status from rest_framework import status
from analyticsdataserver.tests import TestCaseWithAuthentication from analyticsdataserver.tests import TestCaseWithAuthentication
from analytics_data_api.constants.engagement_entity_types import DISCUSSION, PROBLEM, VIDEO from analytics_data_api.constants.engagement_events import (ATTEMPTED, COMPLETED, CONTRIBUTED, DISCUSSION,
from analytics_data_api.constants.engagement_events import ATTEMPTED, COMPLETED, CONTRIBUTED, VIEWED PROBLEM, VIDEO, VIEWED)
from analytics_data_api.v0 import models from analytics_data_api.v0 import models
from analytics_data_api.v0.tests.views import DemoCourseMixin, VerifyCourseIdMixin from analytics_data_api.v0.tests.views import DemoCourseMixin, VerifyCourseIdMixin
......
...@@ -17,7 +17,7 @@ from django.conf import settings ...@@ -17,7 +17,7 @@ from django.conf import settings
from django.core import management from django.core import management
from analyticsdataserver.tests import TestCaseWithAuthentication from analyticsdataserver.tests import TestCaseWithAuthentication
from analytics_data_api.constants import engagement_entity_types, engagement_events from analytics_data_api.constants import engagement_events
from analytics_data_api.v0.models import ModuleEngagementMetricRanges from analytics_data_api.v0.models import ModuleEngagementMetricRanges
from analytics_data_api.v0.tests.views import DemoCourseMixin, VerifyCourseIdMixin from analytics_data_api.v0.tests.views import DemoCourseMixin, VerifyCourseIdMixin
...@@ -571,19 +571,10 @@ class CourseLearnerMetadataTests(DemoCourseMixin, VerifyCourseIdMixin, ...@@ -571,19 +571,10 @@ class CourseLearnerMetadataTests(DemoCourseMixin, VerifyCourseIdMixin,
empty_range = { empty_range = {
range_type: None for range_type in ['below_average', 'average', 'above_average'] range_type: None for range_type in ['below_average', 'average', 'above_average']
} }
for metric in self.engagement_metrics: for metric in engagement_events.EVENTS:
empty_engagement_ranges['engagement_ranges'][metric] = copy.deepcopy(empty_range) empty_engagement_ranges['engagement_ranges'][metric] = copy.deepcopy(empty_range)
return empty_engagement_ranges return empty_engagement_ranges
@property
def engagement_metrics(self):
""" Convenience method for getting the metric types. """
metrics = []
for entity_type in engagement_entity_types.AGGREGATE_TYPES:
for event in engagement_events.EVENTS[entity_type]:
metrics.append('{0}_{1}'.format(entity_type, event))
return metrics
def test_no_engagement_ranges(self): def test_no_engagement_ranges(self):
response = self._get(self.course_id) response = self._get(self.course_id)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
...@@ -627,7 +618,7 @@ class CourseLearnerMetadataTests(DemoCourseMixin, VerifyCourseIdMixin, ...@@ -627,7 +618,7 @@ class CourseLearnerMetadataTests(DemoCourseMixin, VerifyCourseIdMixin,
} }
max_value = 1000.0 max_value = 1000.0
for metric_type in self.engagement_metrics: for metric_type in engagement_events.EVENTS:
low_ceil = 100.5 low_ceil = 100.5
G(ModuleEngagementMetricRanges, course_id=self.course_id, start_date=start_date, end_date=end_date, G(ModuleEngagementMetricRanges, course_id=self.course_id, start_date=start_date, end_date=end_date,
metric=metric_type, range_type='low', low_value=0, high_value=low_ceil) metric=metric_type, range_type='low', low_value=0, high_value=low_ceil)
...@@ -649,12 +640,8 @@ class CourseLearnerMetadataTests(DemoCourseMixin, VerifyCourseIdMixin, ...@@ -649,12 +640,8 @@ class CourseLearnerMetadataTests(DemoCourseMixin, VerifyCourseIdMixin,
self.assertDictContainsSubset(expected, json.loads(response.content)) self.assertDictContainsSubset(expected, json.loads(response.content))
def test_engagement_ranges_fields(self): def test_engagement_ranges_fields(self):
actual_entity_types = engagement_entity_types.INDIVIDUAL_TYPES expected_events = engagement_events.EVENTS
expected_entity_types = ['discussion', 'problem', 'video'] response = json.loads(self._get(self.course_id).content)
self.assertEqual(actual_entity_types, expected_entity_types) self.assertTrue('engagement_ranges' in response)
actual_events = [] for event in expected_events:
for entity_type in actual_entity_types: self.assertTrue(event in response['engagement_ranges'])
for event in engagement_events.EVENTS[entity_type]:
actual_events.append(event)
expected_events = ['contributed', 'attempted', 'attempts_per_completed', 'completed', 'viewed']
self.assertEqual(actual_events, expected_events)
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