Commit abf9e77b by Clinton Blackburn

Added Endpoint to Get Submission Counts for Multiple Problems

parent 5971ac6a
from django.conf import settings
from rest_framework import serializers
from analytics_data_api.constants import enrollment_modes, genders
from analytics_data_api.v0 import models
# Below are the enrollment modes supported by this API. The audit and honor enrollment modes are merged into honor.
ENROLLMENT_MODES = [enrollment_modes.HONOR, enrollment_modes.PROFESSIONAL, enrollment_modes.VERIFIED]
......@@ -37,6 +39,16 @@ class ModelSerializerWithCreatedField(serializers.ModelSerializer):
created = serializers.DateTimeField(format=settings.DATETIME_FORMAT)
class ProblemSubmissionCountSerializer(serializers.Serializer):
"""
Serializer for problem submission counts.
"""
module_id = serializers.CharField()
total = serializers.IntegerField(default=0)
correct = serializers.IntegerField(default=0)
class ProblemResponseAnswerDistributionSerializer(ModelSerializerWithCreatedField):
"""
Representation of the Answer Distribution table, without id.
......@@ -67,6 +79,7 @@ class GradeDistributionSerializer(ModelSerializerWithCreatedField):
"""
Representation of the grade_distribution table without id
"""
class Meta(object):
model = models.GradeDistribution
fields = (
......
from opaque_keys.edx.keys import CourseKey
DEMO_COURSE_ID = u'course-v1:edX+DemoX+Demo_2014'
class DemoCourseMixin(object):
course_key = None
course_id = None
@classmethod
def setUpClass(cls):
cls.course_id = DEMO_COURSE_ID
cls.course_key = CourseKey.from_string(cls.course_id)
super(DemoCourseMixin, cls).setUpClass()
......@@ -12,29 +12,16 @@ import urllib
from django.conf import settings
from django_dynamic_fixture import G
import pytz
from opaque_keys.edx.keys import CourseKey
from analytics_data_api.constants.country import get_country
from analytics_data_api.v0 import models
from analytics_data_api.constants import country, enrollment_modes, genders
from analytics_data_api.v0.models import CourseActivityWeekly
from analytics_data_api.v0.tests.utils import flatten
from analytics_data_api.v0.tests.views import DemoCourseMixin, DEMO_COURSE_ID
from analyticsdataserver.tests import TestCaseWithAuthentication
DEMO_COURSE_ID = u'course-v1:edX+DemoX+Demo_2014'
class DemoCourseMixin(object):
course_key = None
course_id = None
def setUp(self):
self.course_id = DEMO_COURSE_ID
self.course_key = CourseKey.from_string(self.course_id)
super(DemoCourseMixin, self).setUp()
class DefaultFillTestMixin(object):
"""
Test that the view fills in missing data with a default value.
......
......@@ -10,6 +10,7 @@ from django_dynamic_fixture import G
from analytics_data_api.v0 import models
from analytics_data_api.v0.serializers import ProblemResponseAnswerDistributionSerializer, \
GradeDistributionSerializer, SequentialOpenDistributionSerializer
from analytics_data_api.v0.tests.views import DemoCourseMixin
from analyticsdataserver.tests import TestCaseWithAuthentication
......@@ -97,3 +98,68 @@ class SequentialOpenDistributionTests(TestCaseWithAuthentication):
def test_get_404(self):
response = self.authenticated_get('/api/v0/problems/%s%s' % ("DOES-NOT-EXIST", self.path))
self.assertEquals(response.status_code, 404)
class SubmissionCountsListViewTests(DemoCourseMixin, TestCaseWithAuthentication):
path = '/api/v0/problems/submission_counts/'
@classmethod
def setUpClass(cls):
super(SubmissionCountsListViewTests, cls).setUpClass()
cls.ad_1 = G(models.ProblemResponseAnswerDistribution)
cls.ad_2 = G(models.ProblemResponseAnswerDistribution)
def _get_data(self, problem_ids=None):
"""
Retrieve data for the specified problems from the server.
"""
url = self.path
if problem_ids:
problem_ids = ','.join(problem_ids)
url = '{}?problem_ids={}'.format(url, problem_ids)
return self.authenticated_get(url)
def assertValidResponse(self, *problem_ids):
expected_data = []
for problem_id in problem_ids:
_models = models.ProblemResponseAnswerDistribution.objects.filter(module_id=problem_id)
serialized = [{'module_id': model.module_id, 'total': model.count, 'correct': model.correct or 0} for model
in _models]
expected_data += serialized
response = self._get_data(problem_ids)
self.assertEquals(response.status_code, 200)
actual = response.data
self.assertListEqual(actual, expected_data)
def test_get(self):
"""
The view should return data when data exists for at least one of the problems.
"""
problem_id_1 = self.ad_1.module_id
problem_id_2 = self.ad_2.module_id
self.assertValidResponse(problem_id_1)
self.assertValidResponse(problem_id_1, problem_id_2)
self.assertValidResponse(problem_id_1, problem_id_2, 'DOES-NOT-EXIST')
def test_get_404(self):
"""
The view should return 404 if data does not exist for at least one of the provided problems.
"""
problem_ids = ['DOES-NOT-EXIST']
response = self._get_data(problem_ids)
self.assertEquals(response.status_code, 404)
def test_get_406(self):
"""
The view should return a 406 if no problem ID values are supplied.
"""
response = self._get_data()
self.assertEquals(response.status_code, 406)
......@@ -2,19 +2,18 @@ import re
from django.conf.urls import patterns, url
from analytics_data_api.v0.views.problems import ProblemResponseAnswerDistributionView
from analytics_data_api.v0.views.problems import GradeDistributionView
from analytics_data_api.v0.views.problems import SequentialOpenDistributionView
from analytics_data_api.v0.views import problems as views
PROBLEM_URLS = [
('answer_distribution', ProblemResponseAnswerDistributionView, 'answer_distribution'),
('grade_distribution', GradeDistributionView, 'grade_distribution'),
('answer_distribution', views.ProblemResponseAnswerDistributionView, 'answer_distribution'),
('grade_distribution', views.GradeDistributionView, 'grade_distribution'),
]
urlpatterns = patterns(
'',
url(r'^submission_counts/$', views.SubmissionCountsListView.as_view(), name='submission_counts'),
url(r'^(?P<module_id>.+)/sequential_open_distribution/$',
SequentialOpenDistributionView.as_view(), name='sequential_open_distribution'),
views.SequentialOpenDistributionView.as_view(), name='sequential_open_distribution'),
)
for path, view, name in PROBLEM_URLS:
......
from itertools import groupby
from rest_framework import generics
from rest_framework.exceptions import NotAcceptable
from analytics_data_api.v0.models import ProblemResponseAnswerDistribution
from analytics_data_api.v0.serializers import ProblemResponseAnswerDistributionSerializer
from analytics_data_api.v0.serializers import ProblemResponseAnswerDistributionSerializer, \
ProblemSubmissionCountSerializer
from analytics_data_api.v0.models import GradeDistribution
from analytics_data_api.v0.serializers import GradeDistributionSerializer
from analytics_data_api.v0.models import SequentialOpenDistribution
from analytics_data_api.v0.serializers import SequentialOpenDistributionSerializer
class SubmissionCountsListView(generics.ListAPIView):
"""
Get the number of submissions to one, or more, problems.
**Example request**
GET /api/v0/problems/submission_counts/?problem_ids={problem_id},{problem_id}
**Response Values**
Returns a collection of counts of total and correct solutions to the specified
problems. Each collection contains:
* module_id: The ID of the problem.
* total: Total number of submissions
* correct: Total number of *correct* submissions.
**Parameters**
problem_ids -- Comma-separated list of problem IDs representing the problems whose data should be returned.
"""
serializer_class = ProblemSubmissionCountSerializer
allow_empty = False
def get_queryset(self):
problem_ids = self.request.QUERY_PARAMS.get('problem_ids', '')
if not problem_ids:
raise NotAcceptable
problem_ids = problem_ids.split(',')
queryset = ProblemResponseAnswerDistribution.objects.filter(module_id__in=problem_ids).order_by('module_id')
data = []
for problem_id, distribution in groupby(queryset, lambda x: x.module_id):
total = 0
correct = 0
for answer in distribution:
count = answer.count
total += count
if answer.correct:
correct += count
data.append({
'module_id': problem_id,
'total': total,
'correct': correct
})
return data
class ProblemResponseAnswerDistributionView(generics.ListAPIView):
"""
Get the distribution of student answers to a specific problem.
......
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