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
from submissions.serializers import (
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")
......@@ -643,7 +645,7 @@ def get_latest_score_for_submission(submission_uuid, read_replica=False):
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 (boolean): If True, unlink the Submission and StudentItem so the Submission cannot be accessed.
Returns:
None
......@@ -684,6 +687,27 @@ def reset_score(student_id, course_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:
msg = (
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):
)
class Submission(models.Model):
class SubmissionBase(models.Model):
"""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
......@@ -125,14 +125,17 @@ class Submission(models.Model):
answer = JSONField(blank=True, db_column="raw_answer")
def __repr__(self):
return repr(dict(
return repr(self._clone_dict())
def _clone_dict(self):
return dict(
uuid=self.uuid,
student_item=self.student_item,
attempt_number=self.attempt_number,
submitted_at=self.submitted_at,
created_at=self.created_at,
answer=self.answer,
))
)
def __unicode__(self):
return u"Submission {}".format(self.uuid)
......@@ -140,6 +143,24 @@ class Submission(models.Model):
class Meta:
app_label = "submissions"
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):
......
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