Commit 6f838319 by Eric Fischer

Allow null edited_timestamp

Some old mongo courses do not have this field, and the team has opted
to allow null values rather than inserting a default. This change
affects both course and subsection grades.

TNL-6408
parent a78db97d
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('grades', '0010_auto_20170112_1156'),
]
operations = [
migrations.AlterField(
model_name='persistentcoursegrade',
name='course_edited_timestamp',
field=models.DateTimeField(null=True, verbose_name='Last content edit timestamp', blank=True),
),
migrations.AlterField(
model_name='persistentsubsectiongrade',
name='course_version',
field=models.CharField(max_length=255, verbose_name='Guid of latest course version', blank=True),
),
migrations.AlterField(
model_name='persistentsubsectiongrade',
name='subtree_edited_timestamp',
field=models.DateTimeField(null=True, verbose_name='Last content edit timestamp', blank=True),
),
]
...@@ -277,8 +277,8 @@ class PersistentSubsectionGrade(DeleteGradesMixin, TimeStampedModel): ...@@ -277,8 +277,8 @@ class PersistentSubsectionGrade(DeleteGradesMixin, TimeStampedModel):
usage_key = UsageKeyField(blank=False, max_length=255) usage_key = UsageKeyField(blank=False, max_length=255)
# Information relating to the state of content when grade was calculated # Information relating to the state of content when grade was calculated
subtree_edited_timestamp = models.DateTimeField('last content edit timestamp', blank=False) subtree_edited_timestamp = models.DateTimeField(u'Last content edit timestamp', blank=True, null=True)
course_version = models.CharField('guid of latest course version', blank=True, max_length=255) course_version = models.CharField(u'Guid of latest course version', blank=True, max_length=255)
# earned/possible refers to the number of points achieved and available to achieve. # earned/possible refers to the number of points achieved and available to achieve.
# graded refers to the subset of all problems that are marked as being graded. # graded refers to the subset of all problems that are marked as being graded.
...@@ -529,7 +529,7 @@ class PersistentCourseGrade(DeleteGradesMixin, TimeStampedModel): ...@@ -529,7 +529,7 @@ class PersistentCourseGrade(DeleteGradesMixin, TimeStampedModel):
course_id = CourseKeyField(blank=False, max_length=255) course_id = CourseKeyField(blank=False, max_length=255)
# Information relating to the state of content when grade was calculated # Information relating to the state of content when grade was calculated
course_edited_timestamp = models.DateTimeField(u'Last content edit timestamp', blank=False) course_edited_timestamp = models.DateTimeField(u'Last content edit timestamp', blank=True, null=True)
course_version = models.CharField(u'Course content version identifier', blank=True, max_length=255) course_version = models.CharField(u'Course content version identifier', blank=True, max_length=255)
grading_policy_hash = models.CharField(u'Hash of grading policy', blank=False, max_length=255) grading_policy_hash = models.CharField(u'Hash of grading policy', blank=False, max_length=255)
......
...@@ -32,7 +32,7 @@ class SubsectionGrade(object): ...@@ -32,7 +32,7 @@ class SubsectionGrade(object):
self.graded = getattr(subsection, 'graded', False) self.graded = getattr(subsection, 'graded', False)
self.course_version = getattr(subsection, 'course_version', None) self.course_version = getattr(subsection, 'course_version', None)
self.subtree_edited_timestamp = subsection.subtree_edited_on self.subtree_edited_timestamp = getattr(subsection, 'subtree_edited_on', None)
self.graded_total = None # aggregated grade for all graded problems self.graded_total = None # aggregated grade for all graded problems
self.all_total = None # aggregated grade for all problems, regardless of whether they are graded self.all_total = None # aggregated grade for all problems, regardless of whether they are graded
...@@ -341,6 +341,6 @@ class SubsectionGradeFactory(object): ...@@ -341,6 +341,6 @@ class SubsectionGradeFactory(object):
log_statement, log_statement,
self.course.id, self.course.id,
getattr(subsection, 'course_version', None), getattr(subsection, 'course_version', None),
subsection.subtree_edited_on, getattr(subsection, 'subtree_edited_on', None),
self.student.id, self.student.id,
)) ))
...@@ -27,7 +27,7 @@ from .signals import ( ...@@ -27,7 +27,7 @@ from .signals import (
from ..constants import ScoreDatabaseTableEnum from ..constants import ScoreDatabaseTableEnum
from ..new.course_grade import CourseGradeFactory from ..new.course_grade import CourseGradeFactory
from ..scores import weighted_score from ..scores import weighted_score
from ..tasks import recalculate_subsection_grade_v3 from ..tasks import recalculate_subsection_grade_v3, RECALCULATE_GRADE_DELAY
log = getLogger(__name__) log = getLogger(__name__)
...@@ -192,7 +192,7 @@ def enqueue_subsection_update(sender, **kwargs): # pylint: disable=unused-argum ...@@ -192,7 +192,7 @@ def enqueue_subsection_update(sender, **kwargs): # pylint: disable=unused-argum
event_transaction_type=unicode(get_event_transaction_type()), event_transaction_type=unicode(get_event_transaction_type()),
score_db_table=kwargs['score_db_table'], score_db_table=kwargs['score_db_table'],
), ),
countdown=2, countdown=RECALCULATE_GRADE_DELAY,
) )
log.info( log.info(
u'Grades: Request async calculation of subsection grades with args: {}. Task [{}]'.format( u'Grades: Request async calculation of subsection grades with args: {}. Task [{}]'.format(
......
...@@ -33,6 +33,7 @@ from .transformer import GradesTransformer ...@@ -33,6 +33,7 @@ from .transformer import GradesTransformer
log = getLogger(__name__) log = getLogger(__name__)
KNOWN_RETRY_ERRORS = (DatabaseError, ValidationError) # Errors we expect occasionally, should be resolved on retry KNOWN_RETRY_ERRORS = (DatabaseError, ValidationError) # Errors we expect occasionally, should be resolved on retry
RECALCULATE_GRADE_DELAY = 2 # in seconds, to prevent excessive _has_db_updated failures. See TNL-6424.
@task(bind=True, base=PersistOnFailureTask, default_retry_delay=30, routing_key=settings.RECALCULATE_GRADES_ROUTING_KEY) @task(bind=True, base=PersistOnFailureTask, default_retry_delay=30, routing_key=settings.RECALCULATE_GRADES_ROUTING_KEY)
......
...@@ -231,14 +231,14 @@ class PersistentSubsectionGradeTest(GradesModelTestCase): ...@@ -231,14 +231,14 @@ class PersistentSubsectionGradeTest(GradesModelTestCase):
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
PersistentSubsectionGrade.create_grade(**self.params) PersistentSubsectionGrade.create_grade(**self.params)
def test_optional_fields(self): @ddt.data('course_version', 'subtree_edited_timestamp')
del self.params["course_version"] def test_optional_fields(self, field):
del self.params[field]
PersistentSubsectionGrade.create_grade(**self.params) PersistentSubsectionGrade.create_grade(**self.params)
@ddt.data( @ddt.data(
("user_id", ValidationError), ("user_id", ValidationError),
("usage_key", KeyError), ("usage_key", KeyError),
("subtree_edited_timestamp", ValidationError),
("earned_all", ValidationError), ("earned_all", ValidationError),
("possible_all", ValidationError), ("possible_all", ValidationError),
("earned_graded", ValidationError), ("earned_graded", ValidationError),
...@@ -427,10 +427,11 @@ class PersistentCourseGradesTest(GradesModelTestCase): ...@@ -427,10 +427,11 @@ class PersistentCourseGradesTest(GradesModelTestCase):
self.assertIsInstance(created_grade.passed_timestamp, datetime) self.assertIsInstance(created_grade.passed_timestamp, datetime)
self.assertEqual(created_grade, read_grade) self.assertEqual(created_grade, read_grade)
def test_course_version_optional(self): @ddt.data('course_version', 'course_edited_timestamp')
del self.params["course_version"] def test_optional_fields(self, field):
del self.params[field]
grade = PersistentCourseGrade.update_or_create_course_grade(**self.params) grade = PersistentCourseGrade.update_or_create_course_grade(**self.params)
self.assertEqual("", grade.course_version) self.assertFalse(getattr(grade, field))
@ddt.data( @ddt.data(
("percent_grade", "Not a float at all", ValueError), ("percent_grade", "Not a float at all", ValueError),
......
...@@ -27,7 +27,7 @@ from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, chec ...@@ -27,7 +27,7 @@ from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, chec
from lms.djangoapps.grades.config.models import PersistentGradesEnabledFlag from lms.djangoapps.grades.config.models import PersistentGradesEnabledFlag
from lms.djangoapps.grades.constants import ScoreDatabaseTableEnum from lms.djangoapps.grades.constants import ScoreDatabaseTableEnum
from lms.djangoapps.grades.signals.signals import PROBLEM_WEIGHTED_SCORE_CHANGED from lms.djangoapps.grades.signals.signals import PROBLEM_WEIGHTED_SCORE_CHANGED
from lms.djangoapps.grades.tasks import recalculate_subsection_grade_v3 from lms.djangoapps.grades.tasks import recalculate_subsection_grade_v3, RECALCULATE_GRADE_DELAY
@patch.dict(settings.FEATURES, {'PERSISTENT_GRADES_ENABLED_FOR_ALL_TESTS': False}) @patch.dict(settings.FEATURES, {'PERSISTENT_GRADES_ENABLED_FOR_ALL_TESTS': False})
...@@ -114,7 +114,7 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase): ...@@ -114,7 +114,7 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
return_value=None return_value=None
) as mock_task_apply: ) as mock_task_apply:
PROBLEM_WEIGHTED_SCORE_CHANGED.send(sender=None, **send_args) PROBLEM_WEIGHTED_SCORE_CHANGED.send(sender=None, **send_args)
mock_task_apply.assert_called_once_with(kwargs=local_task_args) mock_task_apply.assert_called_once_with(countdown=RECALCULATE_GRADE_DELAY, kwargs=local_task_args)
@patch('lms.djangoapps.grades.signals.signals.SUBSECTION_SCORE_CHANGED.send') @patch('lms.djangoapps.grades.signals.signals.SUBSECTION_SCORE_CHANGED.send')
def test_triggers_subsection_score_signal(self, mock_subsection_signal): def test_triggers_subsection_score_signal(self, mock_subsection_signal):
......
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