Commit 3da5e45f by Stephen Sanchez

Merge pull request #26 from edx/sanchez/workflow_api

WIP: Outlining the API intended for Peer Grading Workflow.
parents 5f0dd7a3 ddbc8775
...@@ -6,9 +6,11 @@ be a lot here, like rubrics and such. ...@@ -6,9 +6,11 @@ be a lot here, like rubrics and such.
from django.db import models from django.db import models
from django.utils.timezone import now from django.utils.timezone import now
from submissions.models import Submission
class PeerEvaluation(models.Model): class PeerEvaluation(models.Model):
# submission = models.ForeignKey(Submission) submission = models.ForeignKey(Submission)
points_earned = models.PositiveIntegerField(default=0) points_earned = models.PositiveIntegerField(default=0)
points_possible = models.PositiveIntegerField(default=0) points_possible = models.PositiveIntegerField(default=0)
scored_at = models.DateTimeField(default=now, db_index=True) scored_at = models.DateTimeField(default=now, db_index=True)
...@@ -16,3 +18,16 @@ class PeerEvaluation(models.Model): ...@@ -16,3 +18,16 @@ class PeerEvaluation(models.Model):
score_type = models.CharField(max_length=2) score_type = models.CharField(max_length=2)
feedback = models.TextField(max_length=10000, default="") feedback = models.TextField(max_length=10000, default="")
def __repr__(self):
return repr(dict(
submission=self.submission,
points_earned=self.points_earned,
points_possible=self.points_possible,
scored_at=self.scored_at,
scorer_id=self.scorer_id,
score_type=self.score_type,
feedback=self.feedback,
))
class Meta:
ordering = ["-scored_at"]
"""
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 openassessment.peer.models import PeerEvaluation
class PeerEvaluationSerializer(serializers.ModelSerializer):
class Meta:
model = PeerEvaluation
fields = (
'submission',
'points_earned',
'points_possible',
'scored_at',
'scorer_id',
'score_type',
'feedback',
)
from ddt import ddt, file_data
from django.db import DatabaseError
import pytz
import datetime
from django.test import TestCase
from nose.tools import raises
from mock import patch
from openassessment.peer import api
from openassessment.peer.models import PeerEvaluation
from submissions.api import create_submission
from submissions.models import Submission
from submissions.tests.test_api import STUDENT_ITEM, ANSWER_ONE
ASSESSMENT_DICT = dict(
points_earned=[1, 0, 3, 2],
points_possible=12,
feedback="Your submission was thrilling.",
)
MONDAY = datetime.datetime(2007, 9, 12, 0, 0, 0, 0, pytz.UTC)
TUESDAY = datetime.datetime(2007, 9, 13, 0, 0, 0, 0, pytz.UTC)
WEDNESDAY = datetime.datetime(2007, 9, 15, 0, 0, 0, 0, pytz.UTC)
THURSDAY = datetime.datetime(2007, 9, 16, 0, 0, 0, 0, pytz.UTC)
@ddt
class TestApi(TestCase):
def test_create_evaluation(self):
submission = create_submission(STUDENT_ITEM, ANSWER_ONE)
evaluation = api.create_evaluation(
submission["uuid"],
STUDENT_ITEM["student_id"],
ASSESSMENT_DICT
)
self._assert_evaluation(evaluation, **ASSESSMENT_DICT)
@file_data('test_valid_evaluations.json')
def test_get_evaluations(self, assessment_dict):
submission = create_submission(STUDENT_ITEM, ANSWER_ONE)
api.create_evaluation(
submission["uuid"],
STUDENT_ITEM["student_id"],
assessment_dict
)
evaluations = api.get_evaluations(submission["uuid"])
self.assertEqual(1, len(evaluations))
self._assert_evaluation(evaluations[0], **assessment_dict)
@file_data('test_valid_evaluations.json')
def test_get_evaluations_with_date(self, assessment_dict):
submission = create_submission(STUDENT_ITEM, ANSWER_ONE)
api.create_evaluation(
submission["uuid"],
STUDENT_ITEM["student_id"],
assessment_dict,
MONDAY
)
evaluations = api.get_evaluations(submission["uuid"])
self.assertEqual(1, len(evaluations))
self._assert_evaluation(evaluations[0], **assessment_dict)
self.assertEqual(evaluations[0]["scored_at"], MONDAY)
def test_student_finished_evaluating(self):
bob = self._create_student_and_submission("Bob", "Bob's answer")
sally = self._create_student_and_submission("Sally", "Sally's answer")
jim = self._create_student_and_submission("Jim", "Jim's answer")
self.assertFalse(api.has_finished_required_evaluating("Tim", 3))
api.create_evaluation(bob["uuid"], "Tim", ASSESSMENT_DICT)
api.create_evaluation(sally["uuid"], "Tim", ASSESSMENT_DICT)
self.assertFalse(api.has_finished_required_evaluating("Tim", 3))
api.create_evaluation(jim["uuid"], "Tim", ASSESSMENT_DICT)
self.assertTrue(api.has_finished_required_evaluating("Tim", 3))
@raises(api.PeerEvaluationRequestError)
def test_bad_configuration(self):
api.has_finished_required_evaluating("Tim", -1)
def test_get_submission_to_evaluate(self):
self._create_student_and_submission("Tim", "Tim's answer", MONDAY)
self._create_student_and_submission("Bob", "Bob's answer", TUESDAY)
self._create_student_and_submission(
"Sally", "Sally's answer", WEDNESDAY
)
self._create_student_and_submission("Jim", "Jim's answer", THURSDAY)
submission = api.get_submission_to_evaluate(STUDENT_ITEM)
self.assertIsNotNone(submission)
self.assertEqual(submission["answer"], u"Bob's answer")
self.assertEqual(submission["student_item"], 2)
self.assertEqual(submission["attempt_number"], 1)
@raises(api.PeerEvaluationWorkflowError)
def test_no_submissions_to_evaluate_for_tim(self):
self._create_student_and_submission("Tim", "Tim's answer", MONDAY)
api.get_submission_to_evaluate(STUDENT_ITEM)
"""
Some Error Checking Tests against DB failures.
"""
@patch.object(Submission.objects, 'get')
@raises(api.PeerEvaluationInternalError)
def test_error_on_evaluation_creation(self, mock_filter):
mock_filter.side_effect = DatabaseError("Bad things happened")
submission = create_submission(STUDENT_ITEM, ANSWER_ONE)
api.create_evaluation(
submission["uuid"],
STUDENT_ITEM["student_id"],
ASSESSMENT_DICT,
MONDAY
)
@patch.object(PeerEvaluation.objects, 'filter')
@raises(api.PeerEvaluationInternalError)
def test_error_on_get_evaluation(self, mock_filter):
submission = create_submission(STUDENT_ITEM, ANSWER_ONE)
api.create_evaluation(
submission["uuid"],
STUDENT_ITEM["student_id"],
ASSESSMENT_DICT,
MONDAY
)
mock_filter.side_effect = DatabaseError("Bad things happened")
api.get_evaluations(submission["uuid"])
@staticmethod
def _create_student_and_submission(student, answer, date=None):
new_student_item = STUDENT_ITEM.copy()
new_student_item["student_id"] = student
return create_submission(new_student_item, answer, date)
def _assert_evaluation(self, evaluation, points_earned, points_possible,
feedback):
self.assertIsNotNone(evaluation)
self.assertEqual(evaluation["points_earned"], sum(points_earned))
self.assertEqual(evaluation["points_possible"], points_possible)
self.assertEqual(evaluation["feedback"], feedback)
\ No newline at end of file
{
"unicode_evaluation": {
"points_earned": [10, 0, 24, 36],
"points_possible": 12,
"feedback": "这是中国"
},
"basic_evaluation": {
"points_earned": [1, 0, 3, 2],
"points_possible": 12,
"feedback": "Your submission was thrilling."
}
}
\ No newline at end of file
...@@ -129,7 +129,7 @@ def create_submission(student_item_dict, answer, submitted_at=None, ...@@ -129,7 +129,7 @@ def create_submission(student_item_dict, answer, submitted_at=None,
u"Submission answer could not be properly decoded to unicode.") u"Submission answer could not be properly decoded to unicode.")
model_kwargs = { model_kwargs = {
"student_item": student_item_model, "student_item": student_item_model.pk,
"answer": answer, "answer": answer,
"attempt_number": attempt_number, "attempt_number": attempt_number,
} }
...@@ -137,12 +137,7 @@ def create_submission(student_item_dict, answer, submitted_at=None, ...@@ -137,12 +137,7 @@ def create_submission(student_item_dict, answer, submitted_at=None,
model_kwargs["submitted_at"] = submitted_at model_kwargs["submitted_at"] = submitted_at
try: try:
# Serializer validation requires the student item primary key, rather submission_serializer = SubmissionSerializer(data=model_kwargs)
# than the student item model itself. Create a copy of the submission
# kwargs and replace the student item model with it's primary key.
validation_data = model_kwargs.copy()
validation_data["student_item"] = student_item_model.pk
submission_serializer = SubmissionSerializer(data=validation_data)
if not submission_serializer.is_valid(): if not submission_serializer.is_valid():
raise SubmissionRequestError(submission_serializer.errors) raise SubmissionRequestError(submission_serializer.errors)
submission_serializer.save() submission_serializer.save()
......
...@@ -5,6 +5,7 @@ different problem types, and is therefore ignorant of ORA workflow. ...@@ -5,6 +5,7 @@ different problem types, and is therefore ignorant of ORA workflow.
""" """
from django.db import models from django.db import models
from django.utils.timezone import now from django.utils.timezone import now
from django_extensions.db.fields import UUIDField
class StudentItem(models.Model): class StudentItem(models.Model):
...@@ -52,6 +53,8 @@ class Submission(models.Model): ...@@ -52,6 +53,8 @@ class Submission(models.Model):
because it makes caching trivial. because it makes caching trivial.
""" """
uuid = UUIDField()
student_item = models.ForeignKey(StudentItem) student_item = models.ForeignKey(StudentItem)
# Which attempt is this? Consecutive Submissions do not necessarily have # Which attempt is this? Consecutive Submissions do not necessarily have
......
""" """
Serializers are created to ensure models do not have to be accessed outside the scope of the Tim APIs. 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 rest_framework import serializers
from submissions.models import StudentItem, Submission, Score from submissions.models import StudentItem, Submission, Score
...@@ -14,7 +15,14 @@ class StudentItemSerializer(serializers.ModelSerializer): ...@@ -14,7 +15,14 @@ class StudentItemSerializer(serializers.ModelSerializer):
class SubmissionSerializer(serializers.ModelSerializer): class SubmissionSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Submission model = Submission
fields = ('student_item', 'attempt_number', 'submitted_at', 'created_at', 'answer') fields = (
'uuid',
'student_item',
'attempt_number',
'submitted_at',
'created_at',
'answer'
)
class ScoreSerializer(serializers.ModelSerializer): class ScoreSerializer(serializers.ModelSerializer):
......
...@@ -5,6 +5,7 @@ from django.db import DatabaseError ...@@ -5,6 +5,7 @@ from django.db import DatabaseError
from django.test import TestCase from django.test import TestCase
from nose.tools import raises from nose.tools import raises
from mock import patch from mock import patch
import pytz
from submissions.api import create_submission, get_submissions, SubmissionRequestError, SubmissionInternalError from submissions.api import create_submission, get_submissions, SubmissionRequestError, SubmissionInternalError
from submissions.models import Submission from submissions.models import Submission
...@@ -62,8 +63,8 @@ class TestApi(TestCase): ...@@ -62,8 +63,8 @@ class TestApi(TestCase):
self._assert_submission(submission, ANSWER_ONE, 1, 1) self._assert_submission(submission, ANSWER_ONE, 1, 1)
def test_get_latest_submission(self): def test_get_latest_submission(self):
past_date = datetime.date(2007, 11, 23) past_date = datetime.datetime(2007, 9, 12, 0, 0, 0, 0, pytz.UTC)
more_recent_date = datetime.date(2011, 10, 15) more_recent_date = datetime.datetime(2007, 9, 13, 0, 0, 0, 0, pytz.UTC)
create_submission(STUDENT_ITEM, ANSWER_ONE, more_recent_date) create_submission(STUDENT_ITEM, ANSWER_ONE, more_recent_date)
create_submission(STUDENT_ITEM, ANSWER_TWO, past_date) create_submission(STUDENT_ITEM, ANSWER_TWO, past_date)
......
...@@ -24,6 +24,8 @@ Submissions ...@@ -24,6 +24,8 @@ Submissions
Peer Assessment Peer Assessment
*************** ***************
.. automodule:: openassessment.peer.api
:members:
Django Apps Django Apps
----------- -----------
......
...@@ -7,3 +7,4 @@ django==1.4.8 ...@@ -7,3 +7,4 @@ django==1.4.8
django-extensions==1.3.3 django-extensions==1.3.3
djangorestframework==2.3.5 djangorestframework==2.3.5
Mako==0.9.1 Mako==0.9.1
pytz==2013.9
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