Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-submissions
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-submissions
Commits
87aaf568
Commit
87aaf568
authored
Oct 30, 2015
by
Diana Huang
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #25 from edx/diana/score-annotations
Add the ability to annotate scores.
parents
14aeaa9e
c16512e4
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
153 additions
and
6 deletions
+153
-6
AUTHORS
+1
-0
submissions/api.py
+19
-3
submissions/migrations/0006_auto__add_scoreannotation__chg_field_studentitem_student_id.py
+80
-0
submissions/models.py
+27
-1
submissions/tests/test_api.py
+26
-2
No files found.
AUTHORS
View file @
87aaf568
...
@@ -3,3 +3,4 @@ Will Daly <will@edx.org>
...
@@ -3,3 +3,4 @@ Will Daly <will@edx.org>
David Ormsbee <dave@edx.org>
David Ormsbee <dave@edx.org>
Stephen Sanchez <steve@edx.org>
Stephen Sanchez <steve@edx.org>
Phil McGachey <phil_mcgachey@harvard.edu>
Phil McGachey <phil_mcgachey@harvard.edu>
Diana Huang <dkh@edx.org>
submissions/api.py
View file @
87aaf568
...
@@ -10,13 +10,13 @@ import json
...
@@ -10,13 +10,13 @@ import json
from
django.conf
import
settings
from
django.conf
import
settings
from
django.core.cache
import
cache
from
django.core.cache
import
cache
from
django.db
import
IntegrityError
,
DatabaseError
from
django.db
import
IntegrityError
,
DatabaseError
,
transaction
from
dogapi
import
dog_stats_api
from
dogapi
import
dog_stats_api
from
submissions.serializers
import
(
from
submissions.serializers
import
(
SubmissionSerializer
,
StudentItemSerializer
,
ScoreSerializer
SubmissionSerializer
,
StudentItemSerializer
,
ScoreSerializer
)
)
from
submissions.models
import
Submission
,
StudentItem
,
Score
,
ScoreSummary
,
score_set
,
score_reset
from
submissions.models
import
Submission
,
StudentItem
,
Score
,
ScoreSummary
,
ScoreAnnotation
,
score_set
,
score_reset
logger
=
logging
.
getLogger
(
"submissions.api"
)
logger
=
logging
.
getLogger
(
"submissions.api"
)
...
@@ -698,7 +698,8 @@ def reset_score(student_id, course_id, item_id):
...
@@ -698,7 +698,8 @@ def reset_score(student_id, course_id, item_id):
logger
.
info
(
msg
)
logger
.
info
(
msg
)
def
set_score
(
submission_uuid
,
points_earned
,
points_possible
):
def
set_score
(
submission_uuid
,
points_earned
,
points_possible
,
annotation_creator
=
None
,
annotation_type
=
None
,
annotation_reason
=
None
):
"""Set a score for a particular submission.
"""Set a score for a particular submission.
Sets the score for a particular submission. This score is calculated
Sets the score for a particular submission. This score is calculated
...
@@ -709,6 +710,11 @@ def set_score(submission_uuid, points_earned, points_possible):
...
@@ -709,6 +710,11 @@ def set_score(submission_uuid, points_earned, points_possible):
points_earned (int): The earned points for this submission.
points_earned (int): The earned points for this submission.
points_possible (int): The total points possible for this particular student item.
points_possible (int): The total points possible for this particular student item.
annotation_creator (str): An optional field for recording who gave this particular score
annotation_type (str): An optional field for recording what type of annotation should be created,
e.g. "staff_override".
annotation_reason (str): An optional field for recording why this score was set to its value.
Returns:
Returns:
None
None
...
@@ -761,9 +767,19 @@ def set_score(submission_uuid, points_earned, points_possible):
...
@@ -761,9 +767,19 @@ def set_score(submission_uuid, points_earned, points_possible):
# even though we cannot retrieve it.
# even though we cannot retrieve it.
# In this case, we assume that someone else has already created
# In this case, we assume that someone else has already created
# a score summary and ignore the error.
# a score summary and ignore the error.
# TODO: once we're using Django 1.8, use transactions to ensure that these
# two models are saved at the same time.
try
:
try
:
score_model
=
score
.
save
()
score_model
=
score
.
save
()
_log_score
(
score_model
)
_log_score
(
score_model
)
if
annotation_creator
is
not
None
:
score_annotation
=
ScoreAnnotation
(
score
=
score_model
,
creator
=
annotation_creator
,
annotation_type
=
annotation_type
,
reason
=
annotation_reason
)
score_annotation
.
save
()
# Send a signal out to any listeners who are waiting for scoring events.
# Send a signal out to any listeners who are waiting for scoring events.
score_set
.
send
(
score_set
.
send
(
sender
=
None
,
sender
=
None
,
...
...
submissions/migrations/0006_auto__add_scoreannotation__chg_field_studentitem_student_id.py
0 → 100644
View file @
87aaf568
# -*- coding: utf-8 -*-
from
south.utils
import
datetime_utils
as
datetime
from
south.db
import
db
from
south.v2
import
SchemaMigration
from
django.db
import
models
class
Migration
(
SchemaMigration
):
def
forwards
(
self
,
orm
):
# Adding model 'ScoreAnnotation'
db
.
create_table
(
'submissions_scoreannotation'
,
(
(
'id'
,
self
.
gf
(
'django.db.models.fields.AutoField'
)(
primary_key
=
True
)),
(
'score'
,
self
.
gf
(
'django.db.models.fields.related.ForeignKey'
)(
to
=
orm
[
'submissions.Score'
])),
(
'annotation_type'
,
self
.
gf
(
'django.db.models.fields.CharField'
)(
max_length
=
255
,
db_index
=
True
)),
(
'creator'
,
self
.
gf
(
'submissions.models.AnonymizedUserIDField'
)(
max_length
=
255
,
db_index
=
True
)),
(
'reason'
,
self
.
gf
(
'django.db.models.fields.TextField'
)()),
))
db
.
send_create_signal
(
'submissions'
,
[
'ScoreAnnotation'
])
# Changing field 'StudentItem.student_id'
db
.
alter_column
(
'submissions_studentitem'
,
'student_id'
,
self
.
gf
(
'submissions.models.AnonymizedUserIDField'
)(
max_length
=
255
))
def
backwards
(
self
,
orm
):
# Deleting model 'ScoreAnnotation'
db
.
delete_table
(
'submissions_scoreannotation'
)
# Changing field 'StudentItem.student_id'
db
.
alter_column
(
'submissions_studentitem'
,
'student_id'
,
self
.
gf
(
'django.db.models.fields.CharField'
)(
max_length
=
255
))
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'
}),
'reset'
:
(
'django.db.models.fields.BooleanField'
,
[],
{
'default'
:
'False'
}),
'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.scoreannotation'
:
{
'Meta'
:
{
'object_name'
:
'ScoreAnnotation'
},
'annotation_type'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
,
'db_index'
:
'True'
}),
'creator'
:
(
'submissions.models.AnonymizedUserIDField'
,
[],
{
'max_length'
:
'255'
,
'db_index'
:
'True'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'reason'
:
(
'django.db.models.fields.TextField'
,
[],
{}),
'score'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'to'
:
"orm['submissions.Score']"
})
},
'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'
:
(
'submissions.models.AnonymizedUserIDField'
,
[],
{
'max_length'
:
'255'
,
'db_index'
:
'True'
})
},
'submissions.submission'
:
{
'Meta'
:
{
'ordering'
:
"['-submitted_at', '-id']"
,
'object_name'
:
'Submission'
},
'answer'
:
(
'jsonfield.fields.JSONField'
,
[],
{
'db_column'
:
"'raw_answer'"
,
'blank'
:
'True'
}),
'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'
}),
'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
submissions/models.py
View file @
87aaf568
...
@@ -11,6 +11,7 @@ need to then generate a matching migration for it using:
...
@@ -11,6 +11,7 @@ need to then generate a matching migration for it using:
"""
"""
import
logging
import
logging
from
south.modelsinspector
import
add_introspection_rules
from
django.db
import
models
,
DatabaseError
from
django.db
import
models
,
DatabaseError
from
django.db.models.signals
import
post_save
from
django.db.models.signals
import
post_save
from
django.dispatch
import
receiver
,
Signal
from
django.dispatch
import
receiver
,
Signal
...
@@ -21,6 +22,9 @@ from jsonfield import JSONField
...
@@ -21,6 +22,9 @@ from jsonfield import JSONField
logger
=
logging
.
getLogger
(
__name__
)
logger
=
logging
.
getLogger
(
__name__
)
add_introspection_rules
([],
[
"submissions
\
.models
\
.AnonymizedUserIDField"
])
# Signal to inform listeners that a score has been changed
# Signal to inform listeners that a score has been changed
score_set
=
Signal
(
providing_args
=
[
score_set
=
Signal
(
providing_args
=
[
'points_possible'
,
'points_earned'
,
'anonymous_user_id'
,
'points_possible'
,
'points_earned'
,
'anonymous_user_id'
,
...
@@ -33,6 +37,16 @@ score_reset = Signal(
...
@@ -33,6 +37,16 @@ score_reset = Signal(
)
)
class
AnonymizedUserIDField
(
models
.
CharField
):
""" Field for storing anonymized user ids. """
description
=
"The anonymized User ID that the XBlock sees"
def
__init__
(
self
,
*
args
,
**
kwargs
):
kwargs
[
'max_length'
]
=
255
kwargs
[
'db_index'
]
=
True
super
(
AnonymizedUserIDField
,
self
)
.
__init__
(
*
args
,
**
kwargs
)
class
StudentItem
(
models
.
Model
):
class
StudentItem
(
models
.
Model
):
"""Represents a single item for a single course for a single user.
"""Represents a single item for a single course for a single user.
...
@@ -41,7 +55,7 @@ class StudentItem(models.Model):
...
@@ -41,7 +55,7 @@ class StudentItem(models.Model):
"""
"""
# The anonymized Student ID that the XBlock sees, not their real ID.
# The anonymized Student ID that the XBlock sees, not their real ID.
student_id
=
models
.
CharField
(
max_length
=
255
,
blank
=
False
,
db_index
=
True
)
student_id
=
AnonymizedUserIDField
(
)
# Not sure yet whether these are legacy course_ids or new course_ids
# Not sure yet whether these are legacy course_ids or new course_ids
course_id
=
models
.
CharField
(
max_length
=
255
,
blank
=
False
,
db_index
=
True
)
course_id
=
models
.
CharField
(
max_length
=
255
,
blank
=
False
,
db_index
=
True
)
...
@@ -274,3 +288,15 @@ class ScoreSummary(models.Model):
...
@@ -274,3 +288,15 @@ class ScoreSummary(models.Model):
u"Error while updating score summary for student item {}"
u"Error while updating score summary for student item {}"
.
format
(
score
.
student_item
)
.
format
(
score
.
student_item
)
)
)
class
ScoreAnnotation
(
models
.
Model
):
""" Annotate individual scores with extra information if necessary. """
score
=
models
.
ForeignKey
(
Score
)
# A string that will represent the 'type' of annotation,
# e.g. staff_override, etc.
annotation_type
=
models
.
CharField
(
max_length
=
255
,
blank
=
False
,
db_index
=
True
)
creator
=
AnonymizedUserIDField
()
reason
=
models
.
TextField
()
submissions/tests/test_api.py
View file @
87aaf568
# -*- coding: utf-8 -*-
import
datetime
import
datetime
import
copy
import
copy
...
@@ -10,7 +12,7 @@ from mock import patch
...
@@ -10,7 +12,7 @@ from mock import patch
import
pytz
import
pytz
from
submissions
import
api
as
api
from
submissions
import
api
as
api
from
submissions.models
import
ScoreSummary
,
Submission
,
StudentItem
,
score_set
from
submissions.models
import
ScoreSummary
,
S
coreAnnotation
,
S
ubmission
,
StudentItem
,
score_set
from
submissions.serializers
import
StudentItemSerializer
from
submissions.serializers
import
StudentItemSerializer
STUDENT_ITEM
=
dict
(
STUDENT_ITEM
=
dict
(
...
@@ -252,6 +254,7 @@ class TestSubmissionsApi(TestCase):
...
@@ -252,6 +254,7 @@ class TestSubmissionsApi(TestCase):
api
.
set_score
(
submission
[
"uuid"
],
11
,
12
)
api
.
set_score
(
submission
[
"uuid"
],
11
,
12
)
score
=
api
.
get_latest_score_for_submission
(
submission
[
"uuid"
])
score
=
api
.
get_latest_score_for_submission
(
submission
[
"uuid"
])
self
.
_assert_score
(
score
,
11
,
12
)
self
.
_assert_score
(
score
,
11
,
12
)
self
.
assertFalse
(
ScoreAnnotation
.
objects
.
all
()
.
exists
())
@patch.object
(
score_set
,
'send'
)
@patch.object
(
score_set
,
'send'
)
def
test_set_score_signal
(
self
,
send_mock
):
def
test_set_score_signal
(
self
,
send_mock
):
...
@@ -268,6 +271,28 @@ class TestSubmissionsApi(TestCase):
...
@@ -268,6 +271,28 @@ class TestSubmissionsApi(TestCase):
item_id
=
STUDENT_ITEM
[
'item_id'
]
item_id
=
STUDENT_ITEM
[
'item_id'
]
)
)
@ddt.data
(
u"First score was incorrect"
,
u"☃"
)
def
test_set_score_with_annotation
(
self
,
reason
):
submission
=
api
.
create_submission
(
STUDENT_ITEM
,
ANSWER_ONE
)
creator_uuid
=
"Bob"
annotation_type
=
"staff_override"
api
.
set_score
(
submission
[
"uuid"
],
11
,
12
,
creator_uuid
,
annotation_type
,
reason
)
score
=
api
.
get_latest_score_for_submission
(
submission
[
"uuid"
])
self
.
_assert_score
(
score
,
11
,
12
)
# We need to do this to verify that one score annotation exists and was
# created for this score. We do not have an api point for retrieving
# annotations, and it doesn't make sense to expose them, since they're
# for auditing purposes.
annotations
=
ScoreAnnotation
.
objects
.
all
()
self
.
assertGreater
(
len
(
annotations
),
0
)
annotation
=
annotations
[
0
]
self
.
assertEqual
(
annotation
.
score
.
points_earned
,
11
)
self
.
assertEqual
(
annotation
.
score
.
points_possible
,
12
)
self
.
assertEqual
(
annotation
.
annotation_type
,
annotation_type
)
self
.
assertEqual
(
annotation
.
creator
,
creator_uuid
)
self
.
assertEqual
(
annotation
.
reason
,
reason
)
def
test_get_score
(
self
):
def
test_get_score
(
self
):
submission
=
api
.
create_submission
(
STUDENT_ITEM
,
ANSWER_ONE
)
submission
=
api
.
create_submission
(
STUDENT_ITEM
,
ANSWER_ONE
)
api
.
set_score
(
submission
[
"uuid"
],
11
,
12
)
api
.
set_score
(
submission
[
"uuid"
],
11
,
12
)
...
@@ -595,4 +620,3 @@ class TestSubmissionsApi(TestCase):
...
@@ -595,4 +620,3 @@ class TestSubmissionsApi(TestCase):
self
.
assertIsNotNone
(
score
)
self
.
assertIsNotNone
(
score
)
self
.
assertEqual
(
score
[
"points_earned"
],
expected_points_earned
)
self
.
assertEqual
(
score
[
"points_earned"
],
expected_points_earned
)
self
.
assertEqual
(
score
[
"points_possible"
],
expected_points_possible
)
self
.
assertEqual
(
score
[
"points_possible"
],
expected_points_possible
)
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