Commit 6de67732 by Will Daly

Allow JSON-serializable answers

parent e7d40e5d
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
<h3 class="submission__answer__display__title">Your Submitted Response</h3> <h3 class="submission__answer__display__title">Your Submitted Response</h3>
<div class="submission__answer__display__content"> <div class="submission__answer__display__content">
{{ student_submission.answer }} {{ student_submission.answer.text|linebreaks }}
</div> </div>
</article> </article>
......
...@@ -56,7 +56,7 @@ ...@@ -56,7 +56,7 @@
</header> </header>
<div class="peer-assessment__display__response"> <div class="peer-assessment__display__response">
{{ peer_submission.answer|linebreaks }} {{ peer_submission.answer.text|linebreaks }}
</div> </div>
</div> </div>
......
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
<h3 class="submission__answer__display__title">Your Submitted Response</h3> <h3 class="submission__answer__display__title">Your Submitted Response</h3>
<div class="submission__answer__display__content"> <div class="submission__answer__display__content">
{{ student_submission.answer|linebreaks }} {{ student_submission.answer.text|linebreaks }}
</div> </div>
</article> </article>
</div> </div>
......
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
<h3 class="submission__answer__display__title">Your Submitted Response</h3> <h3 class="submission__answer__display__title">Your Submitted Response</h3>
<div class="submission__answer__display__content"> <div class="submission__answer__display__content">
{{ student_submission.answer|linebreaks }} {{ student_submission.answer.text|linebreaks }}
</div> </div>
</article> </article>
</div> </div>
......
...@@ -39,7 +39,7 @@ ...@@ -39,7 +39,7 @@
</header> </header>
<div class="self-assessment__display__response"> <div class="self-assessment__display__response">
{{ self_submission.answer|linebreaks }} {{ self_submission.answer.text|linebreaks }}
</div> </div>
</article> </article>
......
...@@ -112,7 +112,12 @@ class SubmissionMixin(object): ...@@ -112,7 +112,12 @@ class SubmissionMixin(object):
return {'success': False, 'msg': _(u"Missing required key 'submission'")} return {'success': False, 'msg': _(u"Missing required key 'submission'")}
def create_submission(self, student_item_dict, student_sub): 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"]) workflow = workflow_api.create_workflow(submission["uuid"])
self.submission_uuid = submission["uuid"] self.submission_uuid = submission["uuid"]
return submission return submission
......
...@@ -68,13 +68,11 @@ class TestPeerAssessment(XBlockHandlerTestCase): ...@@ -68,13 +68,11 @@ class TestPeerAssessment(XBlockHandlerTestCase):
request.params = {} request.params = {}
peer_response = xblock.render_peer_assessment(request) peer_response = xblock.render_peer_assessment(request)
self.assertIsNotNone(peer_response) 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. #Validate Peer Rendering.
self.assertIn("Sally".encode('utf-8'), peer_response.body) self.assertIn("Sally".encode('utf-8'), peer_response.body)
@scenario('data/peer_assessment_scenario.xml', user_id='Bob') @scenario('data/peer_assessment_scenario.xml', user_id='Bob')
def test_assess_handler(self, xblock): def test_assess_handler(self, xblock):
......
...@@ -5,7 +5,7 @@ from submissions.models import Score, StudentItem, Submission ...@@ -5,7 +5,7 @@ from submissions.models import Score, StudentItem, Submission
class SubmissionAdmin(admin.ModelAdmin): class SubmissionAdmin(admin.ModelAdmin):
list_display = ( list_display = (
'student_item', 'uuid', 'attempt_number', 'submitted_at', 'created_at', 'student_item', 'uuid', 'attempt_number', 'submitted_at', 'created_at',
'answer', 'scores' 'raw_answer', 'scores'
) )
def scores(self, obj): def scores(self, obj):
......
...@@ -8,7 +8,9 @@ import logging ...@@ -8,7 +8,9 @@ import logging
from django.db import DatabaseError from django.db import DatabaseError
from django.utils.encoding import force_unicode 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 from submissions.models import Submission, StudentItem, Score, ScoreSummary
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -67,7 +69,7 @@ def create_submission(student_item_dict, answer, submitted_at=None, ...@@ -67,7 +69,7 @@ def create_submission(student_item_dict, answer, submitted_at=None,
student_item_dict (dict): The student_item this student_item_dict (dict): The student_item this
submission is associated with. This is used to determine which submission is associated with. This is used to determine which
course, student, and location this submission belongs to. 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. submitted_at (datetime): The date in which this submission was submitted.
If not specified, defaults to the current date. If not specified, defaults to the current date.
attempt_number (int): A student may be able to submit multiple attempts 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, ...@@ -122,12 +124,6 @@ def create_submission(student_item_dict, answer, submitted_at=None,
raise SubmissionInternalError(error_message) raise SubmissionInternalError(error_message)
attempt_number = submissions[0].attempt_number + 1 if submissions else 1 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 = { model_kwargs = {
"student_item": student_item_model.pk, "student_item": student_item_model.pk,
"answer": answer, "answer": answer,
...@@ -143,6 +139,11 @@ def create_submission(student_item_dict, answer, submitted_at=None, ...@@ -143,6 +139,11 @@ def create_submission(student_item_dict, answer, submitted_at=None,
submission_serializer.save() submission_serializer.save()
return submission_serializer.data 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: except DatabaseError:
error_message = u"An error occurred while creating submission {} for student item: {}".format( error_message = u"An error occurred while creating submission {} for student item: {}".format(
model_kwargs, model_kwargs,
...@@ -181,6 +182,7 @@ def get_submission(submission_uuid): ...@@ -181,6 +182,7 @@ def get_submission(submission_uuid):
try: try:
submission = Submission.objects.get(uuid=submission_uuid) submission = Submission.objects.get(uuid=submission_uuid)
return SubmissionSerializer(submission).data
except Submission.DoesNotExist: except Submission.DoesNotExist:
raise SubmissionNotFoundError( raise SubmissionNotFoundError(
u"No submission matching uuid {}".format(submission_uuid) u"No submission matching uuid {}".format(submission_uuid)
...@@ -191,8 +193,6 @@ def get_submission(submission_uuid): ...@@ -191,8 +193,6 @@ def get_submission(submission_uuid):
logger.exception(err_msg) logger.exception(err_msg)
raise SubmissionInternalError(err_msg) raise SubmissionInternalError(err_msg)
return SubmissionSerializer(submission).data
def get_submission_and_student(uuid): def get_submission_and_student(uuid):
""" """
...@@ -211,8 +211,13 @@ def get_submission_and_student(uuid): ...@@ -211,8 +211,13 @@ def get_submission_and_student(uuid):
return None return None
# There is probably a more idiomatic way to do this using the Django REST framework # There is probably a more idiomatic way to do this using the Django REST framework
submission_dict = SubmissionSerializer(submission).data try:
submission_dict['student_item'] = StudentItemSerializer(submission.student_item).data 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 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: ...@@ -9,6 +9,7 @@ need to then generate a matching migration for it using:
./manage.py schemamigration submissions --auto ./manage.py schemamigration submissions --auto
""" """
import json
import logging import logging
from django.db import models, DatabaseError from django.db import models, DatabaseError
...@@ -84,8 +85,8 @@ class Submission(models.Model): ...@@ -84,8 +85,8 @@ class Submission(models.Model):
# When this row was created. # When this row was created.
created_at = models.DateTimeField(editable=False, default=now, db_index=True) created_at = models.DateTimeField(editable=False, default=now, db_index=True)
# The actual answer, assumed to be a JSON string # The answer (JSON-serialized)
answer = models.TextField(blank=True) raw_answer = models.TextField(blank=True)
def __repr__(self): def __repr__(self):
return repr(dict( return repr(dict(
...@@ -94,7 +95,7 @@ class Submission(models.Model): ...@@ -94,7 +95,7 @@ class Submission(models.Model):
attempt_number=self.attempt_number, attempt_number=self.attempt_number,
submitted_at=self.submitted_at, submitted_at=self.submitted_at,
created_at=self.created_at, created_at=self.created_at,
answer=self.answer, raw_answer=self.raw_answer,
)) ))
class Meta: class Meta:
......
...@@ -2,10 +2,59 @@ ...@@ -2,10 +2,59 @@
Serializers are created to ensure models do not have to be accessed outside the Serializers are created to ensure models do not have to be accessed outside the
scope of the Tim APIs. scope of the Tim APIs.
""" """
import json
from rest_framework import serializers from rest_framework import serializers
from submissions.models import StudentItem, Submission, Score 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 StudentItemSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = StudentItem model = StudentItem
...@@ -14,6 +63,8 @@ class StudentItemSerializer(serializers.ModelSerializer): ...@@ -14,6 +63,8 @@ class StudentItemSerializer(serializers.ModelSerializer):
class SubmissionSerializer(serializers.ModelSerializer): class SubmissionSerializer(serializers.ModelSerializer):
answer = JsonField(source='raw_answer')
class Meta: class Meta:
model = Submission model = Submission
fields = ( fields = (
...@@ -22,6 +73,8 @@ class SubmissionSerializer(serializers.ModelSerializer): ...@@ -22,6 +73,8 @@ class SubmissionSerializer(serializers.ModelSerializer):
'attempt_number', 'attempt_number',
'submitted_at', 'submitted_at',
'created_at', 'created_at',
# Computed
'answer', 'answer',
) )
......
...@@ -41,6 +41,7 @@ class TestSubmissionsApi(TestCase): ...@@ -41,6 +41,7 @@ class TestSubmissionsApi(TestCase):
student_item = self._get_student_item(STUDENT_ITEM) student_item = self._get_student_item(STUDENT_ITEM)
self._assert_submission(submission, ANSWER_ONE, student_item.pk, 1) self._assert_submission(submission, ANSWER_ONE, student_item.pk, 1)
def test_get_submission_and_student(self): def test_get_submission_and_student(self):
submission = api.create_submission(STUDENT_ITEM, ANSWER_ONE) submission = api.create_submission(STUDENT_ITEM, ANSWER_ONE)
...@@ -143,6 +144,24 @@ class TestSubmissionsApi(TestCase): ...@@ -143,6 +144,24 @@ class TestSubmissionsApi(TestCase):
mock_filter.side_effect = DatabaseError("Bad things happened") mock_filter.side_effect = DatabaseError("Bad things happened")
api.create_submission(STUDENT_ITEM, ANSWER_ONE) 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') @patch.object(StudentItemSerializer, 'save')
@raises(api.SubmissionInternalError) @raises(api.SubmissionInternalError)
def test_create_student_item_validation(self, mock_save): 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