Commit f88acf51 by Stephen Sanchez

Merge pull request #14 from edx/sanchez/first_pass_at_basic_api

My first pass at populating the submission API.
parents 73409417 5d935d88
...@@ -10,8 +10,14 @@ might be tricky with XML import based courses). ...@@ -10,8 +10,14 @@ might be tricky with XML import based courses).
""" """
def create_evaluation(submission_id, score, rubric): def create_evaluation(submission_id, score, rubric):
pass pass
def get_evaluations(submission_id):
pass
def get_submission_to_evaluate(student_item, scorer_student_id): def get_submission_to_evaluate(student_item, scorer_student_id):
pass pass
...@@ -6,8 +6,8 @@ Student submission: ...@@ -6,8 +6,8 @@ Student submission:
* XBlock creates a Submission * XBlock creates a Submission
* submissions app sends a general notification that a submission has happened * submissions app sends a general notification that a submission has happened
* openresponse can listen for that signal if it wants, or query itself on demand * openassessment can listen for that signal if it wants, or query itself on demand
* when openresponse is satistifed that it has collected enough information to * when openassessment is satisfied that it has collected enough information to
score the student, it will push that score information back to this app. score the student, it will push that score information back to this app.
* when the LMS wants to know what raw scores a student has, it calls this app. * when the LMS wants to know what raw scores a student has, it calls this app.
...@@ -19,6 +19,7 @@ Things to consider probably aren't worth the extra effort/complexity in the MVP: ...@@ -19,6 +19,7 @@ Things to consider probably aren't worth the extra effort/complexity in the MVP:
from django.db import models from django.db import models
from django.utils.timezone import now from django.utils.timezone import now
class StudentItem(models.Model): class StudentItem(models.Model):
"""Represents a single item for a single course for a single user. """Represents a single item for a single course for a single user.
...@@ -39,13 +40,18 @@ class StudentItem(models.Model): ...@@ -39,13 +40,18 @@ class StudentItem(models.Model):
# What kind of problem is this? The XBlock tag if it's an XBlock # What kind of problem is this? The XBlock tag if it's an XBlock
item_type = models.CharField(max_length=100) item_type = models.CharField(max_length=100)
def __repr__(self):
return repr(dict(
student_id=self.student_id,
course_id=self.course_id,
item_id=self.item_id,
item_type=self.item_type,
))
class Meta: class Meta:
unique_together = ( unique_together = (
# For integrity reasons, and looking up all of a student's items # For integrity reasons, and looking up all of a student's items
("course_id", "student_id", "item_id"), ("course_id", "student_id", "item_id"),
# Composite index for getting information across a course
("course_id", "item_id"),
) )
...@@ -71,6 +77,18 @@ class Submission(models.Model): ...@@ -71,6 +77,18 @@ class Submission(models.Model):
# The actual answer, assumed to be a JSON string # The actual answer, assumed to be a JSON string
answer = models.TextField(blank=True) answer = models.TextField(blank=True)
def __repr__(self):
return repr(dict(
student_item=self.student_item,
attempt_number=self.attempt_number,
submitted_at=self.submitted_at,
created_at=self.created_at,
answer=self.answer,
))
class Meta:
ordering = ["-submitted_at"]
class Score(models.Model): class Score(models.Model):
"""What the user scored for a given StudentItem. """What the user scored for a given StudentItem.
...@@ -85,3 +103,12 @@ class Score(models.Model): ...@@ -85,3 +103,12 @@ class Score(models.Model):
points_possible = models.PositiveIntegerField(default=0) points_possible = models.PositiveIntegerField(default=0)
created_at = models.DateTimeField(editable=False, default=now, db_index=True) created_at = models.DateTimeField(editable=False, default=now, db_index=True)
def __repr__(self):
return repr(dict(
student_item=self.student_item,
submission=self.submission,
created_at=self.created_at,
points_earned=self.points_earned,
points_possible=self.points_possible,
))
"""
Serializers are created to ensure models do not have to be accessed outside the scope of the Tim APIs.
"""
from rest_framework import serializers
from submissions.models import StudentItem, Submission, Score
class StudentItemSerializer(serializers.ModelSerializer):
class Meta:
model = StudentItem
fields = ('student_id', 'course_id', 'item_id', 'item_type')
class SubmissionSerializer(serializers.ModelSerializer):
class Meta:
model = Submission
fields = ('student_item', 'attempt_number', 'submitted_at', 'created_at', 'answer')
class ScoreSerializer(serializers.ModelSerializer):
class Meta:
model = Score
fields = ('student_item', 'submission', 'points_earned', 'points_possible', 'created_at')
import datetime
from ddt import ddt, file_data
from django.db import DatabaseError
from django.test import TestCase
from nose.tools import raises
from mock import patch
from submissions.api import create_submission, get_submissions, SubmissionRequestError, SubmissionInternalError
from submissions.models import Submission
from submissions.serializers import StudentItemSerializer
STUDENT_ITEM = dict(
student_id="Tim",
course_id="Demo_Course",
item_id="item_one",
item_type="Peer_Submission",
)
ANSWER_ONE = u"this is my answer!"
ANSWER_TWO = u"this is my other answer!"
@ddt
class TestApi(TestCase):
def test_create_submission(self):
submission = create_submission(STUDENT_ITEM, ANSWER_ONE)
self._assert_submission(submission, ANSWER_ONE, 1, 1)
def test_get_submissions(self):
create_submission(STUDENT_ITEM, ANSWER_ONE)
create_submission(STUDENT_ITEM, ANSWER_TWO)
submissions = get_submissions(STUDENT_ITEM)
self._assert_submission(submissions[1], ANSWER_ONE, 1, 1)
self._assert_submission(submissions[0], ANSWER_TWO, 1, 2)
@file_data('test_valid_student_items.json')
def test_various_student_items(self, valid_student_item):
create_submission(valid_student_item, ANSWER_ONE)
submission = get_submissions(valid_student_item)[0]
self._assert_submission(submission, ANSWER_ONE, 1, 1)
def test_get_latest_submission(self):
past_date = datetime.date(2007, 11, 23)
more_recent_date = datetime.date(2011, 10, 15)
create_submission(STUDENT_ITEM, ANSWER_ONE, more_recent_date)
create_submission(STUDENT_ITEM, ANSWER_TWO, past_date)
# Test a limit on the submissions
submissions = get_submissions(STUDENT_ITEM, 1)
self.assertEqual(1, len(submissions))
self.assertEqual(ANSWER_ONE, submissions[0]["answer"])
self.assertEqual(more_recent_date.year,
submissions[0]["submitted_at"].year)
def test_set_attempt_number(self):
create_submission(STUDENT_ITEM, ANSWER_ONE, None, 2)
submissions = get_submissions(STUDENT_ITEM)
self._assert_submission(submissions[0], ANSWER_ONE, 1, 2)
@raises(SubmissionRequestError)
@file_data('test_bad_student_items.json')
def test_error_checking(self, bad_student_item):
create_submission(bad_student_item, -100)
@raises(SubmissionRequestError)
def test_error_checking_submissions(self):
create_submission(STUDENT_ITEM, ANSWER_ONE, None, -1)
@patch.object(Submission.objects, 'filter')
@raises(SubmissionInternalError)
def test_error_on_submission_creation(self, mock_filter):
mock_filter.side_effect = DatabaseError("Bad things happened")
create_submission(STUDENT_ITEM, ANSWER_ONE)
@patch.object(StudentItemSerializer, 'save')
@raises(SubmissionInternalError)
def test_create_student_item_validation(self, mock_save):
mock_save.side_effect = DatabaseError("Bad things happened")
create_submission(STUDENT_ITEM, ANSWER_ONE)
def test_unicode_enforcement(self):
create_submission(STUDENT_ITEM, "Testing unicode answers.")
submissions = get_submissions(STUDENT_ITEM, 1)
self.assertEqual(u"Testing unicode answers.", submissions[0]["answer"])
def _assert_submission(self, submission, expected_answer, expected_item,
expected_attempt):
self.assertIsNotNone(submission)
self.assertEqual(submission["answer"], expected_answer)
self.assertEqual(submission["student_item"], expected_item)
self.assertEqual(submission["attempt_number"], expected_attempt)
\ No newline at end of file
{
"no_item_type": {
"student_id": "Bad Tim",
"course_id": "451",
"item_id": "2"
},
"no_student_id": {
"course_id": "Course_One",
"item_id": "5",
"item_type": "Peer"
},
"just_student_and_course": {
"student_id": "Tim",
"course_id": "Course_One"
},
"just_student_id": {
"student_id": "Tim"
},
"just_item_id_and_type": {
"item_id": "5",
"item_type": "Peer"
},
"just_course_id": {
"course_id": "Course_One"
},
"just_item_id": {
"item_id": "5"
},
"bad_item_id_empty": {
"student_id": "Tim",
"course_id": "Course_One",
"item_id": "",
"item_type": "Peer"
}
}
\ No newline at end of file
{
"unicode_characters": {
"student_id": "学生",
"course_id": "漢字",
"item_id": "这是中国",
"item_type": "窥视"
},
"basic_student_item": {
"student_id": "Tom",
"course_id": "Demo_Course",
"item_id": "1",
"item_type": "Peer"
}
}
\ No newline at end of file
__author__ = 'stephensanchez'
...@@ -3,3 +3,4 @@ ...@@ -3,3 +3,4 @@
# Third Party Requirements # Third Party Requirements
django==1.4.8 django==1.4.8
djangorestframework==2.3.5
# Grab everything in base requirements # Grab everything in base requirements
-r base.txt -r base.txt
ddt==0.4.0
django-nose==1.2 django-nose==1.2
mock==1.0.1 mock==1.0.1
nose==1.3.0 nose==1.3.0
......
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