Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-ora2
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-ora2
Commits
6de67732
Commit
6de67732
authored
Mar 18, 2014
by
Will Daly
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Allow JSON-serializable answers
parent
e7d40e5d
Hide whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
174 additions
and
25 deletions
+174
-25
apps/openassessment/templates/openassessmentblock/grade/oa_grade_complete.html
+1
-1
apps/openassessment/templates/openassessmentblock/peer/oa_peer_assessment.html
+1
-1
apps/openassessment/templates/openassessmentblock/response/oa_response_graded.html
+1
-1
apps/openassessment/templates/openassessmentblock/response/oa_response_submitted.html
+1
-1
apps/openassessment/templates/openassessmentblock/self/oa_self_assessment.html
+1
-1
apps/openassessment/xblock/submission_mixin.py
+6
-1
apps/openassessment/xblock/test/test_peer.py
+1
-3
apps/submissions/admin.py
+1
-1
apps/submissions/api.py
+17
-12
apps/submissions/migrations/0003_auto__del_field_submission_answer__add_field_submission_raw_answer.py
+68
-0
apps/submissions/models.py
+4
-3
apps/submissions/serializers.py
+53
-0
apps/submissions/tests/test_api.py
+19
-0
No files found.
apps/openassessment/templates/openassessmentblock/grade/oa_grade_complete.html
View file @
6de67732
...
...
@@ -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>
...
...
apps/openassessment/templates/openassessmentblock/peer/oa_peer_assessment.html
View file @
6de67732
...
...
@@ -56,7 +56,7 @@
</header>
<div
class=
"peer-assessment__display__response"
>
{{ peer_submission.answer|linebreaks }}
{{ peer_submission.answer
.text
|linebreaks }}
</div>
</div>
...
...
apps/openassessment/templates/openassessmentblock/response/oa_response_graded.html
View file @
6de67732
...
...
@@ -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>
...
...
apps/openassessment/templates/openassessmentblock/response/oa_response_submitted.html
View file @
6de67732
...
...
@@ -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>
...
...
apps/openassessment/templates/openassessmentblock/self/oa_self_assessment.html
View file @
6de67732
...
...
@@ -39,7 +39,7 @@
</header>
<div
class=
"self-assessment__display__response"
>
{{ self_submission.answer|linebreaks }}
{{ self_submission.answer
.text
|linebreaks }}
</div>
</article>
...
...
apps/openassessment/xblock/submission_mixin.py
View file @
6de67732
...
...
@@ -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
...
...
apps/openassessment/xblock/test/test_peer.py
View file @
6de67732
...
...
@@ -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
):
...
...
apps/submissions/admin.py
View file @
6de67732
...
...
@@ -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
):
...
...
apps/submissions/api.py
View file @
6de67732
...
...
@@ -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
...
...
apps/submissions/migrations/0003_auto__del_field_submission_answer__add_field_submission_raw_answer.py
0 → 100644
View file @
6de67732
# -*- 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
apps/submissions/models.py
View file @
6de67732
...
...
@@ -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 a
ctual answer, assumed to be a JSON string
answer
=
models
.
TextField
(
blank
=
True
)
# The a
nswer (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
:
...
...
apps/submissions/serializers.py
View file @
6de67732
...
...
@@ -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'
,
)
...
...
apps/submissions/tests/test_api.py
View file @
6de67732
...
...
@@ -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
):
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment