Commit 6de67732 by Will Daly

Allow JSON-serializable answers

parent e7d40e5d
......@@ -19,7 +19,7 @@
<h3 class="submission__answer__display__title">Your Submitted Response</h3>
<div class="submission__answer__display__content">
{{ student_submission.answer }}
{{ student_submission.answer.text|linebreaks }}
</div>
</article>
......
......@@ -56,7 +56,7 @@
</header>
<div class="peer-assessment__display__response">
{{ peer_submission.answer|linebreaks }}
{{ peer_submission.answer.text|linebreaks }}
</div>
</div>
......
......@@ -22,7 +22,7 @@
<h3 class="submission__answer__display__title">Your Submitted Response</h3>
<div class="submission__answer__display__content">
{{ student_submission.answer|linebreaks }}
{{ student_submission.answer.text|linebreaks }}
</div>
</article>
</div>
......
......@@ -22,7 +22,7 @@
<h3 class="submission__answer__display__title">Your Submitted Response</h3>
<div class="submission__answer__display__content">
{{ student_submission.answer|linebreaks }}
{{ student_submission.answer.text|linebreaks }}
</div>
</article>
</div>
......
......@@ -39,7 +39,7 @@
</header>
<div class="self-assessment__display__response">
{{ self_submission.answer|linebreaks }}
{{ self_submission.answer.text|linebreaks }}
</div>
</article>
......
......@@ -112,7 +112,12 @@ class SubmissionMixin(object):
return {'success': False, 'msg': _(u"Missing required key 'submission'")}
def create_submission(self, student_item_dict, student_sub):
submission = api.create_submission(student_item_dict, student_sub)
# Store the student's response text in a JSON-encodable dict
# so that later we can add additional response fields.
student_sub_dict = {'text': student_sub}
submission = api.create_submission(student_item_dict, student_sub_dict)
workflow = workflow_api.create_workflow(submission["uuid"])
self.submission_uuid = submission["uuid"]
return submission
......
......@@ -68,13 +68,11 @@ class TestPeerAssessment(XBlockHandlerTestCase):
request.params = {}
peer_response = xblock.render_peer_assessment(request)
self.assertIsNotNone(peer_response)
self.assertNotIn(submission["answer"].encode('utf-8'), peer_response.body)
self.assertNotIn(submission["answer"]["text"].encode('utf-8'), peer_response.body)
#Validate Peer Rendering.
self.assertIn("Sally".encode('utf-8'), peer_response.body)
@scenario('data/peer_assessment_scenario.xml', user_id='Bob')
def test_assess_handler(self, xblock):
......
......@@ -5,7 +5,7 @@ from submissions.models import Score, StudentItem, Submission
class SubmissionAdmin(admin.ModelAdmin):
list_display = (
'student_item', 'uuid', 'attempt_number', 'submitted_at', 'created_at',
'answer', 'scores'
'raw_answer', 'scores'
)
def scores(self, obj):
......
......@@ -8,7 +8,9 @@ import logging
from django.db import DatabaseError
from django.utils.encoding import force_unicode
from submissions.serializers import SubmissionSerializer, StudentItemSerializer, ScoreSerializer
from submissions.serializers import (
SubmissionSerializer, StudentItemSerializer, ScoreSerializer, JsonFieldError
)
from submissions.models import Submission, StudentItem, Score, ScoreSummary
logger = logging.getLogger(__name__)
......@@ -67,7 +69,7 @@ def create_submission(student_item_dict, answer, submitted_at=None,
student_item_dict (dict): The student_item this
submission is associated with. This is used to determine which
course, student, and location this submission belongs to.
answer (str): The answer given by the student to be assessed.
answer (JSON-serializable): The answer given by the student to be assessed.
submitted_at (datetime): The date in which this submission was submitted.
If not specified, defaults to the current date.
attempt_number (int): A student may be able to submit multiple attempts
......@@ -122,12 +124,6 @@ def create_submission(student_item_dict, answer, submitted_at=None,
raise SubmissionInternalError(error_message)
attempt_number = submissions[0].attempt_number + 1 if submissions else 1
try:
answer = force_unicode(answer)
except UnicodeDecodeError:
raise SubmissionRequestError(
u"Submission answer could not be properly decoded to unicode.")
model_kwargs = {
"student_item": student_item_model.pk,
"answer": answer,
......@@ -143,6 +139,11 @@ def create_submission(student_item_dict, answer, submitted_at=None,
submission_serializer.save()
return submission_serializer.data
except JsonFieldError:
error_message = u"Could not serialize JSON field in submission {} for student item {}".format(
model_kwargs, student_item_dict
)
raise SubmissionRequestError(error_message)
except DatabaseError:
error_message = u"An error occurred while creating submission {} for student item: {}".format(
model_kwargs,
......@@ -181,6 +182,7 @@ def get_submission(submission_uuid):
try:
submission = Submission.objects.get(uuid=submission_uuid)
return SubmissionSerializer(submission).data
except Submission.DoesNotExist:
raise SubmissionNotFoundError(
u"No submission matching uuid {}".format(submission_uuid)
......@@ -191,8 +193,6 @@ def get_submission(submission_uuid):
logger.exception(err_msg)
raise SubmissionInternalError(err_msg)
return SubmissionSerializer(submission).data
def get_submission_and_student(uuid):
"""
......@@ -211,8 +211,13 @@ def get_submission_and_student(uuid):
return None
# There is probably a more idiomatic way to do this using the Django REST framework
submission_dict = SubmissionSerializer(submission).data
submission_dict['student_item'] = StudentItemSerializer(submission.student_item).data
try:
submission_dict = SubmissionSerializer(submission).data
submission_dict['student_item'] = StudentItemSerializer(submission.student_item).data
except Exception as ex:
err_msg = "Could not get submission due to error: {}".format(ex)
logger.exception(err_msg)
raise SubmissionInternalError(err_msg)
return submission_dict
......
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Deleting field 'Submission.answer'
db.delete_column('submissions_submission', 'answer')
# Adding field 'Submission.raw_answer'
db.add_column('submissions_submission', 'raw_answer',
self.gf('django.db.models.fields.TextField')(default='', blank=True),
keep_default=False)
def backwards(self, orm):
# Adding field 'Submission.answer'
db.add_column('submissions_submission', 'answer',
self.gf('django.db.models.fields.TextField')(default='', blank=True),
keep_default=False)
# Deleting field 'Submission.raw_answer'
db.delete_column('submissions_submission', 'raw_answer')
models = {
'submissions.score': {
'Meta': {'object_name': 'Score'},
'created_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'points_earned': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
'points_possible': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
'student_item': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['submissions.StudentItem']"}),
'submission': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['submissions.Submission']", 'null': 'True'})
},
'submissions.scoresummary': {
'Meta': {'object_name': 'ScoreSummary'},
'highest': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'to': "orm['submissions.Score']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'latest': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'to': "orm['submissions.Score']"}),
'student_item': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['submissions.StudentItem']", 'unique': 'True'})
},
'submissions.studentitem': {
'Meta': {'unique_together': "(('course_id', 'student_id', 'item_id'),)", 'object_name': 'StudentItem'},
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'item_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'item_type': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'student_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'})
},
'submissions.submission': {
'Meta': {'ordering': "['-submitted_at', '-id']", 'object_name': 'Submission'},
'attempt_number': ('django.db.models.fields.PositiveIntegerField', [], {}),
'created_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'raw_answer': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'student_item': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['submissions.StudentItem']"}),
'submitted_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'db_index': 'True'}),
'uuid': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '36', 'blank': 'True'})
}
}
complete_apps = ['submissions']
\ No newline at end of file
......@@ -9,6 +9,7 @@ need to then generate a matching migration for it using:
./manage.py schemamigration submissions --auto
"""
import json
import logging
from django.db import models, DatabaseError
......@@ -84,8 +85,8 @@ class Submission(models.Model):
# When this row was created.
created_at = models.DateTimeField(editable=False, default=now, db_index=True)
# The actual answer, assumed to be a JSON string
answer = models.TextField(blank=True)
# The answer (JSON-serialized)
raw_answer = models.TextField(blank=True)
def __repr__(self):
return repr(dict(
......@@ -94,7 +95,7 @@ class Submission(models.Model):
attempt_number=self.attempt_number,
submitted_at=self.submitted_at,
created_at=self.created_at,
answer=self.answer,
raw_answer=self.raw_answer,
))
class Meta:
......
......@@ -2,10 +2,59 @@
Serializers are created to ensure models do not have to be accessed outside the
scope of the Tim APIs.
"""
import json
from rest_framework import serializers
from submissions.models import StudentItem, Submission, Score
class JsonFieldError(Exception):
"""
An error occurred while serializing/deserializing JSON.
"""
pass
class JsonField(serializers.WritableField):
"""
JSON-serializable field.
"""
def to_native(self, obj):
"""
Deserialize the JSON string.
Args:
obj (str): The JSON string stored in the database.
Returns:
JSON-serializable
Raises:
JsonFieldError: The field could not be deserialized.
"""
try:
return json.loads(obj)
except (TypeError, ValueError):
raise JsonFieldError(u"Could not deserialize as JSON: {}".format(obj))
def from_native(self, data):
"""
Serialize an object to JSON.
Args:
data (JSON-serializable): The data to serialize.
Returns:
str
Raises:
ValueError: The data could not be serialized as JSON.
"""
try:
return json.dumps(data)
except (TypeError, ValueError):
raise JsonFieldError(u"Could not serialize as JSON: {}".format(data))
class StudentItemSerializer(serializers.ModelSerializer):
class Meta:
model = StudentItem
......@@ -14,6 +63,8 @@ class StudentItemSerializer(serializers.ModelSerializer):
class SubmissionSerializer(serializers.ModelSerializer):
answer = JsonField(source='raw_answer')
class Meta:
model = Submission
fields = (
......@@ -22,6 +73,8 @@ class SubmissionSerializer(serializers.ModelSerializer):
'attempt_number',
'submitted_at',
'created_at',
# Computed
'answer',
)
......
......@@ -41,6 +41,7 @@ class TestSubmissionsApi(TestCase):
student_item = self._get_student_item(STUDENT_ITEM)
self._assert_submission(submission, ANSWER_ONE, student_item.pk, 1)
def test_get_submission_and_student(self):
submission = api.create_submission(STUDENT_ITEM, ANSWER_ONE)
......@@ -143,6 +144,24 @@ class TestSubmissionsApi(TestCase):
mock_filter.side_effect = DatabaseError("Bad things happened")
api.create_submission(STUDENT_ITEM, ANSWER_ONE)
def test_create_non_json_answer(self):
with self.assertRaises(api.SubmissionRequestError):
api.create_submission(STUDENT_ITEM, datetime.datetime.now())
def test_load_non_json_answer(self):
# This should never happen, if folks are using the public API.
# Create a submission with a raw answer that is NOT valid JSON
submission = api.create_submission(STUDENT_ITEM, ANSWER_ONE)
sub_model = Submission.objects.get(uuid=submission['uuid'])
sub_model.raw_answer = ''
sub_model.save()
with self.assertRaises(api.SubmissionInternalError):
api.get_submission(sub_model.uuid)
with self.assertRaises(api.SubmissionInternalError):
api.get_submission_and_student(sub_model.uuid)
@patch.object(StudentItemSerializer, 'save')
@raises(api.SubmissionInternalError)
def test_create_student_item_validation(self, mock_save):
......
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