Commit bf235469 by Eric Fischer Committed by GitHub

Merge pull request #14087 from edx/efischer/tnl-5995

Go back to comparing timestamps in has_database_updated_with_new_score
parents 72f4178c 5a98a90e
......@@ -1008,6 +1008,7 @@ def set_score(user_id, usage_key, score, max_score):
student_module.grade = score
student_module.max_grade = max_score
student_module.save()
return student_module.modified
def get_score(user_id, usage_key):
......@@ -1024,4 +1025,4 @@ def get_score(user_id, usage_key):
except StudentModule.DoesNotExist:
return None
else:
return student_module.grade, student_module.max_grade
return student_module
......@@ -2,6 +2,7 @@
"""
Test for lms courseware app, module render unit
"""
from datetime import datetime
import ddt
import itertools
import json
......@@ -15,10 +16,12 @@ from django.conf import settings
from django.test.client import RequestFactory
from django.test.utils import override_settings
from django.contrib.auth.models import AnonymousUser
from freezegun import freeze_time
from mock import MagicMock, patch, Mock
from opaque_keys.edx.keys import UsageKey, CourseKey
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from pyquery import PyQuery
import pytz
from xblock.field_data import FieldData
from xblock.runtime import Runtime
from xblock.fields import ScopeIds
......@@ -1831,6 +1834,7 @@ class TestXmoduleRuntimeEvent(TestSubmittingProblems):
self.assertIsNone(student_module.grade)
self.assertIsNone(student_module.max_grade)
@freeze_time(datetime.now().replace(tzinfo=pytz.UTC))
@patch('lms.djangoapps.grades.signals.handlers.PROBLEM_RAW_SCORE_CHANGED.send')
def test_score_change_signal(self, send_mock):
"""Test that a Django signal is generated when a score changes"""
......@@ -1844,6 +1848,7 @@ class TestXmoduleRuntimeEvent(TestSubmittingProblems):
'course_id': unicode(self.course.id),
'usage_id': unicode(self.problem.location),
'only_if_higher': None,
'modified': datetime.now().replace(tzinfo=pytz.UTC)
}
send_mock.assert_called_with(**expected_signal_kwargs)
......
......@@ -4,9 +4,11 @@ Grades related signals.
from logging import getLogger
from courseware.model_data import get_score, set_score
from django.dispatch import receiver
from openedx.core.lib.grade_utils import is_score_higher
from submissions.models import score_set, score_reset
from util.date_utils import to_timestamp
from courseware.model_data import get_score, set_score
from eventtracking import tracker
......@@ -25,7 +27,7 @@ from .signals import (
)
from ..new.course_grade import CourseGradeFactory
from ..scores import weighted_score
from ..tasks import recalculate_subsection_grade
from ..tasks import recalculate_subsection_grade_v2
log = getLogger(__name__)
......@@ -62,7 +64,8 @@ def submissions_score_set_handler(sender, **kwargs): # pylint: disable=unused-a
weighted_possible=points_possible,
user_id=user.id,
course_id=course_id,
usage_id=usage_id
usage_id=usage_id,
modified=kwargs['created_at'],
)
......@@ -92,7 +95,8 @@ def submissions_score_reset_handler(sender, **kwargs): # pylint: disable=unused
weighted_possible=0,
user_id=user.id,
course_id=course_id,
usage_id=usage_id
usage_id=usage_id,
modified=kwargs['created_at'],
)
......@@ -107,7 +111,7 @@ def score_published_handler(sender, block, user, raw_earned, raw_possible, only_
previous_score = get_score(user.id, block.location)
if previous_score is not None:
prev_raw_earned, prev_raw_possible = previous_score # pylint: disable=unpacking-non-sequence
prev_raw_earned, prev_raw_possible = (previous_score.grade, previous_score.max_grade)
if not is_score_higher(prev_raw_earned, prev_raw_possible, raw_earned, raw_possible):
update_score = False
......@@ -119,7 +123,7 @@ def score_published_handler(sender, block, user, raw_earned, raw_possible, only_
)
if update_score:
set_score(user.id, block.location, raw_earned, raw_possible)
score_modified_time = set_score(user.id, block.location, raw_earned, raw_possible)
PROBLEM_RAW_SCORE_CHANGED.send(
sender=None,
raw_earned=raw_earned,
......@@ -129,6 +133,7 @@ def score_published_handler(sender, block, user, raw_earned, raw_possible, only_
course_id=unicode(block.location.course_key),
usage_id=unicode(block.location),
only_if_higher=only_if_higher,
modified=score_modified_time,
)
return update_score
......@@ -157,6 +162,7 @@ def problem_raw_score_changed_handler(sender, **kwargs): # pylint: disable=unus
usage_id=kwargs['usage_id'],
only_if_higher=kwargs['only_if_higher'],
score_deleted=kwargs.get('score_deleted', False),
modified=kwargs['modified'],
)
......@@ -167,14 +173,13 @@ def enqueue_subsection_update(sender, **kwargs): # pylint: disable=unused-argum
enqueueing a subsection update operation to occur asynchronously.
"""
_emit_problem_submitted_event(kwargs)
result = recalculate_subsection_grade.apply_async(
result = recalculate_subsection_grade_v2.apply_async(
kwargs=dict(
user_id=kwargs['user_id'],
course_id=kwargs['course_id'],
usage_id=kwargs['usage_id'],
only_if_higher=kwargs.get('only_if_higher'),
weighted_earned=kwargs.get('weighted_earned'),
weighted_possible=kwargs.get('weighted_possible'),
expected_modified_time=to_timestamp(kwargs['modified']),
score_deleted=kwargs.get('score_deleted', False),
event_transaction_id=unicode(get_event_transaction_id()),
event_transaction_type=unicode(get_event_transaction_type()),
......
......@@ -20,6 +20,8 @@ PROBLEM_RAW_SCORE_CHANGED = Signal(
'weight', # Weight of the problem
'only_if_higher', # Boolean indicating whether updates should be
# made only if the new score is higher than previous.
'modified', # A datetime indicating when the database representation of
# this the problem score was saved.
]
)
......@@ -39,6 +41,8 @@ PROBLEM_WEIGHTED_SCORE_CHANGED = Signal(
'weighted_possible', # Maximum score available for the exercise
'only_if_higher', # Boolean indicating whether updates should be
# made only if the new score is higher than previous.
'modified', # A datetime indicating when the database representation of
# this the problem score was saved.
]
)
......
......@@ -8,15 +8,19 @@ from django.contrib.auth.models import User
from django.db.utils import DatabaseError
from logging import getLogger
from courseware.model_data import get_score
from lms.djangoapps.course_blocks.api import get_course_blocks
from opaque_keys.edx.keys import UsageKey
from opaque_keys.edx.locator import CourseLocator
from submissions import api as sub_api
from student.models import anonymous_id_for_user
from track.event_transaction_utils import (
set_event_transaction_type,
set_event_transaction_id,
get_event_transaction_type,
get_event_transaction_id
)
from util.date_utils import from_timestamp
from xmodule.modulestore.django import modulestore
from .config.models import PersistentGradesEnabledFlag
......@@ -29,9 +33,27 @@ log = getLogger(__name__)
@task(default_retry_delay=30, routing_key=settings.RECALCULATE_GRADES_ROUTING_KEY)
def recalculate_subsection_grade(
# pylint: disable=unused-argument
user_id, course_id, usage_id, only_if_higher, weighted_earned, weighted_possible, **kwargs
):
"""
Shim to allow us to modify this task's signature without blowing up production on deployment.
"""
recalculate_subsection_grade_v2.apply(
kwargs=dict(
user_id=user_id,
course_id=course_id,
usage_id=usage_id,
only_if_higher=only_if_higher,
expected_modified_time=kwargs.get('expected_modified_time', 0), # Use the unix epoch as a default
score_deleted=kwargs['score_deleted'],
)
)
@task(default_retry_delay=30, routing_key=settings.RECALCULATE_GRADES_ROUTING_KEY)
def recalculate_subsection_grade_v2(**kwargs):
"""
Updates a saved subsection grade.
Arguments:
......@@ -41,10 +63,8 @@ def recalculate_subsection_grade(
only_if_higher (boolean): indicating whether grades should
be updated only if the new raw_earned is higher than the
previous value.
weighted_earned (float): the weighted points the learner earned on the
problem that triggered the update.
weighted_possible (float): the max weighted points the leaner could have
earned on the problem.
expected_modified_time (serialized timestamp): indicates when the task
was queued so that we can verify the underlying data update.
score_deleted (boolean): indicating whether the grade change is
a result of the problem's score being deleted.
event_transaction_id(string): uuid identifying the current
......@@ -52,56 +72,74 @@ def recalculate_subsection_grade(
event_transaction_type(string): human-readable type of the
event at the root of the current event transaction.
"""
course_key = CourseLocator.from_string(course_id)
course_key = CourseLocator.from_string(kwargs['course_id'])
if not PersistentGradesEnabledFlag.feature_enabled(course_key):
return
score_deleted = kwargs['score_deleted']
scored_block_usage_key = UsageKey.from_string(kwargs['usage_id']).replace(course_key=course_key)
expected_modified_time = from_timestamp(kwargs['expected_modified_time'])
# The request cache is not maintained on celery workers,
# where this code runs. So we take the values from the
# main request cache and store them in the local request
# cache. This correlates model-level grading events with
# higher-level ones.
set_event_transaction_id(kwargs.get('event_transaction_id', None))
set_event_transaction_type(kwargs.get('event_transaction_type', None))
scored_block_usage_key = UsageKey.from_string(usage_id).replace(course_key=course_key)
set_event_transaction_id(kwargs.pop('event_transaction_id', None))
set_event_transaction_type(kwargs.pop('event_transaction_type', None))
# Verify the database has been updated with the scores when the task was
# created. This race condition occurs if the transaction in the task
# creator's process hasn't committed before the task initiates in the worker
# process.
if not _has_database_updated_with_new_score(
user_id, scored_block_usage_key, weighted_earned, weighted_possible, score_deleted,
kwargs['user_id'], scored_block_usage_key, expected_modified_time, score_deleted,
):
raise _retry_recalculate_subsection_grade(
user_id, course_id, usage_id, only_if_higher, weighted_earned, weighted_possible, score_deleted,
)
raise _retry_recalculate_subsection_grade(**kwargs)
_update_subsection_grades(
course_key,
scored_block_usage_key,
only_if_higher,
course_id,
user_id,
usage_id,
weighted_earned,
weighted_possible,
kwargs['only_if_higher'],
kwargs['course_id'],
kwargs['user_id'],
kwargs['usage_id'],
kwargs['expected_modified_time'],
score_deleted,
)
def _has_database_updated_with_new_score(
user_id, scored_block_usage_key, expected_raw_earned, expected_raw_possible, score_deleted,
user_id, scored_block_usage_key, expected_modified_time, score_deleted,
):
"""
Returns whether the database has been updated with the
expected new score values for the given problem and user.
Just here to let tests run while Eric updates his PR to go back
to timestamp-based comparison.
"""
return True
score = get_score(user_id, scored_block_usage_key)
if score is None:
# score should be None only if it was deleted.
# Otherwise, it hasn't yet been saved.
return score_deleted
elif score.module_type == 'openassessment':
anon_id = anonymous_id_for_user(User.objects.get(id=user_id), scored_block_usage_key.course_key)
course_id = unicode(scored_block_usage_key.course_key)
item_id = unicode(scored_block_usage_key)
api_score = sub_api.get_score(
{
"student_id": anon_id,
"course_id": course_id,
"item_id": item_id,
"item_type": "openassessment"
}
)
reported_modified_time = api_score.created_at
else:
reported_modified_time = score.modified
return reported_modified_time >= expected_modified_time
def _update_subsection_grades(
......@@ -111,8 +149,7 @@ def _update_subsection_grades(
course_id,
user_id,
usage_id,
weighted_earned,
weighted_possible,
expected_modified_time,
score_deleted,
):
"""
......@@ -153,8 +190,7 @@ def _update_subsection_grades(
course_id,
usage_id,
only_if_higher,
weighted_earned,
weighted_possible,
expected_modified_time,
score_deleted,
exc,
)
......@@ -165,8 +201,7 @@ def _retry_recalculate_subsection_grade(
course_id,
usage_id,
only_if_higher,
weighted_earned,
weighted_possible,
expected_modified_time,
score_deleted,
exc=None,
):
......@@ -174,14 +209,13 @@ def _retry_recalculate_subsection_grade(
Calls retry for the recalculate_subsection_grade task with the
given inputs.
"""
recalculate_subsection_grade.retry(
recalculate_subsection_grade_v2.retry(
kwargs=dict(
user_id=user_id,
course_id=course_id,
usage_id=usage_id,
only_if_higher=only_if_higher,
weighted_earned=weighted_earned,
weighted_possible=weighted_possible,
expected_modified_time=expected_modified_time,
score_deleted=score_deleted,
event_transaction_id=unicode(get_event_transaction_id()),
event_transaction_type=unicode(get_event_transaction_type()),
......
......@@ -156,6 +156,9 @@ class GradesEventIntegrationTest(ProblemSubmissionTestMixin, SharedModuleStoreTe
'course_version': unicode(course.course_version),
}
)
enrollment_tracker.reset_mock()
models_tracker.reset_mock()
handlers_tracker.reset_mock()
@patch('lms.djangoapps.instructor_task.tasks_helper.tracker')
@patch('lms.djangoapps.grades.signals.handlers.tracker')
......@@ -223,3 +226,6 @@ class GradesEventIntegrationTest(ProblemSubmissionTestMixin, SharedModuleStoreTe
'course_edited_timestamp': unicode(course.subtree_edited_on),
}
)
instructor_task_tracker.reset_mock()
models_tracker.reset_mock()
handlers_tracker.reset_mock()
......@@ -4,9 +4,12 @@ Tests for the score change signals defined in the courseware models module.
import re
from datetime import datetime
import ddt
from django.test import TestCase
from mock import patch, MagicMock
import pytz
from util.date_utils import to_timestamp
from ..signals.handlers import (
enqueue_subsection_update,
......@@ -17,19 +20,24 @@ from ..signals.handlers import (
UUID_REGEX = re.compile(ur'%(hex)s{8}-%(hex)s{4}-%(hex)s{4}-%(hex)s{4}-%(hex)s{12}' % {'hex': u'[0-9a-f]'})
FROZEN_NOW_DATETIME = datetime.now().replace(tzinfo=pytz.UTC)
FROZEN_NOW_TIMESTAMP = to_timestamp(FROZEN_NOW_DATETIME)
SUBMISSION_SET_KWARGS = {
'points_possible': 10,
'points_earned': 5,
'anonymous_user_id': 'anonymous_id',
'course_id': 'CourseID',
'item_id': 'i4x://org/course/usage/123456'
'item_id': 'i4x://org/course/usage/123456',
'created_at': FROZEN_NOW_TIMESTAMP,
}
SUBMISSION_RESET_KWARGS = {
'anonymous_user_id': 'anonymous_id',
'course_id': 'CourseID',
'item_id': 'i4x://org/course/usage/123456'
'item_id': 'i4x://org/course/usage/123456',
'created_at': FROZEN_NOW_TIMESTAMP,
}
PROBLEM_RAW_SCORE_CHANGED_KWARGS = {
......@@ -41,6 +49,7 @@ PROBLEM_RAW_SCORE_CHANGED_KWARGS = {
'usage_id': 'i4x://org/course/usage/123456',
'only_if_higher': False,
'score_deleted': True,
'modified': FROZEN_NOW_TIMESTAMP,
}
......@@ -102,7 +111,8 @@ class ScoreChangedSignalRelayTest(TestCase):
'weighted_earned': earned,
'user_id': self.user_mock.id,
'course_id': 'CourseID',
'usage_id': 'i4x://org/course/usage/123456'
'usage_id': 'i4x://org/course/usage/123456',
'modified': FROZEN_NOW_TIMESTAMP,
}
self.signal_mock.assert_called_once_with(**expected_set_kwargs)
self.get_user_mock.assert_called_once_with(kwargs['anonymous_user_id'])
......@@ -152,6 +162,7 @@ class ScoreChangedSignalRelayTest(TestCase):
'usage_id': 'i4x://org/course/usage/123456',
'only_if_higher': False,
'score_deleted': True,
'modified': FROZEN_NOW_TIMESTAMP
}
self.signal_mock.assert_called_with(**expected_set_kwargs)
......@@ -168,6 +179,7 @@ class ScoreChangedSignalRelayTest(TestCase):
'usage_id': 'i4x://org/course/usage/123456',
'only_if_higher': False,
'score_deleted': False,
'modified': FROZEN_NOW_TIMESTAMP
}
self.signal_mock.assert_called_with(**expected_set_kwargs)
......@@ -178,6 +190,7 @@ class ScoreChangedSignalRelayTest(TestCase):
user_id=1,
course_id=u'course-v1:edX+Demo_Course+DemoX',
usage_id=u'block-v1:block-key',
modified=FROZEN_NOW_DATETIME,
)
log_statement = mocklog.call_args[0][0]
log_statement = UUID_REGEX.sub(u'*UUID*', log_statement)
......@@ -185,7 +198,7 @@ class ScoreChangedSignalRelayTest(TestCase):
log_statement,
(
u'Grades: Request async calculation of subsection grades with args: '
u'course_id:course-v1:edX+Demo_Course+DemoX, usage_id:block-v1:block-key, '
u'user_id:1. Task [*UUID*]'
)
u'course_id:course-v1:edX+Demo_Course+DemoX, modified:{time}, '
u'usage_id:block-v1:block-key, user_id:1. Task [*UUID*]'
).format(time=FROZEN_NOW_DATETIME)
)
......@@ -4,10 +4,13 @@ Tests for the functionality and infrastructure of grades tasks.
from collections import OrderedDict
from contextlib import contextmanager
from datetime import datetime, timedelta
import ddt
from django.conf import settings
from django.db.utils import IntegrityError
from mock import patch
from mock import patch, MagicMock
import pytz
from util.date_utils import to_timestamp
from unittest import skip
from student.models import anonymous_id_for_user
......@@ -23,7 +26,7 @@ from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, chec
from lms.djangoapps.grades.config.models import PersistentGradesEnabledFlag
from lms.djangoapps.grades.signals.signals import PROBLEM_WEIGHTED_SCORE_CHANGED
from lms.djangoapps.grades.tasks import recalculate_subsection_grade
from lms.djangoapps.grades.tasks import recalculate_subsection_grade_v2
@patch.dict(settings.FEATURES, {'PERSISTENT_GRADES_ENABLED_FOR_ALL_TESTS': False})
......@@ -54,6 +57,9 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
self.sequential = ItemFactory.create(parent=self.chapter, category='sequential', display_name="Sequential1")
self.problem = ItemFactory.create(parent=self.sequential, category='problem', display_name='Problem')
self.frozen_now_datetime = datetime.now().replace(tzinfo=pytz.UTC)
self.frozen_now_timestamp = to_timestamp(self.frozen_now_datetime)
self.problem_weighted_score_changed_kwargs = OrderedDict([
('weighted_earned', 1.0),
('weighted_possible', 2.0),
......@@ -61,6 +67,7 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
('course_id', unicode(self.course.id)),
('usage_id', unicode(self.problem.location)),
('only_if_higher', None),
('modified', self.frozen_now_datetime),
])
create_new_event_transaction_id()
......@@ -70,8 +77,7 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
('course_id', unicode(self.course.id)),
('usage_id', unicode(self.problem.location)),
('only_if_higher', None),
('weighted_earned', 1.0),
('weighted_possible', 2.0),
('expected_modified_time', self.frozen_now_timestamp),
('score_deleted', False),
('event_transaction_id', unicode(get_event_transaction_id())),
('event_transaction_type', u'edx.grades.problem.submitted'),
......@@ -81,6 +87,15 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
_ = anonymous_id_for_user(self.user, self.course.id)
# pylint: enable=attribute-defined-outside-init,no-member
@contextmanager
def mock_get_score(self, score=MagicMock(grade=1.0, max_grade=2.0)):
"""
Mocks the scores needed by the SCORE_PUBLISHED signal
handler. By default, sets the returned score to 1/2.
"""
with patch("lms.djangoapps.grades.tasks.get_score", return_value=score):
yield
def test_problem_weighted_score_changed_queues_task(self):
"""
Ensures that the PROBLEM_WEIGHTED_SCORE_CHANGED signal enqueues the correct task.
......@@ -89,8 +104,8 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
send_args = self.problem_weighted_score_changed_kwargs
local_task_args = self.recalculate_subsection_grade_kwargs.copy()
local_task_args['event_transaction_type'] = u'edx.grades.problem.submitted'
with patch(
'lms.djangoapps.grades.tasks.recalculate_subsection_grade.apply_async',
with self.mock_get_score() and patch(
'lms.djangoapps.grades.tasks.recalculate_subsection_grade_v2.apply_async',
return_value=None
) as mock_task_apply:
PROBLEM_WEIGHTED_SCORE_CHANGED.send(sender=None, **send_args)
......@@ -186,9 +201,9 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
with self.store.default_store(default_store):
self.set_up_course()
with check_mongo_calls(0) and self.assertNumQueries(3 if feature_flag else 2):
recalculate_subsection_grade.apply(kwargs=self.recalculate_subsection_grade_kwargs)
self._apply_recalculate_subsection_grade()
@patch('lms.djangoapps.grades.tasks.recalculate_subsection_grade.retry')
@patch('lms.djangoapps.grades.tasks.recalculate_subsection_grade_v2.retry')
@patch('lms.djangoapps.grades.new.subsection_grade.SubsectionGradeFactory.update')
def test_retry_subsection_update_on_integrity_error(self, mock_update, mock_retry):
"""
......@@ -199,18 +214,18 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
self._apply_recalculate_subsection_grade()
self._assert_retry_called(mock_retry)
@skip # Pending completion of TNL-5995
@patch('lms.djangoapps.grades.tasks.recalculate_subsection_grade.retry')
@patch('lms.djangoapps.grades.tasks.recalculate_subsection_grade_v2.retry')
def test_retry_subsection_grade_on_update_not_complete(self, mock_retry):
self.set_up_course()
self._apply_recalculate_subsection_grade()
self._apply_recalculate_subsection_grade(
mock_score=MagicMock(modified=datetime.utcnow().replace(tzinfo=pytz.UTC) - timedelta(days=1))
)
self._assert_retry_called(mock_retry)
@skip # Pending completion of TNL-5995
@patch('lms.djangoapps.grades.tasks.recalculate_subsection_grade.retry')
@patch('lms.djangoapps.grades.tasks.recalculate_subsection_grade_v2.retry')
def test_retry_subsection_grade_on_no_score(self, mock_retry):
self.set_up_course()
self._apply_recalculate_subsection_grade()
self._apply_recalculate_subsection_grade(mock_score=None)
self._assert_retry_called(mock_retry)
@patch('lms.djangoapps.grades.signals.signals.SUBSECTION_SCORE_CHANGED.send')
......@@ -224,12 +239,16 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
self._apply_recalculate_subsection_grade()
self.assertEquals(mock_course_signal.call_count, 1)
def _apply_recalculate_subsection_grade(self):
def _apply_recalculate_subsection_grade(
self,
mock_score=MagicMock(modified=datetime.utcnow().replace(tzinfo=pytz.UTC) + timedelta(days=1))
):
"""
Calls the recalculate_subsection_grade task with necessary
mocking in place.
"""
recalculate_subsection_grade.apply(kwargs=self.recalculate_subsection_grade_kwargs)
with self.mock_get_score(mock_score):
recalculate_subsection_grade_v2.apply(kwargs=self.recalculate_subsection_grade_kwargs)
def _assert_retry_called(self, mock_retry):
"""
......
......@@ -7,12 +7,18 @@ Does not include any access control, be sure to check access before calling.
import json
import logging
from django.conf import settings
from datetime import datetime
from django.contrib.auth.models import User
from django.conf import settings
from django.core.mail import send_mail
from django.core.urlresolvers import reverse
from django.utils.translation import override as override_language
from eventtracking import tracker
import pytz
from course_modes.models import CourseMode
from courseware.models import StudentModule
from edxmako.shortcuts import render_to_string
from lms.djangoapps.grades.signals.signals import PROBLEM_RAW_SCORE_CHANGED
from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
......@@ -321,7 +327,7 @@ def _fire_score_changed_for_block(
"""
Fires a PROBLEM_RAW_SCORE_CHANGED event for the given module.
The earned points are always zero. We must retrieve the possible points
from the XModule, as noted below.
from the XModule, as noted below. The effective time is now().
"""
if block and block.has_score and block.max_score() is not None:
PROBLEM_RAW_SCORE_CHANGED.send(
......@@ -334,6 +340,7 @@ def _fire_score_changed_for_block(
usage_id=unicode(module_state_key),
score_deleted=True,
only_if_higher=False,
modified=datetime.now().replace(tzinfo=pytz.UTC),
)
......
......@@ -37,9 +37,11 @@ Our next steps would be to:
import HTMLParser
import collections
from datetime import datetime, timedelta
import json
import mock
import sys
import pytz
import unittest
from bs4 import BeautifulSoup
......@@ -202,6 +204,8 @@ class GradePublishTestMixin(object):
'usage': usage_key,
'score': score,
'max_score': max_score})
# Shim a return time, defaults to 1 hour before now
return datetime.now().replace(tzinfo=pytz.UTC) - timedelta(hours=1)
self.scores = []
patcher = mock.patch("lms.djangoapps.grades.signals.handlers.set_score", capture_score)
......
......@@ -76,8 +76,8 @@ git+https://github.com/edx/XBlock.git@xblock-0.4.12#egg=XBlock==0.4.12
-e git+https://github.com/edx/event-tracking.git@0.2.1#egg=event-tracking==0.2.1
-e git+https://github.com/edx/django-splash.git@v0.2#egg=django-splash==0.2
-e git+https://github.com/edx/acid-block.git@e46f9cda8a03e121a00c7e347084d142d22ebfb7#egg=acid-xblock
git+https://github.com/edx/edx-ora2.git@1.1.11#egg=ora2==1.1.11
-e git+https://github.com/edx/edx-submissions.git@1.1.1#egg=edx-submissions==1.1.1
git+https://github.com/edx/edx-ora2.git@1.1.12#egg=ora2==1.1.12
-e git+https://github.com/edx/edx-submissions.git@1.1.4#egg=edx-submissions==1.1.4
git+https://github.com/edx/ease.git@release-2015-07-14#egg=ease==0.1.3
git+https://github.com/edx/i18n-tools.git@v0.3.2#egg=i18n-tools==v0.3.2
git+https://github.com/edx/edx-val.git@0.0.11#egg=edxval==0.0.11
......
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