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
......
......@@ -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"
)
......
......@@ -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
......
......@@ -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