Commit 99dcad6c by Jason Bau

Answer Distribution API, first take

Change-Id: I872ae5a34e7ef28b0efe6c6fa855d31ffd4394a3
parent 1c69bab7
...@@ -55,3 +55,6 @@ docs/_build/ ...@@ -55,3 +55,6 @@ docs/_build/
# Sqlite Database # Sqlite Database
*.db *.db
# PyCharm
.idea/
...@@ -35,7 +35,7 @@ Loading Data ...@@ -35,7 +35,7 @@ Loading Data
The fixtures directory contains demo data. This data can be loaded with the following commands: The fixtures directory contains demo data. This data can be loaded with the following commands:
$ ./manage.py syncdb --migrate --noinput --database=analytics $ ./manage.py syncdb --migrate --noinput --database=analytics
$ ./manage.py loaddata courses education_levels single_course_activity course_enrollment_birth_year course_enrollment_education course_enrollment_gender --database=analytics $ ./manage.py loaddata courses education_levels single_course_activity course_enrollment_birth_year course_enrollment_education course_enrollment_gender problem_response_answer_distribution --database=analytics
Running Tests Running Tests
------------- -------------
......
[
{
"fields": {
"answer_value_numeric": 36.02736,
"answer_value_text": "36.02736",
"correct": false,
"count": 1,
"course_id": "EarthSciences/GP202/Spring2014",
"created": "2014-06-24T17:13:11",
"module_id": "i4x://EarthSciences/GP202/problem/d50bf059fa05468e94f61d52dd37f228",
"part_id": "i4x-EarthSciences-GP202-problem-d50bf059fa05468e94f61d52dd37f228_4_1",
"value_id": null,
"variant": null
},
"model": "v0.problemresponseanswerdistribution",
"pk": 1
},
{
"fields": {
"answer_value_numeric": 33.0,
"answer_value_text": "33.0",
"correct": false,
"count": 1,
"course_id": "EarthSciences/GP202/Spring2014",
"created": "2014-06-24T17:13:11",
"module_id": "i4x://EarthSciences/GP202/problem/d50bf059fa05468e94f61d52dd37f228",
"part_id": "i4x-EarthSciences-GP202-problem-d50bf059fa05468e94f61d52dd37f228_4_1",
"value_id": null,
"variant": null
},
"model": "v0.problemresponseanswerdistribution",
"pk": 2
},
{
"fields": {
"answer_value_numeric": 27.51936482,
"answer_value_text": "27.51936482",
"correct": true,
"count": 1,
"course_id": "EarthSciences/GP202/Spring2014",
"created": "2014-06-24T17:13:11",
"module_id": "i4x://EarthSciences/GP202/problem/d50bf059fa05468e94f61d52dd37f228",
"part_id": "i4x-EarthSciences-GP202-problem-d50bf059fa05468e94f61d52dd37f228_4_1",
"value_id": null,
"variant": null
},
"model": "v0.problemresponseanswerdistribution",
"pk": 3
},
{
"fields": {
"answer_value_numeric": 28.65,
"answer_value_text": "28.65",
"correct": true,
"count": 1,
"course_id": "EarthSciences/GP202/Spring2014",
"created": "2014-06-24T17:13:11",
"module_id": "i4x://EarthSciences/GP202/problem/d50bf059fa05468e94f61d52dd37f228",
"part_id": "i4x-EarthSciences-GP202-problem-d50bf059fa05468e94f61d52dd37f228_4_1",
"value_id": null,
"variant": null
},
"model": "v0.problemresponseanswerdistribution",
"pk": 4
},
{
"fields": {
"answer_value_numeric": null,
"answer_value_text": "line 15: mean(glm.pred==Direction)",
"correct": true,
"count": 95,
"course_id": "HumanitiesScience/StatLearning/Winter2014",
"created": "2014-06-24T17:13:11",
"module_id": "i4x://HumanitiesScience/StatLearning/problem/3c53e173c9af464aa3afb8fac82c3702",
"part_id": "i4x-HumanitiesScience-StatLearning-problem-3c53e173c9af464aa3afb8fac82c3702_2_1",
"value_id": "choice_0",
"variant": null
},
"model": "v0.problemresponseanswerdistribution",
"pk": 5
},
{
"fields": {
"answer_value_numeric": null,
"answer_value_text": "line 22: Direction.2005=Smarket$Direction[!train]",
"correct": false,
"count": 1,
"course_id": "HumanitiesScience/StatLearning/Winter2014",
"created": "2014-06-24T17:13:11",
"module_id": "i4x://HumanitiesScience/StatLearning/problem/3c53e173c9af464aa3afb8fac82c3702",
"part_id": "i4x-HumanitiesScience-StatLearning-problem-3c53e173c9af464aa3afb8fac82c3702_2_1",
"value_id": "choice_2",
"variant": null
},
"model": "v0.problemresponseanswerdistribution",
"pk": 6
},
{
"fields": {
"answer_value_numeric": null,
"answer_value_text": "line 30: table(glm.pred,Direction.2005)",
"correct": false,
"count": 1,
"course_id": "HumanitiesScience/StatLearning/Winter2014",
"created": "2014-06-24T17:13:12",
"module_id": "i4x://HumanitiesScience/StatLearning/problem/3c53e173c9af464aa3afb8fac82c3702",
"part_id": "i4x-HumanitiesScience-StatLearning-problem-3c53e173c9af464aa3afb8fac82c3702_2_1",
"value_id": "choice_3",
"variant": null
},
"model": "v0.problemresponseanswerdistribution",
"pk": 7
}
]
...@@ -69,3 +69,22 @@ class CourseEnrollmentByGender(BaseCourseEnrollment): ...@@ -69,3 +69,22 @@ class CourseEnrollmentByGender(BaseCourseEnrollment):
class Meta(object): class Meta(object):
db_table = 'course_enrollment_gender' db_table = 'course_enrollment_gender'
ordering = ('course', 'gender') ordering = ('course', 'gender')
class ProblemResponseAnswerDistribution(models.Model):
""" Each row stores the count of a particular answer to a response in a problem in a course (usage). """
class Meta(object):
db_table = 'answer_distribution'
course_id = models.CharField(db_index=True, max_length=255, db_column='course_id')
module_id = models.CharField(db_index=True, max_length=255, db_column='module_id')
part_id = models.CharField(db_index=True, max_length=255, db_column='part_id')
correct = models.BooleanField(db_column='correct')
count = models.IntegerField(db_column='count')
value_id = models.CharField(db_index=True, max_length=255, db_column='value_id', null=True)
answer_value_text = models.TextField(db_column='answer_value_text', null=True)
answer_value_numeric = models.FloatField(db_column='answer_value_numeric', null=True)
variant = models.IntegerField(db_column='variant', null=True)
created = models.DateTimeField(auto_now_add=True, db_column='created')
from rest_framework import serializers from rest_framework import serializers
from analytics_data_api.v0.models import CourseActivityByWeek from analytics_data_api.v0.models import CourseActivityByWeek, ProblemResponseAnswerDistribution
class CourseActivityByWeekSerializer(serializers.ModelSerializer): class CourseActivityByWeekSerializer(serializers.ModelSerializer):
...@@ -17,3 +17,27 @@ class CourseActivityByWeekSerializer(serializers.ModelSerializer): ...@@ -17,3 +17,27 @@ class CourseActivityByWeekSerializer(serializers.ModelSerializer):
class Meta(object): class Meta(object):
model = CourseActivityByWeek model = CourseActivityByWeek
fields = ('interval_start', 'interval_end', 'activity_type', 'count', 'course_id') fields = ('interval_start', 'interval_end', 'activity_type', 'count', 'course_id')
class ProblemResponseAnswerDistributionSerializer(serializers.ModelSerializer):
"""
Representation of the Answer Distribution table, without id.
This table is managed by the data pipeline, and records can be removed and added at any time. The id for a
particular record is likely to change unexpectedly so we avoid exposing it.
"""
class Meta(object):
model = ProblemResponseAnswerDistribution
fields = (
'course_id',
'module_id',
'part_id',
'correct',
'count',
'value_id',
'answer_value_text',
'answer_value_numeric',
'variant',
'created'
)
...@@ -11,7 +11,8 @@ from django_dynamic_fixture import G ...@@ -11,7 +11,8 @@ from django_dynamic_fixture import G
import pytz import pytz
from analytics_data_api.v0.models import CourseEnrollmentByBirthYear, CourseEnrollmentByEducation, EducationLevel, \ from analytics_data_api.v0.models import CourseEnrollmentByBirthYear, CourseEnrollmentByEducation, EducationLevel, \
CourseEnrollmentByGender, CourseActivityByWeek, Course CourseEnrollmentByGender, CourseActivityByWeek, Course, ProblemResponseAnswerDistribution
from analytics_data_api.v0.serializers import ProblemResponseAnswerDistributionSerializer
from analyticsdataserver.tests import TestCaseWithAuthentication from analyticsdataserver.tests import TestCaseWithAuthentication
...@@ -179,3 +180,34 @@ class CourseManagerTests(TestCase): ...@@ -179,3 +180,34 @@ class CourseManagerTests(TestCase):
course = G(Course, course_id=course_id) course = G(Course, course_id=course_id)
self.assertEqual(course, Course.objects.get_by_natural_key(course_id)) self.assertEqual(course, Course.objects.get_by_natural_key(course_id))
# pylint: disable=no-member,no-value-for-parameter
class AnswerDistributionTests(TestCaseWithAuthentication):
path = '/answer_distribution'
maxDiff = None
@classmethod
def setUpClass(cls):
cls.course_id = "org/num/run"
cls.module_id = "i4x://org/num/run/problem/RANDOMNUMBER"
cls.part_id1 = "i4x-org-num-run-problem-RANDOMNUMBER_2_1"
cls.ad1 = G(
ProblemResponseAnswerDistribution,
course_id=cls.course_id,
module_id=cls.module_id,
part_id=cls.part_id1
)
def test_get(self):
response = self.authenticated_get('/api/v0/problems/%s%s' % (self.module_id, self.path))
self.assertEquals(response.status_code, 200)
expected_dict = ProblemResponseAnswerDistributionSerializer(self.ad1).data
actual_list = response.data
self.assertEquals(len(actual_list), 1)
self.assertDictEqual(actual_list[0], expected_dict)
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)
...@@ -3,4 +3,5 @@ from django.conf.urls import patterns, url, include ...@@ -3,4 +3,5 @@ from django.conf.urls import patterns, url, include
urlpatterns = patterns( urlpatterns = patterns(
'', '',
url(r'^courses/', include('analytics_data_api.v0.urls.courses', namespace='courses')), url(r'^courses/', include('analytics_data_api.v0.urls.courses', namespace='courses')),
url(r'^problems/', include('analytics_data_api.v0.urls.problems', namespace='problems')),
) )
import re
from django.conf.urls import patterns, url
from analytics_data_api.v0.views.problems import ProblemResponseAnswerDistributionView
PROBLEM_URLS = [
('answer_distribution', ProblemResponseAnswerDistributionView, 'answer_distribution'),
]
urlpatterns = patterns(
'',
)
for path, view, name in PROBLEM_URLS:
urlpatterns += patterns('', url(r'^(?P<problem_id>.+)/' + re.escape(path) + r'$', view.as_view(), name=name))
from rest_framework import generics
from analytics_data_api.v0.models import ProblemResponseAnswerDistribution
from analytics_data_api.v0.serializers import ProblemResponseAnswerDistributionSerializer
class ProblemResponseAnswerDistributionView(generics.ListAPIView):
"""
Distribution of student answers for a particular problem, as used in a particular course.
Results are available for most (but not all) multiple-choice and short answer response types.
"""
serializer_class = ProblemResponseAnswerDistributionSerializer
allow_empty = False
def get_queryset(self):
"""Select all the answer distribution response having to do with this usage of the problem."""
problem_id = self.kwargs.get('problem_id')
return ProblemResponseAnswerDistribution.objects.filter(module_id=problem_id)
"""
A variation on the local environment that uses mysql for the analytics database.
Useful for developers running both mysql ingress locally and the api locally
"""
from analyticsdataserver.settings.local import *
########## DATABASE CONFIGURATION
# See: https://docs.djangoproject.com/en/dev/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': normpath(join(DJANGO_ROOT, 'default.db')),
'USER': '',
'PASSWORD': '',
'HOST': '',
'PORT': '',
},
'analytics': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'analytics',
'USER': 'root',
'PASSWORD': '',
'HOST': '',
'PORT': '',
}
}
\ No newline at end of file
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