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).
"""
def create_evaluation(submission_id, score, rubric):
pass
def get_evaluations(submission_id):
pass
def get_submission_to_evaluate(student_item, scorer_student_id):
pass
......@@ -6,8 +6,8 @@ Student submission:
* XBlock creates a Submission
* 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
* when openresponse is satistifed that it has collected enough information to
* openassessment can listen for that signal if it wants, or query itself on demand
* when openassessment is satisfied that it has collected enough information to
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.
......@@ -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.utils.timezone import now
class StudentItem(models.Model):
"""Represents a single item for a single course for a single user.
......@@ -39,13 +40,18 @@ class StudentItem(models.Model):
# What kind of problem is this? The XBlock tag if it's an XBlock
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:
unique_together = (
# For integrity reasons, and looking up all of a student's items
("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):
# The actual answer, assumed to be a JSON string
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):
"""What the user scored for a given StudentItem.
......@@ -85,3 +103,12 @@ class Score(models.Model):
points_possible = models.PositiveIntegerField(default=0)
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 @@
# Third Party Requirements
django==1.4.8
djangorestframework==2.3.5
# Grab everything in base requirements
-r base.txt
ddt==0.4.0
django-nose==1.2
mock==1.0.1
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