Commit f8e3a3c4 by Nimisha Asthagiri

Grades Cleanup

EDUCATOR-1405
EDUCATOR-1451
parent 0de55de2
......@@ -9,7 +9,6 @@ WAFFLE_NAMESPACE = u'grades'
# Switches
ASSUME_ZERO_GRADE_IF_ABSENT = u'assume_zero_grade_if_absent'
ESTIMATE_FIRST_ATTEMPTED = u'estimate_first_attempted'
DISABLE_REGRADE_ON_POLICY_CHANGE = u'disable_regrade_on_policy_change'
# Course Flags
......
......@@ -14,10 +14,6 @@ from .subsection_grade import ZeroSubsectionGrade
from .subsection_grade_factory import SubsectionGradeFactory
def uniqueify(iterable):
return OrderedDict([(item, None) for item in iterable]).keys()
class CourseGradeBase(object):
"""
Base class for Course Grades.
......@@ -194,7 +190,7 @@ class CourseGradeBase(object):
"""
return [
self._get_subsection_grade(course_structure[subsection_key])
for subsection_key in uniqueify(course_structure.get_children(chapter_key))
for subsection_key in _uniqueify_and_keep_order(course_structure.get_children(chapter_key))
]
@abstractmethod
......@@ -229,6 +225,11 @@ class CourseGrade(CourseGradeBase):
if self.force_update_subsections is true, via the lazy call
to self.grader_result.
"""
# TODO update this code to be more functional and readable.
# Currently, it is hard to follow since there are plenty of
# side-effects. Once functional, force_update_subsections
# can be passed through and not confusingly stored and used
# at a later time.
grade_cutoffs = self.course_data.course.grade_cutoffs
self.percent = self._compute_percent(self.grader_result)
self.letter_grade = self._compute_letter_grade(grade_cutoffs, self.percent)
......@@ -289,3 +290,7 @@ class CourseGrade(CourseGradeBase):
nonzero_cutoffs = [cutoff for cutoff in grade_cutoffs.values() if cutoff > 0]
success_cutoff = min(nonzero_cutoffs) if nonzero_cutoffs else None
return success_cutoff and percent >= success_cutoff
def _uniqueify_and_keep_order(iterable):
return OrderedDict([(item, None) for item in iterable]).keys()
from collections import namedtuple
from contextlib import contextmanager
from logging import getLogger
import dogstats_wrapper as dog_stats_api
......@@ -9,7 +8,7 @@ from openedx.core.djangoapps.signals.signals import COURSE_GRADE_CHANGED, COURSE
from .config import assume_zero_if_absent, should_persist_grades
from .course_data import CourseData
from .course_grade import CourseGrade, ZeroCourseGrade
from .models import PersistentCourseGrade, VisibleBlocks
from .models import PersistentCourseGrade, prefetch
log = getLogger(__name__)
......@@ -95,18 +94,9 @@ class CourseGradeFactory(object):
user=None, course=course, collected_block_structure=collected_block_structure, course_key=course_key,
)
stats_tags = [u'action:{}'.format(course_data.course_key)]
with self._course_transaction(course_data.course_key):
for user in users:
with dog_stats_api.timer('lms.grades.CourseGradeFactory.iter', tags=stats_tags):
yield self._iter_grade_result(user, course_data, force_update)
@contextmanager
def _course_transaction(self, course_key):
"""
Provides a transaction context in which GradeResults are created.
"""
yield
VisibleBlocks.clear_cache(course_key)
for user in users:
with dog_stats_api.timer('lms.grades.CourseGradeFactory.iter', tags=stats_tags):
yield self._iter_grade_result(user, course_data, force_update)
def _iter_grade_result(self, user, course_data, force_update):
try:
......@@ -169,13 +159,15 @@ class CourseGradeFactory(object):
Sends a COURSE_GRADE_CHANGED signal to listeners and a
COURSE_GRADE_NOW_PASSED if learner has passed course.
"""
should_persist = should_persist_grades(course_data.course_key)
if should_persist and force_update_subsections:
prefetch(user, course_data.course_key)
course_grade = CourseGrade(user, course_data, force_update_subsections=force_update_subsections)
course_grade = course_grade.update()
should_persist = (
should_persist_grades(course_data.course_key) and
course_grade.attempted
)
should_persist = should_persist and course_grade.attempted
if should_persist:
course_grade._subsection_grade_factory.bulk_create_unsaved()
PersistentCourseGrade.update_or_create(
......
from crum import get_current_user
from eventtracking import tracker
from track import contexts
from track.event_transaction_utils import (
create_new_event_transaction_id,
get_event_transaction_id,
get_event_transaction_type,
set_event_transaction_type
)
COURSE_GRADE_CALCULATED = u'edx.grades.course.grade_calculated'
GRADES_OVERRIDE_EVENT_TYPE = u'edx.grades.problem.score_overridden'
GRADES_RESCORE_EVENT_TYPE = u'edx.grades.problem.rescored'
PROBLEM_SUBMITTED_EVENT_TYPE = u'edx.grades.problem.submitted'
STATE_DELETED_EVENT_TYPE = u'edx.grades.problem.state_deleted'
SUBSECTION_OVERRIDE_EVENT_TYPE = u'edx.grades.subsection.score_overridden'
SUBSECTION_GRADE_CALCULATED = u'edx.grades.subsection.grade_calculated'
def grade_updated(**kwargs):
"""
Emits the appropriate grade-related event after checking for which
event-transaction is active.
Emits a problem.submitted event only if there is no current event
transaction type, i.e. we have not reached this point in the code via
an outer event type (such as problem.rescored or score_overridden).
"""
root_type = get_event_transaction_type()
if not root_type:
root_id = get_event_transaction_id()
if not root_id:
root_id = create_new_event_transaction_id()
set_event_transaction_type(PROBLEM_SUBMITTED_EVENT_TYPE)
tracker.emit(
unicode(PROBLEM_SUBMITTED_EVENT_TYPE),
{
'user_id': unicode(kwargs['user_id']),
'course_id': unicode(kwargs['course_id']),
'problem_id': unicode(kwargs['usage_id']),
'event_transaction_id': unicode(root_id),
'event_transaction_type': unicode(PROBLEM_SUBMITTED_EVENT_TYPE),
'weighted_earned': kwargs.get('weighted_earned'),
'weighted_possible': kwargs.get('weighted_possible'),
}
)
elif root_type in [GRADES_RESCORE_EVENT_TYPE, GRADES_OVERRIDE_EVENT_TYPE]:
current_user = get_current_user()
instructor_id = getattr(current_user, 'id', None)
tracker.emit(
unicode(root_type),
{
'course_id': unicode(kwargs['course_id']),
'user_id': unicode(kwargs['user_id']),
'problem_id': unicode(kwargs['usage_id']),
'new_weighted_earned': kwargs.get('weighted_earned'),
'new_weighted_possible': kwargs.get('weighted_possible'),
'only_if_higher': kwargs.get('only_if_higher'),
'instructor_id': unicode(instructor_id),
'event_transaction_id': unicode(get_event_transaction_id()),
'event_transaction_type': unicode(root_type),
}
)
elif root_type in [SUBSECTION_OVERRIDE_EVENT_TYPE]:
tracker.emit(
unicode(root_type),
{
'course_id': unicode(kwargs['course_id']),
'user_id': unicode(kwargs['user_id']),
'problem_id': unicode(kwargs['usage_id']),
'only_if_higher': kwargs.get('only_if_higher'),
'override_deleted': kwargs.get('score_deleted', False),
'event_transaction_id': unicode(get_event_transaction_id()),
'event_transaction_type': unicode(root_type),
}
)
def subsection_grade_calculated(subsection_grade):
"""
Emits an edx.grades.subsection.grade_calculated event
with data from the passed subsection_grade.
"""
event_name = SUBSECTION_GRADE_CALCULATED
context = contexts.course_context_from_course_id(subsection_grade.course_id)
# TODO (AN-6134): remove this context manager
with tracker.get_tracker().context(event_name, context):
tracker.emit(
event_name,
{
'user_id': unicode(subsection_grade.user_id),
'course_id': unicode(subsection_grade.course_id),
'block_id': unicode(subsection_grade.usage_key),
'course_version': unicode(subsection_grade.course_version),
'weighted_total_earned': subsection_grade.earned_all,
'weighted_total_possible': subsection_grade.possible_all,
'weighted_graded_earned': subsection_grade.earned_graded,
'weighted_graded_possible': subsection_grade.possible_graded,
'first_attempted': unicode(subsection_grade.first_attempted),
'subtree_edited_timestamp': unicode(subsection_grade.subtree_edited_timestamp),
'event_transaction_id': unicode(get_event_transaction_id()),
'event_transaction_type': unicode(get_event_transaction_type()),
'visible_blocks_hash': unicode(subsection_grade.visible_blocks_id),
}
)
def course_grade_calculated(course_grade):
"""
Emits an edx.grades.course.grade_calculated event
with data from the passed course_grade.
"""
event_name = COURSE_GRADE_CALCULATED
context = contexts.course_context_from_course_id(course_grade.course_id)
# TODO (AN-6134): remove this context manager
with tracker.get_tracker().context(event_name, context):
tracker.emit(
event_name,
{
'user_id': unicode(course_grade.user_id),
'course_id': unicode(course_grade.course_id),
'course_version': unicode(course_grade.course_version),
'percent_grade': course_grade.percent_grade,
'letter_grade': unicode(course_grade.letter_grade),
'course_edited_timestamp': unicode(course_grade.course_edited_timestamp),
'event_transaction_id': unicode(get_event_transaction_id()),
'event_transaction_type': unicode(get_event_transaction_type()),
'grading_policy_hash': unicode(course_grade.grading_policy_hash),
}
)
......@@ -13,7 +13,7 @@ from pytz import utc
from courseware.models import StudentModule
from lms.djangoapps.grades.constants import ScoreDatabaseTableEnum
from lms.djangoapps.grades.signals.handlers import PROBLEM_SUBMITTED_EVENT_TYPE
from lms.djangoapps.grades.events import PROBLEM_SUBMITTED_EVENT_TYPE
from lms.djangoapps.grades.tasks import recalculate_subsection_grade_v3
from student.models import user_by_anonymous_id
from submissions.models import Submission
......
......@@ -21,13 +21,11 @@ from model_utils.models import TimeStampedModel
from opaque_keys.edx.keys import CourseKey, UsageKey
from coursewarehistoryextended.fields import UnsignedBigIntAutoField, UnsignedBigIntOneToOneField
from eventtracking import tracker
from openedx.core.djangoapps.xmodule_django.models import CourseKeyField, UsageKeyField
from request_cache import get_cache
from track import contexts
from track.event_transaction_utils import get_event_transaction_id, get_event_transaction_type
from .config import waffle
import events
log = logging.getLogger(__name__)
......@@ -122,24 +120,6 @@ class BlockRecordList(tuple):
return cls(blocks, course_key)
class VisibleBlocksQuerySet(models.QuerySet):
"""
A custom QuerySet representing VisibleBlocks.
"""
def create_from_blockrecords(self, blocks):
"""
Creates a new VisibleBlocks model object.
Argument 'blocks' should be a BlockRecordList.
"""
model, _ = self.get_or_create(
hashed=blocks.hash_value,
defaults={u'blocks_json': blocks.json_value, u'course_id': blocks.course_key},
)
return model
class VisibleBlocks(models.Model):
"""
A django model used to track the state of a set of visible blocks under a
......@@ -149,12 +129,11 @@ class VisibleBlocks(models.Model):
in the blocks_json field. A hash of this json array is used for lookup
purposes.
"""
CACHE_NAMESPACE = u"grades.models.VisibleBlocks"
blocks_json = models.TextField()
hashed = models.CharField(max_length=100, unique=True)
course_id = CourseKeyField(blank=False, max_length=255, db_index=True)
objects = VisibleBlocksQuerySet.as_manager()
_CACHE_NAMESPACE = u"grades.models.VisibleBlocks"
class Meta(object):
app_label = "grades"
......@@ -184,12 +163,29 @@ class VisibleBlocks(models.Model):
Arguments:
course_key: The course identifier for the desired records
"""
prefetched = get_cache(cls.CACHE_NAMESPACE).get(cls._cache_key(course_key))
if not prefetched:
prefetched = get_cache(cls._CACHE_NAMESPACE).get(cls._cache_key(course_key), None)
if prefetched is None:
prefetched = cls._initialize_cache(course_key)
return prefetched
@classmethod
def cached_get_or_create(cls, blocks):
prefetched = get_cache(cls._CACHE_NAMESPACE).get(cls._cache_key(blocks.course_key))
if prefetched is not None:
model = prefetched.get(blocks.hash_value)
if not model:
model = cls.objects.create(
hashed=blocks.hash_value, blocks_json=blocks.json_value, course_id=blocks.course_key,
)
cls._update_cache(blocks.course_key, [model])
else:
model, _ = cls.objects.get_or_create(
hashed=blocks.hash_value,
defaults={u'blocks_json': blocks.json_value, u'course_id': blocks.course_key},
)
return model
@classmethod
def bulk_create(cls, course_key, block_record_lists):
"""
Bulk creates VisibleBlocks for the given iterator of
......@@ -227,7 +223,7 @@ class VisibleBlocks(models.Model):
block record objects.
"""
prefetched = {record.hashed: record for record in cls.objects.filter(course_id=course_key)}
get_cache(cls.CACHE_NAMESPACE)[cls._cache_key(course_key)] = prefetched
get_cache(cls._CACHE_NAMESPACE)[cls._cache_key(course_key)] = prefetched
return prefetched
@classmethod
......@@ -236,19 +232,11 @@ class VisibleBlocks(models.Model):
Adds a specific set of visible blocks to the request cache.
This assumes that prefetch has already been called.
"""
get_cache(cls.CACHE_NAMESPACE)[cls._cache_key(course_key)].update(
get_cache(cls._CACHE_NAMESPACE)[cls._cache_key(course_key)].update(
{visible_block.hashed: visible_block for visible_block in visible_blocks}
)
@classmethod
def clear_cache(cls, course_key):
"""
Clears the cache of all contents for a given course.
"""
cache = get_cache(cls.CACHE_NAMESPACE)
cache.pop(cls._cache_key(course_key), None)
@classmethod
def _cache_key(cls, course_key):
return u"visible_blocks_cache.{}".format(course_key)
......@@ -348,7 +336,7 @@ class PersistentSubsectionGrade(TimeStampedModel):
Raises PersistentSubsectionGrade.DoesNotExist if applicable
"""
return cls.objects.select_related('visible_blocks').get(
return cls.objects.select_related('visible_blocks', 'override').get(
user_id=user_id,
course_id=usage_key.course_key, # course_id is included to take advantage of db indexes
usage_key=usage_key,
......@@ -363,7 +351,7 @@ class PersistentSubsectionGrade(TimeStampedModel):
user_id: The user associated with the desired grades
course_key: The course identifier for the desired grades
"""
return cls.objects.select_related('visible_blocks').filter(
return cls.objects.select_related('visible_blocks', 'override').filter(
user_id=user_id,
course_id=course_key,
)
......@@ -373,30 +361,15 @@ class PersistentSubsectionGrade(TimeStampedModel):
"""
Wrapper for objects.update_or_create.
"""
cls._prepare_params_and_visible_blocks(params)
cls._prepare_params(params)
VisibleBlocks.cached_get_or_create(params['visible_blocks'])
cls._prepare_params_visible_blocks_id(params)
cls._prepare_params_override(params)
first_attempted = params.pop('first_attempted')
user_id = params.pop('user_id')
usage_key = params.pop('usage_key')
# apply grade override if one exists before saving model
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(
user_id=user_id,
course_id=usage_key.course_key,
......@@ -404,48 +377,27 @@ class PersistentSubsectionGrade(TimeStampedModel):
defaults=params,
)
if first_attempted is not None and grade.first_attempted is None:
if waffle.waffle().is_enabled(waffle.ESTIMATE_FIRST_ATTEMPTED):
grade.first_attempted = first_attempted
else:
grade.first_attempted = now()
grade.first_attempted = first_attempted
grade.save()
cls._emit_grade_calculated_event(grade)
return grade
@classmethod
def _prepare_first_attempted_for_create(cls, params):
"""
Update the value of 'first_attempted' to now() if we aren't
using score-based estimates.
"""
if params['first_attempted'] is not None and not waffle.waffle().is_enabled(waffle.ESTIMATE_FIRST_ATTEMPTED):
params['first_attempted'] = now()
@classmethod
def create_grade(cls, **params):
"""
Wrapper for objects.create.
"""
cls._prepare_params_and_visible_blocks(params)
cls._prepare_first_attempted_for_create(params)
grade = cls.objects.create(**params)
cls._emit_grade_calculated_event(grade)
return grade
@classmethod
def bulk_create_grades(cls, grade_params_iter, course_key):
def bulk_create_grades(cls, grade_params_iter, user_id, course_key):
"""
Bulk creation of grades.
"""
if not grade_params_iter:
return
PersistentSubsectionGradeOverride.prefetch(user_id, course_key)
map(cls._prepare_params, grade_params_iter)
VisibleBlocks.bulk_get_or_create([params['visible_blocks'] for params in grade_params_iter], course_key)
map(cls._prepare_params_visible_blocks_id, grade_params_iter)
map(cls._prepare_first_attempted_for_create, grade_params_iter)
map(cls._prepare_params_override, grade_params_iter)
grades = [PersistentSubsectionGrade(**params) for params in grade_params_iter]
grades = cls.objects.bulk_create(grades)
for grade in grades:
......@@ -453,15 +405,6 @@ class PersistentSubsectionGrade(TimeStampedModel):
return grades
@classmethod
def _prepare_params_and_visible_blocks(cls, params):
"""
Prepares the fields for the grade record, while
creating the related VisibleBlocks, if needed.
"""
cls._prepare_params(params)
params['visible_blocks'] = VisibleBlocks.objects.create_from_blockrecords(params['visible_blocks'])
@classmethod
def _prepare_params(cls, params):
"""
Prepares the fields for the grade record.
......@@ -484,34 +427,22 @@ class PersistentSubsectionGrade(TimeStampedModel):
params['visible_blocks_id'] = params['visible_blocks'].hash_value
del params['visible_blocks']
@classmethod
def _prepare_params_override(cls, params):
override = PersistentSubsectionGradeOverride.get_override(params['user_id'], params['usage_key'])
if override:
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
@staticmethod
def _emit_grade_calculated_event(grade):
"""
Emits an edx.grades.subsection.grade_calculated event
with data from the passed grade.
"""
# TODO: remove this context manager after completion of AN-6134
event_name = u'edx.grades.subsection.grade_calculated'
context = contexts.course_context_from_course_id(grade.course_id)
with tracker.get_tracker().context(event_name, context):
tracker.emit(
event_name,
{
'user_id': unicode(grade.user_id),
'course_id': unicode(grade.course_id),
'block_id': unicode(grade.usage_key),
'course_version': unicode(grade.course_version),
'weighted_total_earned': grade.earned_all,
'weighted_total_possible': grade.possible_all,
'weighted_graded_earned': grade.earned_graded,
'weighted_graded_possible': grade.possible_graded,
'first_attempted': unicode(grade.first_attempted),
'subtree_edited_timestamp': unicode(grade.subtree_edited_timestamp),
'event_transaction_id': unicode(get_event_transaction_id()),
'event_transaction_type': unicode(get_event_transaction_type()),
'visible_blocks_hash': unicode(grade.visible_blocks_id),
}
)
events.subsection_grade_calculated(grade)
class PersistentCourseGrade(TimeStampedModel):
......@@ -553,7 +484,7 @@ class PersistentCourseGrade(TimeStampedModel):
# Information related to course completion
passed_timestamp = models.DateTimeField(u'Date learner earned a passing grade', blank=True, null=True)
CACHE_NAMESPACE = u"grades.models.PersistentCourseGrade"
_CACHE_NAMESPACE = u"grades.models.PersistentCourseGrade"
def __unicode__(self):
"""
......@@ -569,15 +500,11 @@ class PersistentCourseGrade(TimeStampedModel):
])
@classmethod
def _cache_key(cls, course_id):
return u"grades_cache.{}".format(course_id)
@classmethod
def prefetch(cls, course_id, users):
"""
Prefetches grades for the given users for the given course.
"""
get_cache(cls.CACHE_NAMESPACE)[cls._cache_key(course_id)] = {
get_cache(cls._CACHE_NAMESPACE)[cls._cache_key(course_id)] = {
grade.user_id: grade
for grade in
cls.objects.filter(user_id__in=[user.id for user in users], course_id=course_id)
......@@ -595,7 +522,7 @@ class PersistentCourseGrade(TimeStampedModel):
Raises PersistentCourseGrade.DoesNotExist if applicable
"""
try:
prefetched_grades = get_cache(cls.CACHE_NAMESPACE)[cls._cache_key(course_id)]
prefetched_grades = get_cache(cls._CACHE_NAMESPACE)[cls._cache_key(course_id)]
try:
return prefetched_grades[user_id]
except KeyError:
......@@ -625,33 +552,24 @@ class PersistentCourseGrade(TimeStampedModel):
if passed and not grade.passed_timestamp:
grade.passed_timestamp = now()
grade.save()
cls._emit_grade_calculated_event(grade)
cls._update_cache(course_id, user_id, grade)
return grade
@classmethod
def _update_cache(cls, course_id, user_id, grade):
course_cache = get_cache(cls._CACHE_NAMESPACE).get(cls._cache_key(course_id))
if course_cache is not None:
course_cache[user_id] = grade
@classmethod
def _cache_key(cls, course_id):
return u"grades_cache.{}".format(course_id)
@staticmethod
def _emit_grade_calculated_event(grade):
"""
Emits an edx.grades.course.grade_calculated event
with data from the passed grade.
"""
# TODO: remove this context manager after completion of AN-6134
event_name = u'edx.grades.course.grade_calculated'
context = contexts.course_context_from_course_id(grade.course_id)
with tracker.get_tracker().context(event_name, context):
tracker.emit(
event_name,
{
'user_id': unicode(grade.user_id),
'course_id': unicode(grade.course_id),
'course_version': unicode(grade.course_version),
'percent_grade': grade.percent_grade,
'letter_grade': unicode(grade.letter_grade),
'course_edited_timestamp': unicode(grade.course_edited_timestamp),
'event_transaction_id': unicode(get_event_transaction_id()),
'event_transaction_type': unicode(get_event_transaction_type()),
'grading_policy_hash': unicode(grade.grading_policy_hash),
}
)
events.course_grade_calculated(grade)
class PersistentSubsectionGradeOverride(models.Model):
......@@ -673,3 +591,32 @@ class PersistentSubsectionGradeOverride(models.Model):
possible_all_override = models.FloatField(null=True, blank=True)
earned_graded_override = models.FloatField(null=True, blank=True)
possible_graded_override = models.FloatField(null=True, blank=True)
_CACHE_NAMESPACE = u"grades.models.PersistentSubsectionGradeOverride"
@classmethod
def prefetch(cls, user_id, course_key):
get_cache(cls._CACHE_NAMESPACE)[(user_id, str(course_key))] = {
override.grade.usage_key: override
for override in
cls.objects.filter(grade__user_id=user_id, grade__course_id=course_key)
}
@classmethod
def get_override(cls, user_id, usage_key):
prefetch_values = get_cache(cls._CACHE_NAMESPACE).get((user_id, str(usage_key.course_key)), None)
if prefetch_values is not None:
return prefetch_values.get(usage_key)
try:
return cls.objects.get(
grade__user_id=user_id,
grade__course_id=usage_key.course_key,
grade__usage_key=usage_key,
)
except PersistentSubsectionGradeOverride.DoesNotExist:
pass
def prefetch(user, course_key):
PersistentSubsectionGradeOverride.prefetch(user.id, course_key)
VisibleBlocks.bulk_read(course_key)
......@@ -4,10 +4,10 @@ import pytz
from opaque_keys.edx.keys import CourseKey, UsageKey
from track.event_transaction_utils import create_new_event_transaction_id, set_event_transaction_type
from util.date_utils import to_timestamp
from .config.waffle import waffle_flags, REJECTED_EXAM_OVERRIDES_GRADE
from .constants import ScoreDatabaseTableEnum
from .events import SUBSECTION_OVERRIDE_EVENT_TYPE
from .models import PersistentSubsectionGrade, PersistentSubsectionGradeOverride
from .signals.signals import SUBSECTION_OVERRIDE_CHANGED
......@@ -70,9 +70,6 @@ class GradesService(object):
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.
"""
# prevent circular imports:
from .signals.handlers import SUBSECTION_OVERRIDE_EVENT_TYPE
course_key = _get_key(course_key_or_id, CourseKey)
usage_key = _get_key(usage_key_or_id, UsageKey)
......@@ -113,9 +110,6 @@ class GradesService(object):
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.
"""
# prevent circular imports:
from .signals.handlers import SUBSECTION_OVERRIDE_EVENT_TYPE
course_key = _get_key(course_key_or_id, CourseKey)
usage_key = _get_key(usage_key_or_id, UsageKey)
......
......@@ -5,21 +5,13 @@ from contextlib import contextmanager
from logging import getLogger
from courseware.model_data import get_score, set_score
from crum import get_current_user
from django.dispatch import receiver
from eventtracking import tracker
from lms.djangoapps.instructor_task.tasks_helper.module_state import GRADES_OVERRIDE_EVENT_TYPE
from openedx.core.djangoapps.course_groups.signals.signals import COHORT_MEMBERSHIP_UPDATED
from openedx.core.lib.grade_utils import is_score_higher_or_equal
from student.models import user_by_anonymous_id
from student.signals.signals import ENROLLMENT_TRACK_UPDATED
from submissions.models import score_reset, score_set
from track.event_transaction_utils import (
create_new_event_transaction_id,
get_event_transaction_id,
get_event_transaction_type,
set_event_transaction_type
)
from track.event_transaction_utils import get_event_transaction_id, get_event_transaction_type
from util.date_utils import to_timestamp
from xblock.scorable import ScorableXBlockMixin, Score
......@@ -32,17 +24,12 @@ from .signals import (
)
from ..constants import ScoreDatabaseTableEnum
from ..course_grade_factory import CourseGradeFactory
from .. import events
from ..scores import weighted_score
from ..tasks import RECALCULATE_GRADE_DELAY, recalculate_subsection_grade_v3
log = getLogger(__name__)
# define values to be used in grading events
GRADES_RESCORE_EVENT_TYPE = 'edx.grades.problem.rescored'
PROBLEM_SUBMITTED_EVENT_TYPE = 'edx.grades.problem.submitted'
SUBSECTION_OVERRIDE_EVENT_TYPE = 'edx.grades.subsection.score_overridden'
STATE_DELETED_EVENT_TYPE = 'edx.grades.problem.state_deleted'
@receiver(score_set)
def submissions_score_set_handler(sender, **kwargs): # pylint: disable=unused-argument
......@@ -127,7 +114,7 @@ def disconnect_submissions_signal_receiver(signal):
handler = submissions_score_set_handler
else:
if signal != score_reset:
raise ValueError("This context manager only deal with score_set and score_reset signals.")
raise ValueError("This context manager only handles score_set and score_reset signals.")
handler = submissions_score_reset_handler
signal.disconnect(handler)
......@@ -220,8 +207,8 @@ def enqueue_subsection_update(sender, **kwargs): # pylint: disable=unused-argum
Handles the PROBLEM_WEIGHTED_SCORE_CHANGED or SUBSECTION_OVERRIDE_CHANGED signals by
enqueueing a subsection update operation to occur asynchronously.
"""
_emit_event(kwargs)
result = recalculate_subsection_grade_v3.apply_async(
events.grade_updated(**kwargs)
recalculate_subsection_grade_v3.apply_async(
kwargs=dict(
user_id=kwargs['user_id'],
anonymous_user_id=kwargs.get('anonymous_user_id'),
......@@ -249,7 +236,7 @@ def recalculate_course_grade_only(sender, course, course_structure, user, **kwar
@receiver(ENROLLMENT_TRACK_UPDATED)
@receiver(COHORT_MEMBERSHIP_UPDATED)
def force_recalculate_course_and_subsection_grades(sender, user, course_key, **kwargs):
def recalculate_course_and_subsection_grades(sender, user, course_key, **kwargs):
"""
Updates a saved course grade, forcing the subsection grades
from which it is calculated to update along the way.
......@@ -257,65 +244,3 @@ def force_recalculate_course_and_subsection_grades(sender, user, course_key, **k
previous_course_grade = CourseGradeFactory().read(user, course_key=course_key)
if previous_course_grade and previous_course_grade.attempted:
CourseGradeFactory().update(user=user, course_key=course_key, force_update_subsections=True)
def _emit_event(kwargs):
"""
Emits a problem submitted event only if there is no current event
transaction type, i.e. we have not reached this point in the code via a
rescore or student state deletion.
If the event transaction type has already been set and the transacation is
a rescore, emits a problem rescored event.
"""
root_type = get_event_transaction_type()
if not root_type:
root_id = get_event_transaction_id()
if not root_id:
root_id = create_new_event_transaction_id()
set_event_transaction_type(PROBLEM_SUBMITTED_EVENT_TYPE)
tracker.emit(
unicode(PROBLEM_SUBMITTED_EVENT_TYPE),
{
'user_id': unicode(kwargs['user_id']),
'course_id': unicode(kwargs['course_id']),
'problem_id': unicode(kwargs['usage_id']),
'event_transaction_id': unicode(root_id),
'event_transaction_type': unicode(PROBLEM_SUBMITTED_EVENT_TYPE),
'weighted_earned': kwargs.get('weighted_earned'),
'weighted_possible': kwargs.get('weighted_possible'),
}
)
if root_type in [GRADES_RESCORE_EVENT_TYPE, GRADES_OVERRIDE_EVENT_TYPE]:
current_user = get_current_user()
instructor_id = getattr(current_user, 'id', None)
tracker.emit(
unicode(GRADES_RESCORE_EVENT_TYPE),
{
'course_id': unicode(kwargs['course_id']),
'user_id': unicode(kwargs['user_id']),
'problem_id': unicode(kwargs['usage_id']),
'new_weighted_earned': kwargs.get('weighted_earned'),
'new_weighted_possible': kwargs.get('weighted_possible'),
'only_if_higher': kwargs.get('only_if_higher'),
'instructor_id': unicode(instructor_id),
'event_transaction_id': unicode(get_event_transaction_id()),
'event_transaction_type': unicode(root_type),
}
)
if root_type in [SUBSECTION_OVERRIDE_EVENT_TYPE]:
tracker.emit(
unicode(SUBSECTION_OVERRIDE_EVENT_TYPE),
{
'course_id': unicode(kwargs['course_id']),
'user_id': unicode(kwargs['user_id']),
'problem_id': unicode(kwargs['usage_id']),
'only_if_higher': kwargs.get('only_if_higher'),
'override_deleted': kwargs.get('score_deleted', False),
'event_transaction_id': unicode(get_event_transaction_id()),
'event_transaction_type': unicode(root_type),
}
)
......@@ -101,46 +101,57 @@ class SubsectionGrade(SubsectionGradeBase):
"""
Class for Subsection Grades.
"""
def __init__(self, subsection):
def __init__(self, subsection, problem_scores, all_total, graded_total, override=None):
super(SubsectionGrade, self).__init__(subsection)
self.problem_scores = OrderedDict() # dict of problem locations to ProblemScore
self.problem_scores = problem_scores
self.all_total = all_total
self.graded_total = graded_total
self.override = override
def init_from_structure(self, student, course_structure, submissions_scores, csm_scores):
@classmethod
def create(cls, subsection, course_structure, submissions_scores, csm_scores):
"""
Compute the grade of this subsection for the given student and course.
Compute and create the subsection grade.
"""
for descendant_key in course_structure.post_order_traversal(
problem_scores = OrderedDict()
for block_key in course_structure.post_order_traversal(
filter_func=possibly_scored,
start_node=self.location,
start_node=subsection.location,
):
self._compute_block_score(descendant_key, course_structure, submissions_scores, csm_scores)
problem_score = cls._compute_block_score(block_key, course_structure, submissions_scores, csm_scores)
if problem_score:
problem_scores[block_key] = problem_score
all_total, graded_total = graders.aggregate_scores(problem_scores.values())
self.all_total, self.graded_total = graders.aggregate_scores(self.problem_scores.values())
self._log_event(log.debug, u"init_from_structure", student)
return self
return cls(subsection, problem_scores, all_total, graded_total)
def init_from_model(self, student, model, course_structure, submissions_scores, csm_scores):
@classmethod
def read(cls, subsection, model, course_structure, submissions_scores, csm_scores):
"""
Load the subsection grade from the persisted model.
Read the subsection grade from the persisted model.
"""
problem_scores = OrderedDict()
for block in model.visible_blocks.blocks:
self._compute_block_score(block.locator, course_structure, submissions_scores, csm_scores, block)
problem_score = cls._compute_block_score(
block.locator, course_structure, submissions_scores, csm_scores, block,
)
if problem_score:
problem_scores[block.locator] = problem_score
self.graded_total = AggregatedScore(
tw_earned=model.earned_graded,
tw_possible=model.possible_graded,
graded=True,
first_attempted=model.first_attempted,
)
self.all_total = AggregatedScore(
all_total = AggregatedScore(
tw_earned=model.earned_all,
tw_possible=model.possible_all,
graded=False,
first_attempted=model.first_attempted,
)
self.override = model.override if hasattr(model, 'override') else None
self._log_event(log.debug, u"init_from_model", student)
return self
graded_total = AggregatedScore(
tw_earned=model.earned_graded,
tw_possible=model.possible_graded,
graded=True,
first_attempted=model.first_attempted,
)
override = model.override if hasattr(model, 'override') else None
return cls(subsection, problem_scores, all_total, graded_total, override)
@classmethod
def bulk_create_models(cls, student, subsection_grades, course_key):
......@@ -153,17 +164,9 @@ class SubsectionGrade(SubsectionGradeBase):
if subsection_grade
if subsection_grade._should_persist_per_attempted() # pylint: disable=protected-access
]
return PersistentSubsectionGrade.bulk_create_grades(params, course_key)
return PersistentSubsectionGrade.bulk_create_grades(params, student.id, course_key)
def create_model(self, student):
"""
Saves the subsection grade in a persisted model.
"""
if self._should_persist_per_attempted():
self._log_event(log.debug, u"create_model", student)
return PersistentSubsectionGrade.create_grade(**self._persisted_model_params(student))
def update_or_create_model(self, student, score_deleted):
def update_or_create_model(self, student, score_deleted=False):
"""
Saves or updates the subsection grade in a persisted model.
"""
......@@ -184,8 +187,8 @@ class SubsectionGrade(SubsectionGradeBase):
score_deleted
)
@staticmethod
def _compute_block_score(
self,
block_key,
course_structure,
submissions_scores,
......@@ -205,14 +208,12 @@ class SubsectionGrade(SubsectionGradeBase):
pass
else:
if getattr(block, 'has_score', False):
problem_score = get_score(
return get_score(
submissions_scores,
csm_scores,
persisted_block,
block,
)
if problem_score:
self.problem_scores[block_key] = problem_score
def _persisted_model_params(self, student):
"""
......
......@@ -43,14 +43,14 @@ class SubsectionGradeFactory(object):
if assume_zero_if_absent(self.course_data.course_key):
subsection_grade = ZeroSubsectionGrade(subsection, self.course_data)
else:
subsection_grade = SubsectionGrade(subsection).init_from_structure(
self.student, self.course_data.structure, self._submissions_scores, self._csm_scores,
subsection_grade = SubsectionGrade.create(
subsection, self.course_data.structure, self._submissions_scores, self._csm_scores,
)
if should_persist_grades(self.course_data.course_key):
if read_only:
self._unsaved_subsection_grades[subsection_grade.location] = subsection_grade
else:
grade_model = subsection_grade.create_model(self.student)
grade_model = subsection_grade.update_or_create_model(self.student)
self._update_saved_subsection_grade(subsection.location, grade_model)
return subsection_grade
......@@ -69,8 +69,8 @@ class SubsectionGradeFactory(object):
"""
self._log_event(log.debug, u"update, subsection: {}".format(subsection.location), subsection)
calculated_grade = SubsectionGrade(subsection).init_from_structure(
self.student, self.course_data.structure, self._submissions_scores, self._csm_scores,
calculated_grade = SubsectionGrade.create(
subsection, self.course_data.structure, self._submissions_scores, self._csm_scores,
)
if should_persist_grades(self.course_data.course_key):
......@@ -80,8 +80,8 @@ class SubsectionGradeFactory(object):
except PersistentSubsectionGrade.DoesNotExist:
pass
else:
orig_subsection_grade = SubsectionGrade(subsection).init_from_model(
self.student, grade_model, self.course_data.structure, self._submissions_scores, self._csm_scores,
orig_subsection_grade = SubsectionGrade.read(
subsection, grade_model, self.course_data.structure, self._submissions_scores, self._csm_scores,
)
if not is_score_higher_or_equal(
orig_subsection_grade.graded_total.earned,
......@@ -123,10 +123,10 @@ class SubsectionGradeFactory(object):
"""
if should_persist_grades(self.course_data.course_key):
saved_subsection_grades = self._get_bulk_cached_subsection_grades()
subsection_grade = saved_subsection_grades.get(subsection.location)
if subsection_grade:
return SubsectionGrade(subsection).init_from_model(
self.student, subsection_grade, self.course_data.structure, self._submissions_scores, self._csm_scores,
grade = saved_subsection_grades.get(subsection.location)
if grade:
return SubsectionGrade.read(
subsection, grade, self.course_data.structure, self._submissions_scores, self._csm_scores,
)
def _get_bulk_cached_subsection_grades(self):
......
......@@ -24,7 +24,7 @@ from track.event_transaction_utils import set_event_transaction_id, set_event_tr
from util.date_utils import from_timestamp
from xmodule.modulestore.django import modulestore
from .config.waffle import ESTIMATE_FIRST_ATTEMPTED, DISABLE_REGRADE_ON_POLICY_CHANGE, waffle
from .config.waffle import DISABLE_REGRADE_ON_POLICY_CHANGE, waffle
from .constants import ScoreDatabaseTableEnum
from .course_grade_factory import CourseGradeFactory
from .exceptions import DatabaseNotReadyError
......@@ -83,14 +83,6 @@ def compute_grades_for_course_v2(self, **kwargs):
TODO: Roll this back into compute_grades_for_course once all workers have
the version with **kwargs.
Sets the ESTIMATE_FIRST_ATTEMPTED flag, then calls the original task as a
synchronous function.
estimate_first_attempted:
controls whether to unconditionally set the ESTIMATE_FIRST_ATTEMPTED
waffle switch. If false or not provided, use the global value of
the ESTIMATE_FIRST_ATTEMPTED waffle switch.
"""
if 'event_transaction_id' in kwargs:
set_event_transaction_id(kwargs['event_transaction_id'])
......@@ -98,9 +90,6 @@ def compute_grades_for_course_v2(self, **kwargs):
if 'event_transaction_type' in kwargs:
set_event_transaction_type(kwargs['event_transaction_type'])
if kwargs.get('estimate_first_attempted'):
waffle().override_for_request(ESTIMATE_FIRST_ATTEMPTED, True)
try:
return compute_grades_for_course(kwargs['course_key'], kwargs['offset'], kwargs['batch_size'])
except Exception as exc: # pylint: disable=broad-except
......
......@@ -26,7 +26,7 @@ class GradeTestBase(SharedModuleStoreTestCase):
cls.sequence = ItemFactory.create(
parent=cls.chapter,
category='sequential',
display_name="Test Sequential 1",
display_name="Test Sequential X",
graded=True,
format="Homework"
)
......@@ -49,7 +49,7 @@ class GradeTestBase(SharedModuleStoreTestCase):
cls.sequence2 = ItemFactory.create(
parent=cls.chapter,
category='sequential',
display_name="Test Sequential 2",
display_name="Test Sequential A",
graded=True,
format="Homework"
)
......
......@@ -3,7 +3,7 @@ Test grading events across apps.
"""
# pylint: disable=protected-access
from mock import patch
from mock import call as mock_call, patch
from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory
from courseware.tests.test_submitting_problems import ProblemSubmissionTestMixin
......@@ -16,9 +16,7 @@ from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
STATE_DELETED_TYPE = 'edx.grades.problem.state_deleted'
RESCORE_TYPE = 'edx.grades.problem.rescored'
SUBMITTED_TYPE = 'edx.grades.problem.submitted'
from ... import events
class GradesEventIntegrationTest(ProblemSubmissionTestMixin, SharedModuleStoreTestCase):
......@@ -75,99 +73,84 @@ class GradesEventIntegrationTest(ProblemSubmissionTestMixin, SharedModuleStoreTe
self.instructor = UserFactory.create(is_staff=True, username=u'test_instructor', password=u'test')
self.refresh_course()
@patch('lms.djangoapps.instructor.enrollment.tracker')
@patch('lms.djangoapps.grades.signals.handlers.tracker')
@patch('lms.djangoapps.grades.models.tracker')
def test_delete_student_state_events(self, models_tracker, handlers_tracker, enrollment_tracker):
# submit answer
@patch('lms.djangoapps.grades.events.tracker')
def test_submit_answer(self, events_tracker):
self.submit_question_answer('p1', {'2_1': 'choice_choice_2'})
course = self.store.get_course(self.course.id, depth=0)
# check logging to make sure id's are tracked correctly across events
event_transaction_id = handlers_tracker.emit.mock_calls[0][1][1]['event_transaction_id']
for call in models_tracker.emit.mock_calls:
self.assertEqual(event_transaction_id, call[1][1]['event_transaction_id'])
self.assertEqual(unicode(SUBMITTED_TYPE), call[1][1]['event_transaction_type'])
handlers_tracker.emit.assert_called_with(
unicode(SUBMITTED_TYPE),
{
'user_id': unicode(self.student.id),
'event_transaction_id': event_transaction_id,
'event_transaction_type': unicode(SUBMITTED_TYPE),
'course_id': unicode(self.course.id),
'problem_id': unicode(self.problem.location),
'weighted_earned': 2.0,
'weighted_possible': 2.0,
}
event_transaction_id = events_tracker.emit.mock_calls[0][1][1]['event_transaction_id']
events_tracker.emit.assert_has_calls(
[
mock_call(
events.PROBLEM_SUBMITTED_EVENT_TYPE,
{
'user_id': unicode(self.student.id),
'event_transaction_id': event_transaction_id,
'event_transaction_type': events.PROBLEM_SUBMITTED_EVENT_TYPE,
'course_id': unicode(self.course.id),
'problem_id': unicode(self.problem.location),
'weighted_earned': 2.0,
'weighted_possible': 2.0,
},
),
mock_call(
events.COURSE_GRADE_CALCULATED,
{
'course_version': unicode(course.course_version),
'percent_grade': 0.02,
'grading_policy_hash': u'ChVp0lHGQGCevD0t4njna/C44zQ=',
'user_id': unicode(self.student.id),
'letter_grade': u'',
'event_transaction_id': event_transaction_id,
'event_transaction_type': events.PROBLEM_SUBMITTED_EVENT_TYPE,
'course_id': unicode(self.course.id),
'course_edited_timestamp': unicode(course.subtree_edited_on),
}
),
],
any_order=True,
)
course = self.store.get_course(self.course.id, depth=0)
models_tracker.emit.assert_called_with(
u'edx.grades.course.grade_calculated',
{
'course_version': unicode(course.course_version),
'percent_grade': 0.02,
'grading_policy_hash': u'ChVp0lHGQGCevD0t4njna/C44zQ=',
'user_id': unicode(self.student.id),
'letter_grade': u'',
'event_transaction_id': event_transaction_id,
'event_transaction_type': unicode(SUBMITTED_TYPE),
'course_id': unicode(self.course.id),
'course_edited_timestamp': unicode(course.subtree_edited_on),
}
)
models_tracker.reset_mock()
handlers_tracker.reset_mock()
def test_delete_student_state(self):
self.submit_question_answer('p1', {'2_1': 'choice_choice_2'})
# delete state
reset_student_attempts(self.course.id, self.student, self.problem.location, self.instructor, delete_module=True)
with patch('lms.djangoapps.instructor.enrollment.tracker') as enrollment_tracker:
with patch('lms.djangoapps.grades.events.tracker') as events_tracker:
reset_student_attempts(
self.course.id, self.student, self.problem.location, self.instructor, delete_module=True,
)
course = self.store.get_course(self.course.id, depth=0)
# check logging to make sure id's are tracked correctly across events
event_transaction_id = enrollment_tracker.method_calls[0][1][1]['event_transaction_id']
# make sure the id is propagated throughout the event flow
for call in models_tracker.emit.mock_calls:
self.assertEqual(event_transaction_id, call[1][1]['event_transaction_id'])
self.assertEqual(unicode(STATE_DELETED_TYPE), call[1][1]['event_transaction_type'])
# ensure we do not log a problem submitted event when state is deleted
handlers_tracker.assert_not_called()
enrollment_tracker.emit.assert_called_with(
unicode(STATE_DELETED_TYPE),
events.STATE_DELETED_EVENT_TYPE,
{
'user_id': unicode(self.student.id),
'course_id': unicode(self.course.id),
'problem_id': unicode(self.problem.location),
'instructor_id': unicode(self.instructor.id),
'event_transaction_id': event_transaction_id,
'event_transaction_type': unicode(STATE_DELETED_TYPE),
'event_transaction_type': events.STATE_DELETED_EVENT_TYPE,
}
)
course = self.store.get_course(self.course.id, depth=0)
models_tracker.emit.assert_called_with(
u'edx.grades.course.grade_calculated',
events_tracker.emit.assert_called_with(
events.COURSE_GRADE_CALCULATED,
{
'percent_grade': 0.0,
'grading_policy_hash': u'ChVp0lHGQGCevD0t4njna/C44zQ=',
'user_id': unicode(self.student.id),
'letter_grade': u'',
'event_transaction_id': event_transaction_id,
'event_transaction_type': unicode(STATE_DELETED_TYPE),
'event_transaction_type': events.STATE_DELETED_EVENT_TYPE,
'course_id': unicode(self.course.id),
'course_edited_timestamp': unicode(course.subtree_edited_on),
'course_version': unicode(course.course_version),
}
)
@patch('lms.djangoapps.grades.signals.handlers.tracker')
@patch('lms.djangoapps.grades.models.tracker')
def test_rescoring_events(self, models_tracker, handlers_tracker):
# submit answer
def test_rescoring_events(self):
self.submit_question_answer('p1', {'2_1': 'choice_choice_3'})
models_tracker.reset_mock()
handlers_tracker.reset_mock()
new_problem_xml = MultipleChoiceResponseXMLFactory().build_xml(
question_text='The correct answer is Choice 3',
choices=[False, False, False, True],
......@@ -178,56 +161,53 @@ class GradesEventIntegrationTest(ProblemSubmissionTestMixin, SharedModuleStoreTe
self.store.update_item(self.problem, self.instructor.id)
self.store.publish(self.problem.location, self.instructor.id)
submit_rescore_problem_for_student(
request=get_mock_request(self.instructor),
usage_key=self.problem.location,
student=self.student,
only_if_higher=False
)
# check logging to make sure id's are tracked correctly across
# events
event_transaction_id = handlers_tracker.emit.mock_calls[0][1][1]['event_transaction_id']
# make sure the id is propagated throughout the event flow
for call in models_tracker.emit.mock_calls:
self.assertEqual(event_transaction_id, call[1][1]['event_transaction_id'])
self.assertEqual(unicode(RESCORE_TYPE), call[1][1]['event_transaction_type'])
with patch('lms.djangoapps.grades.events.tracker') as events_tracker:
submit_rescore_problem_for_student(
request=get_mock_request(self.instructor),
usage_key=self.problem.location,
student=self.student,
only_if_higher=False
)
course = self.store.get_course(self.course.id, depth=0)
# make sure the models calls have re-added the course id to the context
for args in models_tracker.get_tracker().context.call_args_list:
# make sure the tracker's context is updated with course info
for args in events_tracker.get_tracker().context.call_args_list:
self.assertEqual(
args[0][1],
{'course_id': unicode(self.course.id), 'org_id': unicode(self.course.org)}
)
handlers_tracker.assert_not_called()
handlers_tracker.emit.assert_called_with(
unicode(RESCORE_TYPE),
{
'course_id': unicode(self.course.id),
'user_id': unicode(self.student.id),
'problem_id': unicode(self.problem.location),
'new_weighted_earned': 2,
'new_weighted_possible': 2,
'only_if_higher': False,
'instructor_id': unicode(self.instructor.id),
'event_transaction_id': event_transaction_id,
'event_transaction_type': unicode(RESCORE_TYPE),
}
)
course = self.store.get_course(self.course.id, depth=0)
models_tracker.emit.assert_called_with(
u'edx.grades.course.grade_calculated',
{
'course_version': unicode(course.course_version),
'percent_grade': 0.02,
'grading_policy_hash': u'ChVp0lHGQGCevD0t4njna/C44zQ=',
'user_id': unicode(self.student.id),
'letter_grade': u'',
'event_transaction_id': event_transaction_id,
'event_transaction_type': unicode(RESCORE_TYPE),
'course_id': unicode(self.course.id),
'course_edited_timestamp': unicode(course.subtree_edited_on),
}
event_transaction_id = events_tracker.emit.mock_calls[0][1][1]['event_transaction_id']
events_tracker.emit.assert_has_calls(
[
mock_call(
events.GRADES_RESCORE_EVENT_TYPE,
{
'course_id': unicode(self.course.id),
'user_id': unicode(self.student.id),
'problem_id': unicode(self.problem.location),
'new_weighted_earned': 2,
'new_weighted_possible': 2,
'only_if_higher': False,
'instructor_id': unicode(self.instructor.id),
'event_transaction_id': event_transaction_id,
'event_transaction_type': events.GRADES_RESCORE_EVENT_TYPE,
},
),
mock_call(
events.COURSE_GRADE_CALCULATED,
{
'course_version': unicode(course.course_version),
'percent_grade': 0.02,
'grading_policy_hash': u'ChVp0lHGQGCevD0t4njna/C44zQ=',
'user_id': unicode(self.student.id),
'letter_grade': u'',
'event_transaction_id': event_transaction_id,
'event_transaction_type': events.GRADES_RESCORE_EVENT_TYPE,
'course_id': unicode(self.course.id),
'course_edited_timestamp': unicode(course.subtree_edited_on),
},
),
],
any_order=True,
)
......@@ -16,8 +16,8 @@ from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore.tests.utils import TEST_DATA_DIR
from xmodule.modulestore.xml_importer import import_course_from_xml
from ..subsection_grade_factory import SubsectionGradeFactory
from .utils import answer_problem, mock_get_submissions_score
from ...subsection_grade_factory import SubsectionGradeFactory
from ..utils import answer_problem, mock_get_submissions_score
@ddt.ddt
......
......@@ -2,25 +2,19 @@ import itertools
from nose.plugins.attrib import attr
import ddt
from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory
from courseware.access import has_access
from courseware.tests.test_submitting_problems import ProblemSubmissionTestMixin
from django.conf import settings
from lms.djangoapps.course_blocks.api import get_course_blocks
from lms.djangoapps.grades.config.tests.utils import persistent_grades_feature_flags
from mock import patch
from openedx.core.djangolib.testing.utils import get_mock_request
from openedx.core.djangoapps.content.block_structure.factory import BlockStructureFactory
from student.models import CourseEnrollment
from student.tests.factories import UserFactory
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore.tests.factories import CourseFactory
from ..config.waffle import ASSUME_ZERO_GRADE_IF_ABSENT, waffle
from ..course_grade import CourseGrade, ZeroCourseGrade
from ..course_grade_factory import CourseGradeFactory
from ..subsection_grade import SubsectionGrade, ZeroSubsectionGrade
from ..subsection_grade_factory import SubsectionGradeFactory
from .base import GradeTestBase
from .utils import mock_get_score
......@@ -75,7 +69,7 @@ class TestCourseGradeFactory(GradeTestBase):
grade_factory.read(self.request.user, self.course)
self.assertEqual(mock_read_grade.called, feature_flag and course_setting)
def test_read(self):
def test_read_and_update(self):
grade_factory = CourseGradeFactory()
def _assert_read(expected_pass, expected_percent):
......@@ -83,18 +77,40 @@ class TestCourseGradeFactory(GradeTestBase):
Creates the grade, ensuring it is as expected.
"""
course_grade = grade_factory.read(self.request.user, self.course)
_assert_grade_values(course_grade, expected_pass, expected_percent)
_assert_section_order(course_grade)
def _assert_grade_values(course_grade, expected_pass, expected_percent):
self.assertEqual(course_grade.letter_grade, u'Pass' if expected_pass else None)
self.assertEqual(course_grade.percent, expected_percent)
with waffle().override(ASSUME_ZERO_GRADE_IF_ABSENT):
with self.assertNumQueries(1), mock_get_score(1, 2):
_assert_read(expected_pass=False, expected_percent=0)
def _assert_section_order(course_grade):
sections = course_grade.chapter_grades[self.chapter.location]['sections']
self.assertEqual(
[section.display_name for section in sections],
[self.sequence.display_name, self.sequence2.display_name]
)
with self.assertNumQueries(2), mock_get_score(1, 2):
_assert_read(expected_pass=False, expected_percent=0) # start off with grade of 0
with self.assertNumQueries(29), mock_get_score(1, 2):
grade_factory.update(self.request.user, self.course, force_update_subsections=True)
with self.assertNumQueries(4):
_assert_read(expected_pass=True, expected_percent=0.5) # updated to grade of .5
with self.assertNumQueries(6), mock_get_score(1, 4):
grade_factory.update(self.request.user, self.course, force_update_subsections=False)
with self.assertNumQueries(10), mock_get_score(1, 2):
grade_factory.update(self.request.user, self.course)
with self.assertNumQueries(4):
_assert_read(expected_pass=True, expected_percent=0.5) # NOT updated to grade of .25
with self.assertNumQueries(12), mock_get_score(2, 2):
grade_factory.update(self.request.user, self.course, force_update_subsections=True)
with self.assertNumQueries(1):
_assert_read(expected_pass=True, expected_percent=0.5)
with self.assertNumQueries(4):
_assert_read(expected_pass=True, expected_percent=1.0) # updated to grade of 1.0
@patch.dict(settings.FEATURES, {'ASSUME_ZERO_GRADE_IF_ABSENT_FOR_ALL_TESTS': False})
@ddt.data(*itertools.product((True, False), (True, False)))
......@@ -109,21 +125,20 @@ class TestCourseGradeFactory(GradeTestBase):
self.assertIsNone(course_grade)
def test_create_zero_subs_grade_for_nonzero_course_grade(self):
with waffle().override(ASSUME_ZERO_GRADE_IF_ABSENT):
subsection = self.course_structure[self.sequence.location]
with mock_get_score(1, 2):
self.subsection_grade_factory.update(subsection)
course_grade = CourseGradeFactory().update(self.request.user, self.course)
subsection1_grade = course_grade.subsection_grades[self.sequence.location]
subsection2_grade = course_grade.subsection_grades[self.sequence2.location]
self.assertIsInstance(subsection1_grade, SubsectionGrade)
self.assertIsInstance(subsection2_grade, ZeroSubsectionGrade)
subsection = self.course_structure[self.sequence.location]
with mock_get_score(1, 2):
self.subsection_grade_factory.update(subsection)
course_grade = CourseGradeFactory().update(self.request.user, self.course)
subsection1_grade = course_grade.subsection_grades[self.sequence.location]
subsection2_grade = course_grade.subsection_grades[self.sequence2.location]
self.assertIsInstance(subsection1_grade, SubsectionGrade)
self.assertIsInstance(subsection2_grade, ZeroSubsectionGrade)
@ddt.data(True, False)
def test_iter_force_update(self, force_update):
with patch('lms.djangoapps.grades.subsection_grade_factory.SubsectionGradeFactory.update') as mock_update:
set(CourseGradeFactory().iter(
users = [self.request.user], course = self.course, force_update = force_update
users=[self.request.user], course=self.course, force_update=force_update,
))
self.assertEqual(mock_update.called, force_update)
......@@ -149,13 +164,13 @@ class TestCourseGradeFactory(GradeTestBase):
'section_breakdown': [
{
'category': 'Homework',
'detail': u'Homework 1 - Test Sequential 1 - 50% (1/2)',
'detail': u'Homework 1 - Test Sequential X - 50% (1/2)',
'label': u'HW 01',
'percent': 0.5
},
{
'category': 'Homework',
'detail': u'Homework 2 - Test Sequential 2 - 0% (0/1)',
'detail': u'Homework 2 - Test Sequential A - 0% (0/1)',
'label': u'HW 02',
'percent': 0.0
},
......@@ -282,81 +297,3 @@ class TestGradeIteration(SharedModuleStoreTestCase):
students_to_errors[student] = error
return students_to_course_grades, students_to_errors
class TestCourseGradeLogging(ProblemSubmissionTestMixin, SharedModuleStoreTestCase):
"""
Tests logging in the course grades module.
Uses a larger course structure than other
unit tests.
"""
def setUp(self):
super(TestCourseGradeLogging, self).setUp()
self.course = CourseFactory.create()
with self.store.bulk_operations(self.course.id):
self.chapter = ItemFactory.create(
parent=self.course,
category="chapter",
display_name="Test Chapter"
)
self.sequence = ItemFactory.create(
parent=self.chapter,
category='sequential',
display_name="Test Sequential 1",
graded=True
)
self.sequence_2 = ItemFactory.create(
parent=self.chapter,
category='sequential',
display_name="Test Sequential 2",
graded=True
)
self.sequence_3 = ItemFactory.create(
parent=self.chapter,
category='sequential',
display_name="Test Sequential 3",
graded=False
)
self.vertical = ItemFactory.create(
parent=self.sequence,
category='vertical',
display_name='Test Vertical 1'
)
self.vertical_2 = ItemFactory.create(
parent=self.sequence_2,
category='vertical',
display_name='Test Vertical 2'
)
self.vertical_3 = ItemFactory.create(
parent=self.sequence_3,
category='vertical',
display_name='Test Vertical 3'
)
problem_xml = MultipleChoiceResponseXMLFactory().build_xml(
question_text='The correct answer is Choice 2',
choices=[False, False, True, False],
choice_names=['choice_0', 'choice_1', 'choice_2', 'choice_3']
)
self.problem = ItemFactory.create(
parent=self.vertical,
category="problem",
display_name="test_problem_1",
data=problem_xml
)
self.problem_2 = ItemFactory.create(
parent=self.vertical_2,
category="problem",
display_name="test_problem_2",
data=problem_xml
)
self.problem_3 = ItemFactory.create(
parent=self.vertical_3,
category="problem",
display_name="test_problem_3",
data=problem_xml
)
self.request = get_mock_request(UserFactory())
self.client.login(username=self.request.user.username, password="test")
self.course_structure = get_course_blocks(self.request.user, self.course.location)
self.subsection_grade_factory = SubsectionGradeFactory(self.request.user, self.course, self.course_structure)
CourseEnrollment.enroll(self.request.user, self.course.id)
......@@ -16,7 +16,6 @@ from freezegun import freeze_time
from mock import patch
from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator
from lms.djangoapps.grades.config import waffle
from lms.djangoapps.grades.models import (
BLOCK_RECORD_LIST_VERSION,
BlockRecord,
......@@ -131,7 +130,7 @@ class VisibleBlocksTest(GradesModelTestCase):
"""
Creates and returns a BlockRecordList for the given blocks.
"""
return VisibleBlocks.objects.create_from_blockrecords(BlockRecordList.from_list(blocks, self.course_key))
return VisibleBlocks.cached_get_or_create(BlockRecordList.from_list(blocks, self.course_key))
def test_creation(self):
"""
......@@ -215,45 +214,30 @@ class PersistentSubsectionGradeTest(GradesModelTestCase):
"first_attempted": datetime(2000, 1, 1, 12, 30, 45, tzinfo=pytz.UTC),
}
def test_create(self):
"""
Tests model creation, and confirms error when trying to recreate model.
"""
created_grade = PersistentSubsectionGrade.create_grade(**self.params)
with self.assertNumQueries(1):
read_grade = PersistentSubsectionGrade.read_grade(
user_id=self.params["user_id"],
usage_key=self.params["usage_key"],
)
self.assertEqual(created_grade, read_grade)
self.assertEqual(read_grade.visible_blocks.blocks, self.block_records)
with self.assertRaises(IntegrityError):
PersistentSubsectionGrade.create_grade(**self.params)
@ddt.data('course_version', 'subtree_edited_timestamp')
def test_optional_fields(self, field):
del self.params[field]
PersistentSubsectionGrade.create_grade(**self.params)
PersistentSubsectionGrade.update_or_create_grade(**self.params)
@ddt.data(
("user_id", IntegrityError),
("user_id", KeyError),
("usage_key", KeyError),
("earned_all", IntegrityError),
("possible_all", IntegrityError),
("earned_graded", IntegrityError),
("possible_graded", IntegrityError),
("first_attempted", KeyError),
("visible_blocks", KeyError),
("first_attempted", KeyError),
)
@ddt.unpack
def test_non_optional_fields(self, field, error):
del self.params[field]
with self.assertRaises(error):
PersistentSubsectionGrade.create_grade(**self.params)
PersistentSubsectionGrade.update_or_create_grade(**self.params)
@ddt.data(True, False)
def test_update_or_create_grade(self, already_created):
created_grade = PersistentSubsectionGrade.create_grade(**self.params) if already_created else None
created_grade = PersistentSubsectionGrade.update_or_create_grade(**self.params) if already_created else None
self.params["earned_all"] = 7
updated_grade = PersistentSubsectionGrade.update_or_create_grade(**self.params)
......@@ -262,53 +246,48 @@ class PersistentSubsectionGradeTest(GradesModelTestCase):
self.assertEqual(created_grade.id, updated_grade.id)
self.assertEqual(created_grade.earned_all, 6)
@ddt.unpack
@ddt.data(
(True, datetime(2000, 1, 1, 12, 30, 45, tzinfo=pytz.UTC)),
(False, None), # Use as now(). Freeze time needs this calculation to happen at test time.
)
def test_update_or_create_attempted(self, is_active, expected_first_attempted):
with freeze_time(now()):
if expected_first_attempted is None:
expected_first_attempted = now()
with waffle.waffle().override(waffle.ESTIMATE_FIRST_ATTEMPTED, active=is_active):
grade = PersistentSubsectionGrade.update_or_create_grade(**self.params)
self.assertEqual(grade.first_attempted, expected_first_attempted)
with self.assertNumQueries(1):
read_grade = PersistentSubsectionGrade.read_grade(
user_id=self.params["user_id"],
usage_key=self.params["usage_key"],
)
self.assertEqual(updated_grade, read_grade)
self.assertEqual(read_grade.visible_blocks.blocks, self.block_records)
def test_unattempted(self):
self.params['first_attempted'] = None
self.params['earned_all'] = 0.0
self.params['earned_graded'] = 0.0
grade = PersistentSubsectionGrade.create_grade(**self.params)
grade = PersistentSubsectionGrade.update_or_create_grade(**self.params)
self.assertIsNone(grade.first_attempted)
self.assertEqual(grade.earned_all, 0.0)
self.assertEqual(grade.earned_graded, 0.0)
def test_first_attempted_not_changed_on_update(self):
PersistentSubsectionGrade.create_grade(**self.params)
PersistentSubsectionGrade.update_or_create_grade(**self.params)
moment = now()
grade = PersistentSubsectionGrade.update_or_create_grade(**self.params)
self.assertLess(grade.first_attempted, moment)
def test_unattempted_save_does_not_remove_attempt(self):
PersistentSubsectionGrade.create_grade(**self.params)
PersistentSubsectionGrade.update_or_create_grade(**self.params)
self.params['first_attempted'] = None
grade = PersistentSubsectionGrade.update_or_create_grade(**self.params)
self.assertIsInstance(grade.first_attempted, datetime)
self.assertEqual(grade.earned_all, 6.0)
def test_update_or_create_event(self):
with patch('lms.djangoapps.grades.models.tracker') as tracker_mock:
with patch('lms.djangoapps.grades.events.tracker') as tracker_mock:
grade = PersistentSubsectionGrade.update_or_create_grade(**self.params)
self._assert_tracker_emitted_event(tracker_mock, grade)
def test_create_event(self):
with patch('lms.djangoapps.grades.models.tracker') as tracker_mock:
grade = PersistentSubsectionGrade.create_grade(**self.params)
with patch('lms.djangoapps.grades.events.tracker') as tracker_mock:
grade = PersistentSubsectionGrade.update_or_create_grade(**self.params)
self._assert_tracker_emitted_event(tracker_mock, grade)
def test_grade_override(self):
grade = PersistentSubsectionGrade.create_grade(**self.params)
grade = PersistentSubsectionGrade.update_or_create_grade(**self.params)
override = PersistentSubsectionGradeOverride(grade=grade, earned_all_override=0.0, earned_graded_override=0.0)
override.save()
grade = PersistentSubsectionGrade.update_or_create_grade(**self.params)
......@@ -456,7 +435,7 @@ class PersistentCourseGradesTest(GradesModelTestCase):
PersistentCourseGrade.read(self.params["user_id"], self.params["course_id"])
def test_update_or_create_event(self):
with patch('lms.djangoapps.grades.models.tracker') as tracker_mock:
with patch('lms.djangoapps.grades.events.tracker') as tracker_mock:
grade = PersistentCourseGrade.update_or_create(**self.params)
self._assert_tracker_emitted_event(tracker_mock, grade)
......
......@@ -4,7 +4,6 @@ from datetime import datetime
from freezegun import freeze_time
from lms.djangoapps.grades.models import PersistentSubsectionGrade, PersistentSubsectionGradeOverride
from lms.djangoapps.grades.services import GradesService, _get_key
from lms.djangoapps.grades.signals.handlers import SUBSECTION_OVERRIDE_EVENT_TYPE
from mock import patch, call
from opaque_keys.edx.keys import CourseKey, UsageKey
from student.tests.factories import UserFactory
......
"""
Tests for the score change signals defined in the courseware models module.
"""
import itertools
import re
from datetime import datetime
......@@ -10,10 +9,6 @@ import pytz
from django.test import TestCase
from mock import MagicMock, patch
from opaque_keys.edx.locations import CourseLocator
from openedx.core.djangoapps.course_groups.signals.signals import COHORT_MEMBERSHIP_UPDATED
from student.signals.signals import ENROLLMENT_TRACK_UPDATED
from student.tests.factories import UserFactory
from submissions.models import score_reset, score_set
from util.date_utils import to_timestamp
......
from ..models import PersistentSubsectionGrade
from ..subsection_grade import SubsectionGrade
from .utils import mock_get_score
from .base import GradeTestBase
from .base import GradeTestBase
class SubsectionGradeTest(GradeTestBase):
def test_save_and_load(self):
def test_create_and_read(self):
with mock_get_score(1, 2):
# Create a grade that *isn't* saved to the database
input_grade = SubsectionGrade(self.sequence)
input_grade.init_from_structure(
self.request.user,
created_grade = SubsectionGrade.create(
self.sequence,
self.course_structure,
self.subsection_grade_factory._submissions_scores,
self.subsection_grade_factory._csm_scores,
......@@ -18,23 +17,22 @@ class SubsectionGradeTest(GradeTestBase):
self.assertEqual(PersistentSubsectionGrade.objects.count(), 0)
# save to db, and verify object is in database
input_grade.create_model(self.request.user)
created_grade.update_or_create_model(self.request.user)
self.assertEqual(PersistentSubsectionGrade.objects.count(), 1)
# load from db, and ensure output matches input
loaded_grade = SubsectionGrade(self.sequence)
# read from db, and ensure output matches input
saved_model = PersistentSubsectionGrade.read_grade(
user_id=self.request.user.id,
usage_key=self.sequence.location,
)
loaded_grade.init_from_model(
self.request.user,
read_grade = SubsectionGrade.read(
self.sequence,
saved_model,
self.course_structure,
self.subsection_grade_factory._submissions_scores,
self.subsection_grade_factory._csm_scores,
)
self.assertEqual(input_grade.url_name, loaded_grade.url_name)
loaded_grade.all_total.first_attempted = input_grade.all_total.first_attempted = None
self.assertEqual(input_grade.all_total, loaded_grade.all_total)
self.assertEqual(created_grade.url_name, read_grade.url_name)
read_grade.all_total.first_attempted = created_grade.all_total.first_attempted = None
self.assertEqual(created_grade.all_total, read_grade.all_total)
......@@ -164,10 +164,10 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
self.assertEquals(mock_block_structure_create.call_count, 1)
@ddt.data(
(ModuleStoreEnum.Type.mongo, 1, 27, True),
(ModuleStoreEnum.Type.mongo, 1, 27, False),
(ModuleStoreEnum.Type.split, 3, 27, True),
(ModuleStoreEnum.Type.split, 3, 27, False),
(ModuleStoreEnum.Type.mongo, 1, 25, True),
(ModuleStoreEnum.Type.mongo, 1, 25, False),
(ModuleStoreEnum.Type.split, 3, 25, True),
(ModuleStoreEnum.Type.split, 3, 25, False),
)
@ddt.unpack
def test_query_counts(self, default_store, num_mongo_calls, num_sql_calls, create_multiple_subsections):
......@@ -179,8 +179,8 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
self._apply_recalculate_subsection_grade()
@ddt.data(
(ModuleStoreEnum.Type.mongo, 1, 27),
(ModuleStoreEnum.Type.split, 3, 27),
(ModuleStoreEnum.Type.mongo, 1, 25),
(ModuleStoreEnum.Type.split, 3, 25),
)
@ddt.unpack
def test_query_counts_dont_change_with_more_content(self, default_store, num_mongo_calls, num_sql_calls):
......@@ -240,8 +240,8 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
self.assertEqual(len(PersistentSubsectionGrade.bulk_read_grades(self.user.id, self.course.id)), 0)
@ddt.data(
(ModuleStoreEnum.Type.mongo, 1, 28),
(ModuleStoreEnum.Type.split, 3, 28),
(ModuleStoreEnum.Type.mongo, 1, 26),
(ModuleStoreEnum.Type.split, 3, 26),
)
@ddt.unpack
def test_persistent_grades_enabled_on_course(self, default_store, num_mongo_queries, num_sql_queries):
......
......@@ -20,7 +20,8 @@ from courseware.models import StudentModule
from edxmako.shortcuts import render_to_string
from eventtracking import tracker
from lms.djangoapps.grades.constants import ScoreDatabaseTableEnum
from lms.djangoapps.grades.signals.handlers import disconnect_submissions_signal_receiver, STATE_DELETED_EVENT_TYPE
from lms.djangoapps.grades.events import STATE_DELETED_EVENT_TYPE
from lms.djangoapps.grades.signals.handlers import disconnect_submissions_signal_receiver
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
......
......@@ -7,7 +7,6 @@ from time import time
from django.contrib.auth.models import User
from opaque_keys.edx.keys import UsageKey
from xblock.runtime import KvsFieldData
import dogstats_wrapper as dog_stats_api
from capa.responsetypes import LoncapaProblemError, ResponseError, StudentInputError
......@@ -15,16 +14,13 @@ from courseware.courses import get_course_by_id, get_problems_in_section
from courseware.model_data import DjangoKeyValueStore, FieldDataCache
from courseware.models import StudentModule
from courseware.module_render import get_module_for_descriptor_internal
from eventtracking import tracker
from lms.djangoapps.grades.scores import weighted_score
from track.contexts import course_context_from_course_id
from lms.djangoapps.grades.events import GRADES_OVERRIDE_EVENT_TYPE, GRADES_RESCORE_EVENT_TYPE
from track.event_transaction_utils import create_new_event_transaction_id, set_event_transaction_type
from track.views import task_track
from util.db import outer_atomic
from xmodule.modulestore.django import modulestore
from xblock.runtime import KvsFieldData
from xblock.scorable import Score, ScorableXBlockMixin
from xblock.scorable import Score
from xmodule.modulestore.django import modulestore
from ..exceptions import UpdateProblemModuleStateError
from .runner import TaskProgress
......@@ -32,10 +28,6 @@ from .utils import UNKNOWN_TASK_ID, UPDATE_STATUS_FAILED, UPDATE_STATUS_SKIPPED,
TASK_LOG = logging.getLogger('edx.celery.task')
# define value to be used in grading events
GRADES_RESCORE_EVENT_TYPE = 'edx.grades.problem.rescored'
GRADES_OVERRIDE_EVENT_TYPE = 'edx.grades.problem.score_overridden'
def perform_module_state_update(update_fcn, filter_fcn, _entry_id, course_id, task_input, action_name):
"""
......
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