Commit c0abeb29 by Eric Fischer

Clear state

To fix the buggy behavior reported in TNL-3880, we need to ensure
that the submission we're dropping is unlinked from the student_item
used to find it. This prevents code (say, an ORA staff tool) from
constructing a student item with an id, a course, and a problem, and
using that to find a "cleared" submission.

Also includes migration for adding SubmissionDeleted model
parent 1c355952
...@@ -16,7 +16,9 @@ from dogapi import dog_stats_api ...@@ -16,7 +16,9 @@ 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, ScoreAnnotation, score_set, score_reset from submissions.models import (
Submission, SubmissionDeleted, StudentItem, Score, ScoreSummary, ScoreAnnotation, score_set, score_reset
)
logger = logging.getLogger("submissions.api") logger = logging.getLogger("submissions.api")
...@@ -643,7 +645,7 @@ def get_latest_score_for_submission(submission_uuid, read_replica=False): ...@@ -643,7 +645,7 @@ def get_latest_score_for_submission(submission_uuid, read_replica=False):
return ScoreSerializer(score).data 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. Reset scores for a specific student on a specific problem.
...@@ -655,6 +657,7 @@ def reset_score(student_id, course_id, item_id): ...@@ -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. 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. 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. item_id (unicode): The ID of the item for which to reset scores.
clear_state (boolean): If True, unlink the Submission and StudentItem so the Submission cannot be accessed.
Returns: Returns:
None None
...@@ -684,6 +687,27 @@ def reset_score(student_id, course_id, item_id): ...@@ -684,6 +687,27 @@ def reset_score(student_id, course_id, item_id):
item_id=item_id, item_id=item_id,
) )
if clear_state:
# sever the link between this student item and any submissions it may currently have
# this is done by creating a SubmissionDeleted (to keep data for analytics), then deleting the Submission
for sub in student_item.submission_set.all():
init_dict = sub._clone_dict()
with transaction.atomic():
archived_sub = SubmissionDeleted.objects.create(**init_dict)
sub.delete()
# Also clear out cached values
cache_key = "submissions.submission.{}".format(sub.uuid)
cache.delete(cache_key)
# TODO: the top scores cache is automatically invalidated after 5 minutes, should we rely on that instead of doing it manually here?
cache_keys = ["submissions.top_submissions.{course}.{item}.{type}.{number}".format(
course=course_id,
item=item_id,
type=student_item.item_type,
number=i+1 # Looping from 1 to MAX_TOP_SUBMISSIONS, inclusive
) for i in range(0, MAX_TOP_SUBMISSIONS)]
cache.delete_many(cache_keys)
except DatabaseError: except DatabaseError:
msg = ( msg = (
u"Error occurred while reseting scores for" u"Error occurred while reseting scores for"
......
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import django.utils.timezone
import jsonfield.fields
import django_extensions.db.fields
class Migration(migrations.Migration):
dependencies = [
('submissions', '0002_auto_20151119_0913'),
]
operations = [
migrations.CreateModel(
name='SubmissionDeleted',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('uuid', django_extensions.db.fields.UUIDField(db_index=True, version=1, editable=False, blank=True)),
('attempt_number', models.PositiveIntegerField()),
('submitted_at', models.DateTimeField(default=django.utils.timezone.now, db_index=True)),
('created_at', models.DateTimeField(default=django.utils.timezone.now, editable=False, db_index=True)),
('answer', jsonfield.fields.JSONField(db_column=b'raw_answer', blank=True)),
('student_item', models.ForeignKey(to='submissions.StudentItem')),
],
options={
'ordering': ['-submitted_at', '-id'],
'abstract': False,
},
),
]
...@@ -89,7 +89,7 @@ class StudentItem(models.Model): ...@@ -89,7 +89,7 @@ class StudentItem(models.Model):
) )
class Submission(models.Model): class SubmissionBase(models.Model):
"""A single response by a student for a given problem in a given course. """A single response by a student for a given problem in a given course.
A student may have multiple submissions for the same problem. Submissions A student may have multiple submissions for the same problem. Submissions
...@@ -125,14 +125,17 @@ class Submission(models.Model): ...@@ -125,14 +125,17 @@ class Submission(models.Model):
answer = JSONField(blank=True, db_column="raw_answer") answer = JSONField(blank=True, db_column="raw_answer")
def __repr__(self): def __repr__(self):
return repr(dict( return repr(self._clone_dict())
def _clone_dict(self):
return dict(
uuid=self.uuid, uuid=self.uuid,
student_item=self.student_item, student_item=self.student_item,
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, answer=self.answer,
)) )
def __unicode__(self): def __unicode__(self):
return u"Submission {}".format(self.uuid) return u"Submission {}".format(self.uuid)
...@@ -140,6 +143,24 @@ class Submission(models.Model): ...@@ -140,6 +143,24 @@ class Submission(models.Model):
class Meta: class Meta:
app_label = "submissions" app_label = "submissions"
ordering = ["-submitted_at", "-id"] ordering = ["-submitted_at", "-id"]
abstract = True
class Submission(SubmissionBase):
"""
The main Submission model.
"""
def __init__(self, *args, **kwargs):
SubmissionBase.__init__(self, *args, **kwargs)
class SubmissionDeleted(SubmissionBase):
"""
A utility class, to allow submissions to be hidden from the main Submissions data
while still remaining useful for later analysis on the backend.
"""
def __init__(self, *args, **kwargs):
SubmissionBase.__init__(self, *args, **kwargs)
class Score(models.Model): class Score(models.Model):
......
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