Merge pull request #20719 from edx/dcs/grade-support
API support for bulk grade import/export
This commit is contained in:
@@ -2,9 +2,17 @@
|
||||
"""
|
||||
Python APIs exposed by the grades app to other in-process apps.
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from datetime import datetime
|
||||
|
||||
import pytz
|
||||
from six import text_type
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
||||
# Public Grades Factories
|
||||
from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory
|
||||
from lms.djangoapps.grades.subsection_grade import CreateSubsectionGrade
|
||||
from lms.djangoapps.grades.subsection_grade_factory import SubsectionGradeFactory
|
||||
|
||||
# Public Grades Functions
|
||||
@@ -12,7 +20,7 @@ from lms.djangoapps.grades.models_api import *
|
||||
from lms.djangoapps.grades.tasks import compute_all_grades_for_course as task_compute_all_grades_for_course
|
||||
|
||||
# Public Grades Modules
|
||||
from lms.djangoapps.grades import events, constants, context
|
||||
from lms.djangoapps.grades import events, constants, context, course_data
|
||||
from lms.djangoapps.grades.signals import signals
|
||||
from lms.djangoapps.grades.util_services import GradesUtilService
|
||||
|
||||
@@ -22,3 +30,125 @@ from lms.djangoapps.grades.signals.handlers import disconnect_submissions_signal
|
||||
# Grades APIs that should NOT belong within the Grades subsystem
|
||||
# TODO move Gradebook to be an external feature outside of core Grades
|
||||
from lms.djangoapps.grades.config.waffle import is_writable_gradebook_enabled
|
||||
from lms.djangoapps.utils import _get_key
|
||||
|
||||
from opaque_keys.edx.keys import CourseKey, UsageKey
|
||||
from track.event_transaction_utils import create_new_event_transaction_id, set_event_transaction_type
|
||||
|
||||
|
||||
def graded_subsections_for_course_id(course_id):
|
||||
"""
|
||||
Return graded subsections for the course.
|
||||
"""
|
||||
from lms.djangoapps.grades.context import graded_subsections_for_course
|
||||
return graded_subsections_for_course(course_data.CourseData(user=None, course_key=course_id).collected_structure)
|
||||
|
||||
|
||||
def override_subsection_grade(
|
||||
user_id, course_key_or_id, usage_key_or_id, overrider=None, earned_all=None, earned_graded=None,
|
||||
feature=constants.GradeOverrideFeatureEnum.proctoring
|
||||
):
|
||||
"""
|
||||
Creates a PersistentSubsectionGradeOverride corresponding to the given
|
||||
user, course, and usage_key.
|
||||
Will also create a ``PersistentSubsectionGrade`` for this (user, course, usage_key)
|
||||
if none currently exists.
|
||||
|
||||
Fires off a recalculate_subsection_grade async task to update the PersistentCourseGrade table.
|
||||
Will not override ``earned_all`` or ``earned_graded`` value if they are ``None``.
|
||||
Both of these parameters have ``None`` as their default value.
|
||||
"""
|
||||
course_key = _get_key(course_key_or_id, CourseKey)
|
||||
usage_key = _get_key(usage_key_or_id, UsageKey)
|
||||
|
||||
try:
|
||||
grade = get_subsection_grade(user_id, usage_key.course_key, usage_key)
|
||||
except ObjectDoesNotExist:
|
||||
grade = _create_subsection_grade(user_id, course_key, usage_key)
|
||||
|
||||
override = update_or_create_override(
|
||||
grade,
|
||||
requesting_user=overrider,
|
||||
subsection_grade_model=grade,
|
||||
feature=feature,
|
||||
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(events.SUBSECTION_OVERRIDE_EVENT_TYPE)
|
||||
|
||||
# This will eventually trigger a re-computation of the course grade,
|
||||
# taking the new PersistentSubsectionGradeOverride into account.
|
||||
signals.SUBSECTION_OVERRIDE_CHANGED.send(
|
||||
sender=None,
|
||||
user_id=user_id,
|
||||
course_id=text_type(course_key),
|
||||
usage_id=text_type(usage_key),
|
||||
only_if_higher=False,
|
||||
modified=override.modified,
|
||||
score_deleted=False,
|
||||
score_db_table=constants.ScoreDatabaseTableEnum.overrides
|
||||
)
|
||||
|
||||
|
||||
def undo_override_subsection_grade(user_id, course_key_or_id, usage_key_or_id, feature=''):
|
||||
"""
|
||||
Delete the override subsection grade row (the PersistentSubsectionGrade model must already exist)
|
||||
|
||||
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.
|
||||
"""
|
||||
course_key = _get_key(course_key_or_id, CourseKey)
|
||||
usage_key = _get_key(usage_key_or_id, UsageKey)
|
||||
|
||||
try:
|
||||
override = get_subsection_grade_override(user_id, course_key, usage_key)
|
||||
except ObjectDoesNotExist:
|
||||
return
|
||||
|
||||
# Older rejected exam attempts that transition to verified might not have an override created
|
||||
if override is not None:
|
||||
override.delete(feature=feature)
|
||||
|
||||
# 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(events.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.
|
||||
signals.SUBSECTION_OVERRIDE_CHANGED.send(
|
||||
sender=None,
|
||||
user_id=user_id,
|
||||
course_id=text_type(course_key),
|
||||
usage_id=text_type(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=constants.ScoreDatabaseTableEnum.overrides
|
||||
)
|
||||
|
||||
|
||||
def should_override_grade_on_rejected_exam(course_key_or_id):
|
||||
"""Convienence function to return the state of the CourseWaffleFlag REJECTED_EXAM_OVERRIDES_GRADE"""
|
||||
from .config.waffle import waffle_flags, REJECTED_EXAM_OVERRIDES_GRADE
|
||||
course_key = _get_key(course_key_or_id, CourseKey)
|
||||
return waffle_flags()[REJECTED_EXAM_OVERRIDES_GRADE].is_enabled(course_key)
|
||||
|
||||
|
||||
def _create_subsection_grade(user_id, course_key, usage_key):
|
||||
"""
|
||||
Given a user_id, course_key, and subsection usage_key,
|
||||
creates a new ``PersistentSubsectionGrade``.
|
||||
"""
|
||||
from lms.djangoapps.courseware.courses import get_course
|
||||
from django.contrib.auth import get_user_model
|
||||
course = get_course(course_key, depth=None)
|
||||
subsection = course.get_child(usage_key)
|
||||
if not subsection:
|
||||
raise Exception('Subsection with given usage_key does not exist.')
|
||||
user = get_user_model().objects.get(id=user_id)
|
||||
subsection_grade = CreateSubsectionGrade(subsection, course_data.CourseData(user, course=course).structure, {}, {})
|
||||
return subsection_grade.update_or_create_model(user, force_update_subsections=True)
|
||||
|
||||
@@ -736,6 +736,15 @@ class PersistentSubsectionGradeOverride(models.Model):
|
||||
)
|
||||
return cleaned_data
|
||||
|
||||
def delete(self, **kwargs): # pylint: disable=arguments-differ
|
||||
# TODO: a proper history table
|
||||
PersistentSubsectionGradeOverrideHistory.objects.create(
|
||||
override_id=self.id,
|
||||
feature=kwargs.pop('feature', ''),
|
||||
action=PersistentSubsectionGradeOverrideHistory.DELETE
|
||||
)
|
||||
super(PersistentSubsectionGradeOverride, self).delete(**kwargs)
|
||||
|
||||
|
||||
class PersistentSubsectionGradeOverrideHistory(models.Model):
|
||||
"""
|
||||
|
||||
@@ -7,6 +7,9 @@ from lms.djangoapps.grades.models import (
|
||||
PersistentSubsectionGradeOverride as _PersistentSubsectionGradeOverride,
|
||||
VisibleBlocks as _VisibleBlocks,
|
||||
)
|
||||
from lms.djangoapps.utils import _get_key
|
||||
|
||||
from opaque_keys.edx.keys import CourseKey, UsageKey
|
||||
|
||||
|
||||
def prefetch_grade_overrides_and_visible_blocks(user, course_key):
|
||||
@@ -46,3 +49,51 @@ def get_recently_modified_grades(course_keys, start_date, end_date):
|
||||
grade_filter_args['modified__lte'] = end_date
|
||||
|
||||
return _PersistentCourseGrade.objects.filter(**grade_filter_args).order_by('modified')
|
||||
|
||||
|
||||
def update_or_create_override(grade, **kwargs):
|
||||
"""
|
||||
Update or creates a subsection override.
|
||||
"""
|
||||
kwargs['subsection_grade_model'] = grade
|
||||
return _PersistentSubsectionGradeOverride.update_or_create_override(**kwargs)
|
||||
|
||||
|
||||
def get_subsection_grade(user_id, course_key_or_id, usage_key_or_id):
|
||||
"""
|
||||
Find and return the earned subsection grade for user
|
||||
"""
|
||||
course_key = _get_key(course_key_or_id, CourseKey)
|
||||
usage_key = _get_key(usage_key_or_id, UsageKey)
|
||||
|
||||
return _PersistentSubsectionGrade.objects.get(
|
||||
user_id=user_id,
|
||||
course_id=course_key,
|
||||
usage_key=usage_key
|
||||
)
|
||||
|
||||
|
||||
def get_subsection_grades(user_id, course_key_or_id):
|
||||
"""
|
||||
Return dictionary of grades for user_id.
|
||||
"""
|
||||
course_key = _get_key(course_key_or_id, CourseKey)
|
||||
grades = {}
|
||||
for grade in _PersistentSubsectionGrade.bulk_read_grades(user_id, course_key):
|
||||
grades[grade.usage_key] = grade
|
||||
return grades
|
||||
|
||||
|
||||
def get_subsection_grade_override(user_id, course_key_or_id, usage_key_or_id):
|
||||
"""
|
||||
Finds the subsection grade for user and returns the override for that grade if it exists
|
||||
|
||||
If override does not exist, returns None. If subsection grade does not exist, will raise an exception.
|
||||
"""
|
||||
usage_key = _get_key(usage_key_or_id, UsageKey)
|
||||
|
||||
# Verify that a corresponding subsection grade exists for the given user and usage_key
|
||||
# Raises PersistentSubsectionGrade.DoesNotExist if it does not exist.
|
||||
_ = get_subsection_grade(user_id, course_key_or_id, usage_key_or_id)
|
||||
|
||||
return _PersistentSubsectionGradeOverride.get_override(user_id, usage_key)
|
||||
|
||||
@@ -1,30 +1,8 @@
|
||||
"""
|
||||
Grade service
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
import pytz
|
||||
from six import text_type
|
||||
|
||||
from lms.djangoapps.grades.course_data import CourseData
|
||||
from lms.djangoapps.grades.subsection_grade import CreateSubsectionGrade
|
||||
from lms.djangoapps.utils import _get_key
|
||||
from opaque_keys.edx.keys import CourseKey, UsageKey
|
||||
from track.event_transaction_utils import create_new_event_transaction_id, set_event_transaction_type
|
||||
|
||||
from .config.waffle import waffle_flags, REJECTED_EXAM_OVERRIDES_GRADE
|
||||
from .constants import ScoreDatabaseTableEnum, GradeOverrideFeatureEnum
|
||||
from .events import SUBSECTION_OVERRIDE_EVENT_TYPE
|
||||
from .models import (
|
||||
PersistentSubsectionGrade,
|
||||
PersistentSubsectionGradeOverride,
|
||||
PersistentSubsectionGradeOverrideHistory
|
||||
)
|
||||
from .signals.signals import SUBSECTION_OVERRIDE_CHANGED
|
||||
|
||||
|
||||
USER_MODEL = get_user_model()
|
||||
from __future__ import absolute_import
|
||||
from . import api
|
||||
|
||||
|
||||
class GradesService(object):
|
||||
@@ -38,14 +16,7 @@ class GradesService(object):
|
||||
"""
|
||||
Finds and returns the earned subsection grade for user
|
||||
"""
|
||||
course_key = _get_key(course_key_or_id, CourseKey)
|
||||
usage_key = _get_key(usage_key_or_id, UsageKey)
|
||||
|
||||
return PersistentSubsectionGrade.objects.get(
|
||||
user_id=user_id,
|
||||
course_id=course_key,
|
||||
usage_key=usage_key
|
||||
)
|
||||
return api.get_subsection_grade(user_id, course_key_or_id, usage_key_or_id)
|
||||
|
||||
def get_subsection_grade_override(self, user_id, course_key_or_id, usage_key_or_id):
|
||||
"""
|
||||
@@ -53,16 +24,11 @@ class GradesService(object):
|
||||
|
||||
If override does not exist, returns None. If subsection grade does not exist, will raise an exception.
|
||||
"""
|
||||
usage_key = _get_key(usage_key_or_id, UsageKey)
|
||||
|
||||
# Verify that a corresponding subsection grade exists for the given user and usage_key
|
||||
# Raises PersistentSubsectionGrade.DoesNotExist if it does not exist.
|
||||
_ = self.get_subsection_grade(user_id, course_key_or_id, usage_key_or_id)
|
||||
|
||||
return PersistentSubsectionGradeOverride.get_override(user_id, usage_key)
|
||||
return api.get_subsection_grade_override(user_id, course_key_or_id, usage_key_or_id)
|
||||
|
||||
def override_subsection_grade(
|
||||
self, user_id, course_key_or_id, usage_key_or_id, earned_all=None, earned_graded=None
|
||||
self, user_id, course_key_or_id, usage_key_or_id, earned_all=None, earned_graded=None,
|
||||
feature=api.constants.GradeOverrideFeatureEnum.proctoring
|
||||
):
|
||||
"""
|
||||
Creates a PersistentSubsectionGradeOverride corresponding to the given
|
||||
@@ -74,100 +40,23 @@ class GradesService(object):
|
||||
Will not override ``earned_all`` or ``earned_graded`` value if they are ``None``.
|
||||
Both of these parameters have ``None`` as their default value.
|
||||
"""
|
||||
course_key = _get_key(course_key_or_id, CourseKey)
|
||||
usage_key = _get_key(usage_key_or_id, UsageKey)
|
||||
return api.override_subsection_grade(user_id,
|
||||
course_key_or_id,
|
||||
usage_key_or_id,
|
||||
earned_all=earned_all,
|
||||
earned_graded=earned_graded,
|
||||
feature=feature)
|
||||
|
||||
try:
|
||||
grade = PersistentSubsectionGrade.read_grade(
|
||||
user_id=user_id,
|
||||
usage_key=usage_key
|
||||
)
|
||||
except PersistentSubsectionGrade.DoesNotExist:
|
||||
grade = self._create_subsection_grade(user_id, course_key, usage_key)
|
||||
|
||||
override = PersistentSubsectionGradeOverride.update_or_create_override(
|
||||
requesting_user=None,
|
||||
subsection_grade_model=grade,
|
||||
feature=GradeOverrideFeatureEnum.proctoring,
|
||||
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)
|
||||
|
||||
# This will eventually trigger a re-computation of the course grade,
|
||||
# taking the new PersistentSubsectionGradeOverride into account.
|
||||
SUBSECTION_OVERRIDE_CHANGED.send(
|
||||
sender=None,
|
||||
user_id=user_id,
|
||||
course_id=text_type(course_key),
|
||||
usage_id=text_type(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,
|
||||
feature=api.constants.GradeOverrideFeatureEnum.proctoring):
|
||||
"""
|
||||
Delete the override subsection grade row (the PersistentSubsectionGrade model must already exist)
|
||||
|
||||
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.
|
||||
"""
|
||||
course_key = _get_key(course_key_or_id, CourseKey)
|
||||
usage_key = _get_key(usage_key_or_id, UsageKey)
|
||||
|
||||
try:
|
||||
override = self.get_subsection_grade_override(user_id, course_key, usage_key)
|
||||
except PersistentSubsectionGrade.DoesNotExist:
|
||||
return
|
||||
|
||||
# Older rejected exam attempts that transition to verified might not have an override created
|
||||
if override is not None:
|
||||
_ = PersistentSubsectionGradeOverrideHistory.objects.create(
|
||||
override_id=override.id,
|
||||
feature=GradeOverrideFeatureEnum.proctoring,
|
||||
action=PersistentSubsectionGradeOverrideHistory.DELETE
|
||||
)
|
||||
override.delete()
|
||||
|
||||
# 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 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=text_type(course_key),
|
||||
usage_id=text_type(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
|
||||
)
|
||||
return api.undo_override_subsection_grade(user_id, course_key_or_id, usage_key_or_id, feature=feature)
|
||||
|
||||
def should_override_grade_on_rejected_exam(self, course_key_or_id):
|
||||
"""Convienence function to return the state of the CourseWaffleFlag REJECTED_EXAM_OVERRIDES_GRADE"""
|
||||
course_key = _get_key(course_key_or_id, CourseKey)
|
||||
return waffle_flags()[REJECTED_EXAM_OVERRIDES_GRADE].is_enabled(course_key)
|
||||
|
||||
def _create_subsection_grade(self, user_id, course_key, usage_key):
|
||||
"""
|
||||
Given a user_id, course_key, and subsection usage_key,
|
||||
creates a new ``PersistentSubsectionGrade``.
|
||||
"""
|
||||
from lms.djangoapps.courseware.courses import get_course
|
||||
course = get_course(course_key, depth=None)
|
||||
subsection = course.get_child(usage_key)
|
||||
if not subsection:
|
||||
raise Exception('Subsection with given usage_key does not exist.')
|
||||
user = USER_MODEL.objects.get(id=user_id)
|
||||
course_data = CourseData(user, course=course)
|
||||
subsection_grade = CreateSubsectionGrade(subsection, course_data.structure, {}, {})
|
||||
return subsection_grade.update_or_create_model(user, force_update_subsections=True)
|
||||
return api.should_override_grade_on_rejected_exam(course_key_or_id)
|
||||
|
||||
@@ -29,7 +29,6 @@ from .config.waffle import DISABLE_REGRADE_ON_POLICY_CHANGE, waffle
|
||||
from .constants import ScoreDatabaseTableEnum
|
||||
from .course_grade_factory import CourseGradeFactory
|
||||
from .exceptions import DatabaseNotReadyError
|
||||
from .services import GradesService
|
||||
from .signals.signals import SUBSECTION_SCORE_CHANGED
|
||||
from .subsection_grade_factory import SubsectionGradeFactory
|
||||
from .transformer import GradesTransformer
|
||||
@@ -270,7 +269,8 @@ def _has_db_updated_with_new_score(self, scored_block_usage_key, **kwargs):
|
||||
found_modified_time = score['created_at'] if score is not None else None
|
||||
else:
|
||||
assert kwargs['score_db_table'] == ScoreDatabaseTableEnum.overrides
|
||||
score = GradesService().get_subsection_grade_override(
|
||||
from . import api
|
||||
score = api.get_subsection_grade_override(
|
||||
user_id=kwargs['user_id'],
|
||||
course_key_or_id=kwargs['course_id'],
|
||||
usage_key_or_id=kwargs['usage_id']
|
||||
|
||||
@@ -68,12 +68,12 @@ class GradesServiceTests(ModuleStoreTestCase):
|
||||
)
|
||||
self.signal_patcher = patch('lms.djangoapps.grades.signals.signals.SUBSECTION_OVERRIDE_CHANGED.send')
|
||||
self.mock_signal = self.signal_patcher.start()
|
||||
self.id_patcher = patch('lms.djangoapps.grades.services.create_new_event_transaction_id')
|
||||
self.id_patcher = patch('lms.djangoapps.grades.api.create_new_event_transaction_id')
|
||||
self.mock_create_id = self.id_patcher.start()
|
||||
self.mock_create_id.return_value = 1
|
||||
self.type_patcher = patch('lms.djangoapps.grades.services.set_event_transaction_type')
|
||||
self.type_patcher = patch('lms.djangoapps.grades.api.set_event_transaction_type')
|
||||
self.mock_set_type = self.type_patcher.start()
|
||||
self.flag_patcher = patch('lms.djangoapps.grades.services.waffle_flags')
|
||||
self.flag_patcher = patch('lms.djangoapps.grades.config.waffle.waffle_flags')
|
||||
self.mock_waffle_flags = self.flag_patcher.start()
|
||||
self.mock_waffle_flags.return_value = {
|
||||
REJECTED_EXAM_OVERRIDES_GRADE: MockWaffleFlag(True)
|
||||
|
||||
@@ -310,7 +310,7 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
|
||||
)
|
||||
else:
|
||||
with patch(
|
||||
'lms.djangoapps.grades.tasks.GradesService',
|
||||
'lms.djangoapps.grades.api',
|
||||
return_value=MockGradesService(mocked_return_value=MagicMock(modified=modified_datetime))
|
||||
):
|
||||
recalculate_subsection_grade_v3.apply(kwargs=self.recalculate_subsection_grade_kwargs)
|
||||
@@ -343,7 +343,7 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
|
||||
mock_score=MagicMock(module_type='any_block_type')
|
||||
)
|
||||
elif score_db_table == ScoreDatabaseTableEnum.overrides:
|
||||
with patch('lms.djangoapps.grades.tasks.GradesService',
|
||||
with patch('lms.djangoapps.grades.api',
|
||||
return_value=MockGradesService(mocked_return_value=None)) as mock_service:
|
||||
mock_service.get_subsection_grade_override.return_value = None
|
||||
recalculate_subsection_grade_v3.apply(kwargs=self.recalculate_subsection_grade_kwargs)
|
||||
@@ -653,7 +653,7 @@ class FreezeGradingAfterCourseEndTest(HasCourseWithProblemsMixin, ModuleStoreTes
|
||||
with override_waffle_flag(self.freeze_grade_flag, active=freeze_flag_value):
|
||||
modified_datetime = datetime.utcnow().replace(tzinfo=pytz.UTC) - timedelta(days=1)
|
||||
with patch(
|
||||
'lms.djangoapps.grades.tasks.GradesService',
|
||||
'lms.djangoapps.grades.api',
|
||||
return_value=MockGradesService(mocked_return_value=MagicMock(modified=modified_datetime))
|
||||
) as mock_grade_service:
|
||||
result = recalculate_subsection_grade_v3.apply_async(kwargs=self.recalculate_subsection_grade_kwargs)
|
||||
|
||||
@@ -219,7 +219,7 @@ def get_cohort(user, course_key, assign=True, use_cached=False):
|
||||
Raises:
|
||||
ValueError if the CourseKey doesn't exist.
|
||||
"""
|
||||
if user.is_anonymous:
|
||||
if user is None or user.is_anonymous:
|
||||
return None
|
||||
cache = RequestCache(COHORT_CACHE_NAMESPACE).data
|
||||
cache_key = _cohort_cache_key(user.id, course_key)
|
||||
|
||||
Reference in New Issue
Block a user