Commit 86b9122c by Eric Fischer

Soft-delete Submissions to reset student state

Previous methods of "deleting state" for a student had just issued
reset scores, so the (unscored) submission was still hanging around.
With this change, the submission can be "deleted" for all django-
relevant purposes, while still remaining in the database in case
it turns out to be interesting to analytics later.

Includes a migration that may take some time to run, and will acquire a
table lock. This will cause an outage for any other request trying to
hit the submissions table during the migration. We've estimated the outage
to last approximately 3 minutes on a database with 700,000 rows.

Test also included.
parent 02ccd59d
......@@ -217,7 +217,7 @@ def get_submission(submission_uuid, read_replica=False):
msg="submission_uuid ({!r}) must be a string type".format(submission_uuid)
)
cache_key = "submissions.submission.{}".format(submission_uuid)
cache_key = Submission.get_cache_key(submission_uuid)
try:
cached_submission_data = cache.get(cache_key)
except Exception as ex:
......@@ -627,8 +627,10 @@ def get_latest_score_for_submission(submission_uuid, read_replica=False):
"""
try:
# Ensure that submission_uuid is valid before fetching score
submission_model = Submission.objects.get(uuid=submission_uuid)
score_qs = Score.objects.filter(
submission__uuid=submission_uuid
submission__uuid=submission_model.uuid
).order_by("-id").select_related("submission")
if read_replica:
......@@ -637,13 +639,13 @@ def get_latest_score_for_submission(submission_uuid, read_replica=False):
score = score_qs[0]
if score.is_hidden():
return None
except IndexError:
except (IndexError, Submission.DoesNotExist):
return None
return ScoreSerializer(score).data
def reset_score(student_id, course_id, item_id):
def reset_score(student_id, course_id, item_id, clear_state=False):
"""
Reset scores for a specific student on a specific problem.
......@@ -655,6 +657,7 @@ def reset_score(student_id, course_id, item_id):
student_id (unicode): The ID of the student for whom to reset scores.
course_id (unicode): The ID of the course containing the item to reset.
item_id (unicode): The ID of the item for which to reset scores.
clear_state (bool): If True, will appear to delete any submissions associated with the specified StudentItem
Returns:
None
......@@ -684,6 +687,16 @@ def reset_score(student_id, course_id, item_id):
item_id=item_id,
)
if clear_state:
for sub in student_item.submission_set.all():
# soft-delete the Submission
sub.status = Submission.DELETED
sub.save(update_fields=["status"])
# Also clear out cached values
cache_key = Submission.get_cache_key(sub.uuid)
cache.delete(cache_key)
except DatabaseError:
msg = (
u"Error occurred while reseting scores for"
......
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('submissions', '0002_auto_20151119_0913'),
]
operations = [
migrations.AddField(
model_name='submission',
name='status',
field=models.CharField(default=b'A', max_length=1, choices=[(b'D', b'Deleted'), (b'A', b'Active')]),
),
]
......@@ -124,6 +124,27 @@ class Submission(models.Model):
# name so it continues to use `raw_answer`.
answer = JSONField(blank=True, db_column="raw_answer")
# Has this submission been soft-deleted? This allows instructors to reset student
# state on an item, while preserving the previous value for potential analytics use.
DELETED = 'D'
ACTIVE = 'A'
STATUS_CHOICES = (
(DELETED, 'Deleted'),
(ACTIVE, 'Active'),
)
status = models.CharField(max_length=1, choices=STATUS_CHOICES, default=ACTIVE)
# Override the default Manager with our custom one to filter out soft-deleted items
class SoftDeletedManager(models.Manager):
def get_queryset(self):
return super(Submission.SoftDeletedManager, self).get_queryset().exclude(status=Submission.DELETED)
objects = SoftDeletedManager()
@staticmethod
def get_cache_key(sub_uuid):
return "submissions.submission.{}".format(sub_uuid)
def __repr__(self):
return repr(dict(
uuid=self.uuid,
......
......@@ -573,6 +573,27 @@ class TestSubmissionsApi(TestCase):
)
self.assertEqual(cached_scores, scores)
def test_clear_state(self):
# Create a submission, give it a score, and verify that score exists
submission = api.create_submission(STUDENT_ITEM, ANSWER_ONE)
api.set_score(submission["uuid"], 11, 12)
score = api.get_score(STUDENT_ITEM)
self._assert_score(score, 11, 12)
self.assertEqual(score['submission_uuid'], submission['uuid'])
# Reset the score with clear_state=True
# This should set the submission's score to None, and make it unavailable to get_submissions
api.reset_score(
STUDENT_ITEM["student_id"],
STUDENT_ITEM["course_id"],
STUDENT_ITEM["item_id"],
clear_state=True,
)
score = api.get_score(STUDENT_ITEM)
self.assertIsNone(score)
subs = api.get_submissions(STUDENT_ITEM)
self.assertEqual(subs, [])
@raises(api.SubmissionRequestError)
def test_error_on_get_top_submissions_too_few(self):
student_item = copy.deepcopy(STUDENT_ITEM)
......
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