Commit a9c1e1cf by Tyler Hallada

Squashed commit of the following:

commit 12880fa21eac0c2e69b2d832d42829f00484561d
Author: Tyler Hallada <thallada@edx.org>
Date:   Fri Aug 11 11:17:22 2017 -0400

    Tweak exam override messaging in progress.html

commit 6f90352dc27b3a199eea7efaf84b72df50f570a9
Merge: e0e9f83789 7c535f5f
Author: Tyler Hallada <thallada@edx.org>
Date:   Thu Aug 10 17:38:13 2017 -0400

    Merge remote-tracking branch 'origin/master' into EDUCATOR-926

commit e0e9f83789efa837577cedbcd5807c6fdba249e4
Author: Tyler Hallada <thallada@edx.org>
Date:   Thu Aug 10 17:35:26 2017 -0400

    Revert "Comment out actual override and log instead"

    This reverts commit 4953cf30.

commit 27d6b537c8915ecd1af8af51c8f9509db6ea0a26
Author: Tyler Hallada <thallada@edx.org>
Date:   Thu Aug 10 17:34:56 2017 -0400

    Revert "Fix tests"

    This reverts commit 1b2fec21.

commit cabddd504ddcb4f92aeed8b5fec4bff0d64a8de3
Author: Tyler Hallada <thallada@edx.org>
Date:   Thu Aug 10 17:34:36 2017 -0400

    Revert "Remove override behavior and log instead"

    This reverts commit 1ab18803.

commit dc9407f4606804e00d5aee4e80d309c7d7f9c437
Author: Tyler Hallada <thallada@edx.org>
Date:   Thu Aug 10 17:34:15 2017 -0400

    Revert "Fix python tests and use proctoring 1.0.0"

    This reverts commit 842ce836.
parent 7c535f5f
...@@ -412,7 +412,22 @@ class PersistentSubsectionGrade(DeleteGradesMixin, TimeStampedModel): ...@@ -412,7 +412,22 @@ class PersistentSubsectionGrade(DeleteGradesMixin, TimeStampedModel):
usage_key = params.pop('usage_key') usage_key = params.pop('usage_key')
# apply grade override if one exists before saving model # apply grade override if one exists before saving model
# EDUCTATOR-1127: remove override until this behavior is verified in production try:
override = PersistentSubsectionGradeOverride.objects.get(
grade__user_id=user_id,
grade__course_id=usage_key.course_key,
grade__usage_key=usage_key,
)
if override.earned_all_override is not None:
params['earned_all'] = override.earned_all_override
if override.possible_all_override is not None:
params['possible_all'] = override.possible_all_override
if override.earned_graded_override is not None:
params['earned_graded'] = override.earned_graded_override
if override.possible_graded_override is not None:
params['possible_graded'] = override.possible_graded_override
except PersistentSubsectionGradeOverride.DoesNotExist:
pass
grade, _ = cls.objects.update_or_create( grade, _ = cls.objects.update_or_create(
user_id=user_id, user_id=user_id,
......
from datetime import datetime from datetime import datetime
import logging
import pytz import pytz
from opaque_keys.edx.keys import CourseKey, UsageKey from opaque_keys.edx.keys import CourseKey, UsageKey
...@@ -12,8 +11,6 @@ from .constants import ScoreDatabaseTableEnum ...@@ -12,8 +11,6 @@ from .constants import ScoreDatabaseTableEnum
from .models import PersistentSubsectionGrade, PersistentSubsectionGradeOverride from .models import PersistentSubsectionGrade, PersistentSubsectionGradeOverride
from .signals.signals import SUBSECTION_OVERRIDE_CHANGED from .signals.signals import SUBSECTION_OVERRIDE_CHANGED
log = logging.getLogger(__name__)
def _get_key(key_or_id, key_cls): def _get_key(key_or_id, key_cls):
""" """
...@@ -73,21 +70,40 @@ class GradesService(object): ...@@ -73,21 +70,40 @@ class GradesService(object):
Fires off a recalculate_subsection_grade async task to update the PersistentSubsectionGrade table. Will not Fires off a recalculate_subsection_grade async task to update the PersistentSubsectionGrade table. Will not
override earned_all or earned_graded value if they are None. Both default to None. override earned_all or earned_graded value if they are None. Both default to None.
""" """
# prevent circular imports:
from .signals.handlers import SUBSECTION_OVERRIDE_EVENT_TYPE
course_key = _get_key(course_key_or_id, CourseKey) course_key = _get_key(course_key_or_id, CourseKey)
usage_key = _get_key(usage_key_or_id, UsageKey) usage_key = _get_key(usage_key_or_id, UsageKey)
log.info( grade = PersistentSubsectionGrade.objects.get(
u"EDUCATOR-1127: Subsection grade override for user {user_id} on subsection {usage_key} in course " user_id=user_id,
u"{course_key} would be created with params: {params}" course_id=course_key,
.format( usage_key=usage_key
user_id=unicode(user_id), )
usage_key=unicode(usage_key),
course_key=unicode(course_key), # Create override that will prevent any future updates to grade
params=unicode({ override, _ = PersistentSubsectionGradeOverride.objects.update_or_create(
'earned_all': earned_all, grade=grade,
'earned_graded': earned_graded, earned_all_override=earned_all,
}) earned_graded_override=earned_graded
) )
# Cache a new event id and event type which the signal handler will use to emit a tracking log event.
create_new_event_transaction_id()
set_event_transaction_type(SUBSECTION_OVERRIDE_EVENT_TYPE)
# Signal will trigger subsection recalculation which will call PersistentSubsectionGrade.update_or_create_grade
# which will use the above override to update the grade before writing to the table.
SUBSECTION_OVERRIDE_CHANGED.send(
sender=None,
user_id=user_id,
course_id=unicode(course_key),
usage_id=unicode(usage_key),
only_if_higher=False,
modified=override.modified,
score_deleted=False,
score_db_table=ScoreDatabaseTableEnum.overrides
) )
def undo_override_subsection_grade(self, user_id, course_key_or_id, usage_key_or_id): def undo_override_subsection_grade(self, user_id, course_key_or_id, usage_key_or_id):
...@@ -97,17 +113,33 @@ class GradesService(object): ...@@ -97,17 +113,33 @@ class GradesService(object):
Fires off a recalculate_subsection_grade async task to update the PersistentSubsectionGrade table. If the Fires off a recalculate_subsection_grade async task to update the PersistentSubsectionGrade table. If the
override does not exist, no error is raised, it just triggers the recalculation. override does not exist, no error is raised, it just triggers the recalculation.
""" """
# prevent circular imports:
from .signals.handlers import SUBSECTION_OVERRIDE_EVENT_TYPE
course_key = _get_key(course_key_or_id, CourseKey) course_key = _get_key(course_key_or_id, CourseKey)
usage_key = _get_key(usage_key_or_id, UsageKey) usage_key = _get_key(usage_key_or_id, UsageKey)
log.info( override = self.get_subsection_grade_override(user_id, course_key, usage_key)
u"EDUCATOR-1127: Subsection grade override for user {user_id} on subsection {usage_key} in course " # Older rejected exam attempts that transition to verified might not have an override created
u"{course_key} would be deleted" if override is not None:
.format( override.delete()
user_id=unicode(user_id),
usage_key=unicode(usage_key), # Cache a new event id and event type which the signal handler will use to emit a tracking log event.
course_key=unicode(course_key) create_new_event_transaction_id()
) set_event_transaction_type(SUBSECTION_OVERRIDE_EVENT_TYPE)
# Signal will trigger subsection recalculation which will call PersistentSubsectionGrade.update_or_create_grade
# which will no longer use the above deleted override, and instead return the grade to the original score from
# the actual problem responses before writing to the table.
SUBSECTION_OVERRIDE_CHANGED.send(
sender=None,
user_id=user_id,
course_id=unicode(course_key),
usage_id=unicode(usage_key),
only_if_higher=False,
modified=datetime.now().replace(tzinfo=pytz.UTC), # Not used when score_deleted=True
score_deleted=True,
score_db_table=ScoreDatabaseTableEnum.overrides
) )
def should_override_grade_on_rejected_exam(self, course_key_or_id): def should_override_grade_on_rejected_exam(self, course_key_or_id):
......
...@@ -312,9 +312,8 @@ class PersistentSubsectionGradeTest(GradesModelTestCase): ...@@ -312,9 +312,8 @@ class PersistentSubsectionGradeTest(GradesModelTestCase):
override = PersistentSubsectionGradeOverride(grade=grade, earned_all_override=0.0, earned_graded_override=0.0) override = PersistentSubsectionGradeOverride(grade=grade, earned_all_override=0.0, earned_graded_override=0.0)
override.save() override.save()
grade = PersistentSubsectionGrade.update_or_create_grade(**self.params) grade = PersistentSubsectionGrade.update_or_create_grade(**self.params)
# EDUCATOR-1127 Override is not enabled yet, change to 0.0 when enabled self.assertEqual(grade.earned_all, 0.0)
self.assertEqual(grade.earned_all, 6.0) self.assertEqual(grade.earned_graded, 0.0)
self.assertEqual(grade.earned_graded, 6.0)
def _assert_tracker_emitted_event(self, tracker_mock, grade): def _assert_tracker_emitted_event(self, tracker_mock, grade):
""" """
......
...@@ -171,10 +171,28 @@ class GradesServiceTests(ModuleStoreTestCase): ...@@ -171,10 +171,28 @@ class GradesServiceTests(ModuleStoreTestCase):
self.course.id, self.course.id,
self.subsection.location self.subsection.location
) )
self.assertIsNone(override_obj) self.assertIsNotNone(override_obj)
self.assertEqual(override_obj.earned_all_override, override['earned_all'])
self.assertEqual(override_obj.earned_graded_override, override['earned_graded'])
self.assertEqual(
self.mock_signal.call_args,
call(
sender=None,
user_id=self.user.id,
course_id=unicode(self.course.id),
usage_id=unicode(self.subsection.location),
only_if_higher=False,
modified=override_obj.modified,
score_deleted=False,
score_db_table=ScoreDatabaseTableEnum.overrides
)
)
@freeze_time('2017-01-01') @freeze_time('2017-01-01')
def test_undo_override_subsection_grade(self): def test_undo_override_subsection_grade(self):
override, _ = PersistentSubsectionGradeOverride.objects.update_or_create(grade=self.grade)
self.service.undo_override_subsection_grade( self.service.undo_override_subsection_grade(
user_id=self.user.id, user_id=self.user.id,
course_key_or_id=self.course.id, course_key_or_id=self.course.id,
...@@ -184,6 +202,20 @@ class GradesServiceTests(ModuleStoreTestCase): ...@@ -184,6 +202,20 @@ class GradesServiceTests(ModuleStoreTestCase):
override = self.service.get_subsection_grade_override(self.user.id, self.course.id, self.subsection.location) override = self.service.get_subsection_grade_override(self.user.id, self.course.id, self.subsection.location)
self.assertIsNone(override) self.assertIsNone(override)
self.assertEqual(
self.mock_signal.call_args,
call(
sender=None,
user_id=self.user.id,
course_id=unicode(self.course.id),
usage_id=unicode(self.subsection.location),
only_if_higher=False,
modified=datetime.now().replace(tzinfo=pytz.UTC),
score_deleted=True,
score_db_table=ScoreDatabaseTableEnum.overrides
)
)
@ddt.data( @ddt.data(
['edX/DemoX/Demo_Course', CourseKey.from_string('edX/DemoX/Demo_Course'), CourseKey], ['edX/DemoX/Demo_Course', CourseKey.from_string('edX/DemoX/Demo_Course'), CourseKey],
['course-v1:edX+DemoX+Demo_Course', CourseKey.from_string('course-v1:edX+DemoX+Demo_Course'), CourseKey], ['course-v1:edX+DemoX+Demo_Course', CourseKey.from_string('course-v1:edX+DemoX+Demo_Course'), CourseKey],
......
...@@ -163,10 +163,10 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest ...@@ -163,10 +163,10 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
self.assertEquals(mock_block_structure_create.call_count, 1) self.assertEquals(mock_block_structure_create.call_count, 1)
@ddt.data( @ddt.data(
(ModuleStoreEnum.Type.mongo, 1, 29, True), (ModuleStoreEnum.Type.mongo, 1, 30, True),
(ModuleStoreEnum.Type.mongo, 1, 25, False), (ModuleStoreEnum.Type.mongo, 1, 26, False),
(ModuleStoreEnum.Type.split, 3, 29, True), (ModuleStoreEnum.Type.split, 3, 30, True),
(ModuleStoreEnum.Type.split, 3, 25, False), (ModuleStoreEnum.Type.split, 3, 26, False),
) )
@ddt.unpack @ddt.unpack
def test_query_counts(self, default_store, num_mongo_calls, num_sql_calls, create_multiple_subsections): def test_query_counts(self, default_store, num_mongo_calls, num_sql_calls, create_multiple_subsections):
...@@ -178,8 +178,8 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest ...@@ -178,8 +178,8 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
self._apply_recalculate_subsection_grade() self._apply_recalculate_subsection_grade()
@ddt.data( @ddt.data(
(ModuleStoreEnum.Type.mongo, 1, 29), (ModuleStoreEnum.Type.mongo, 1, 30),
(ModuleStoreEnum.Type.split, 3, 29), (ModuleStoreEnum.Type.split, 3, 30),
) )
@ddt.unpack @ddt.unpack
def test_query_counts_dont_change_with_more_content(self, default_store, num_mongo_calls, num_sql_calls): def test_query_counts_dont_change_with_more_content(self, default_store, num_mongo_calls, num_sql_calls):
...@@ -239,8 +239,8 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest ...@@ -239,8 +239,8 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
self.assertEqual(len(PersistentSubsectionGrade.bulk_read_grades(self.user.id, self.course.id)), 0) self.assertEqual(len(PersistentSubsectionGrade.bulk_read_grades(self.user.id, self.course.id)), 0)
@ddt.data( @ddt.data(
(ModuleStoreEnum.Type.mongo, 1, 26), (ModuleStoreEnum.Type.mongo, 1, 27),
(ModuleStoreEnum.Type.split, 3, 26), (ModuleStoreEnum.Type.split, 3, 27),
) )
@ddt.unpack @ddt.unpack
def test_persistent_grades_enabled_on_course(self, default_store, num_mongo_queries, num_sql_queries): def test_persistent_grades_enabled_on_course(self, default_store, num_mongo_queries, num_sql_queries):
......
...@@ -183,18 +183,15 @@ from django.utils.http import urlquote_plus ...@@ -183,18 +183,15 @@ from django.utils.http import urlquote_plus
<em class="localized-datetime" data-datetime="${section.due}" data-string="${_('due {date}')}" data-timezone="${user_timezone}" data-language="${user_language}"></em> <em class="localized-datetime" data-datetime="${section.due}" data-string="${_('due {date}')}" data-timezone="${user_timezone}" data-language="${user_language}"></em>
%endif %endif
</p> </p>
<%doc> <p class="override-notice">
EDUCATOR-1127: Do not display override notice until override is enabled
%if section.override is not None: %if section.override is not None:
<p class="override-notice"> %if section.format is not None and section.format == "Exam":
%if section.format is not None and section.format == "Exam": ${_("Suspicious activity detected during proctored exam review. Exam score 0.")}
${_("Exam grade has been overridden due to a failed proctoring review.")} %else:
%else: ${_("Section grade has been overridden.")}
${_("Section grade has been overridden.")} %endif
%endif
</p>
%endif %endif
</%doc> </p>
%if len(section.problem_scores.values()) > 0: %if len(section.problem_scores.values()) > 0:
%if section.show_grades(staff_access): %if section.show_grades(staff_access):
<dl class="scores"> <dl class="scores">
......
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