Have SubsectionGrade compute problem_scores lazily

This commit is contained in:
Nimisha Asthagiri
2017-10-04 09:30:07 -04:00
parent 612c99ba3c
commit 33428519aa
5 changed files with 115 additions and 124 deletions

View File

@@ -1,6 +1,7 @@
"""
SubsectionGrade Class
"""
from abc import ABCMeta
from collections import OrderedDict
from logging import getLogger
@@ -17,8 +18,10 @@ log = getLogger(__name__)
class SubsectionGradeBase(object):
"""
Class for Subsection Grades.
Abstract base class for Subsection Grades.
"""
__metaclass__ = ABCMeta
def __init__(self, subsection):
self.location = subsection.location
self.display_name = block_metadata_utils.display_name_with_default_escaped(subsection)
@@ -108,100 +111,23 @@ class ZeroSubsectionGrade(SubsectionGradeBase):
return locations
class SubsectionGrade(SubsectionGradeBase):
class NonZeroSubsectionGrade(SubsectionGradeBase):
"""
Class for Subsection Grades.
Abstract base class for Subsection Grades with
possibly NonZero values.
"""
def __init__(self, subsection, problem_scores, all_total, graded_total, override=None):
super(SubsectionGrade, self).__init__(subsection)
self.problem_scores = problem_scores
__metaclass__ = ABCMeta
def __init__(self, subsection, all_total, graded_total, override=None):
super(NonZeroSubsectionGrade, self).__init__(subsection)
self.all_total = all_total
self.graded_total = graded_total
self.override = override
@classmethod
def create(cls, subsection, course_structure, submissions_scores, csm_scores):
"""
Compute and create the subsection grade.
"""
problem_scores = OrderedDict()
for block_key in course_structure.post_order_traversal(
filter_func=possibly_scored,
start_node=subsection.location,
):
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())
return cls(subsection, problem_scores, all_total, graded_total)
@classmethod
def read(cls, subsection, model, course_structure, submissions_scores, csm_scores):
"""
Read the subsection grade from the persisted model.
"""
problem_scores = OrderedDict()
for block in model.visible_blocks.blocks:
problem_score = cls._compute_block_score(
block.locator, course_structure, submissions_scores, csm_scores, block,
)
if problem_score:
problem_scores[block.locator] = problem_score
all_total = AggregatedScore(
tw_earned=model.earned_all,
tw_possible=model.possible_all,
graded=False,
first_attempted=model.first_attempted,
)
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):
"""
Saves the subsection grade in a persisted model.
"""
params = [
subsection_grade._persisted_model_params(student) # pylint: disable=protected-access
for subsection_grade in subsection_grades
if subsection_grade
if subsection_grade._should_persist_per_attempted() # pylint: disable=protected-access
]
return PersistentSubsectionGrade.bulk_create_grades(params, student.id, course_key)
def update_or_create_model(self, student, score_deleted=False):
"""
Saves or updates the subsection grade in a persisted model.
"""
if self._should_persist_per_attempted(score_deleted):
self._log_event(log.debug, u"update_or_create_model", student)
return PersistentSubsectionGrade.update_or_create_grade(**self._persisted_model_params(student))
@property
def attempted_graded(self):
return self.graded_total.first_attempted is not None
def _should_persist_per_attempted(self, score_deleted=False):
"""
Returns whether the SubsectionGrade's model should be
persisted based on settings and attempted status.
If the learner's score was just deleted, they will have
no attempts but the grade should still be persisted.
"""
return (
self.all_total.first_attempted is not None or
score_deleted
)
@staticmethod
def _compute_block_score(
block_key,
@@ -210,10 +136,6 @@ class SubsectionGrade(SubsectionGradeBase):
csm_scores,
persisted_block=None,
):
"""
Compute score for the given block. If persisted_values
is provided, it is used for possible and weight.
"""
try:
block = course_structure[block_key]
except KeyError:
@@ -230,6 +152,97 @@ class SubsectionGrade(SubsectionGradeBase):
block,
)
class ReadSubsectionGrade(NonZeroSubsectionGrade):
"""
Class for Subsection grades that are read from the database.
"""
def __init__(self, subsection, model, course_structure, submissions_scores, csm_scores):
all_total = AggregatedScore(
tw_earned=model.earned_all,
tw_possible=model.possible_all,
graded=False,
first_attempted=model.first_attempted,
)
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
# save these for later since we compute problem_scores lazily
self.model = model
self.course_structure = course_structure
self.submissions_scores = submissions_scores
self.csm_scores = csm_scores
super(ReadSubsectionGrade, self).__init__(subsection, all_total, graded_total, override)
@lazy
def problem_scores(self):
problem_scores = OrderedDict()
for block in self.model.visible_blocks.blocks:
problem_score = self._compute_block_score(
block.locator, self.course_structure, self.submissions_scores, self.csm_scores, block,
)
if problem_score:
problem_scores[block.locator] = problem_score
return problem_scores
class CreateSubsectionGrade(NonZeroSubsectionGrade):
"""
Class for Subsection grades that are newly created or updated.
"""
def __init__(self, subsection, course_structure, submissions_scores, csm_scores):
self.problem_scores = OrderedDict()
for block_key in course_structure.post_order_traversal(
filter_func=possibly_scored,
start_node=subsection.location,
):
problem_score = self._compute_block_score(block_key, course_structure, submissions_scores, csm_scores)
if problem_score:
self.problem_scores[block_key] = problem_score
all_total, graded_total = graders.aggregate_scores(self.problem_scores.values())
super(CreateSubsectionGrade, self).__init__(subsection, all_total, graded_total)
def update_or_create_model(self, student, score_deleted=False):
"""
Saves or updates the subsection grade in a persisted model.
"""
if self._should_persist_per_attempted(score_deleted):
return PersistentSubsectionGrade.update_or_create_grade(**self._persisted_model_params(student))
@classmethod
def bulk_create_models(cls, student, subsection_grades, course_key):
"""
Saves the subsection grade in a persisted model.
"""
params = [
subsection_grade._persisted_model_params(student) # pylint: disable=protected-access
for subsection_grade in subsection_grades
if subsection_grade
if subsection_grade._should_persist_per_attempted() # pylint: disable=protected-access
]
return PersistentSubsectionGrade.bulk_create_grades(params, student.id, course_key)
def _should_persist_per_attempted(self, score_deleted=False):
"""
Returns whether the SubsectionGrade's model should be
persisted based on settings and attempted status.
If the learner's score was just deleted, they will have
no attempts but the grade should still be persisted.
"""
return (
self.all_total.first_attempted is not None or
score_deleted
)
def _persisted_model_params(self, student):
"""
Returns the parameters for creating/updating the
@@ -258,25 +271,3 @@ class SubsectionGrade(SubsectionGradeBase):
for location, score in
self.problem_scores.iteritems()
]
def _log_event(self, log_func, log_statement, student):
"""
Logs the given statement, for this instance.
"""
log_func(
u"Grades: SG.{}, subsection: {}, course: {}, "
u"version: {}, edit: {}, user: {},"
u"total: {}/{}, graded: {}/{}, show_correctness: {}".format(
log_statement,
self.location,
self.location.course_key,
self.course_version,
self.subtree_edited_timestamp,
student.id,
self.all_total.earned,
self.all_total.possible,
self.graded_total.earned,
self.graded_total.possible,
self.show_correctness,
)
)

View File

@@ -12,7 +12,7 @@ from student.models import anonymous_id_for_user
from submissions import api as submissions_api
from .course_data import CourseData
from .subsection_grade import SubsectionGrade, ZeroSubsectionGrade
from .subsection_grade import CreateSubsectionGrade, ReadSubsectionGrade, ZeroSubsectionGrade
log = getLogger(__name__)
@@ -43,7 +43,7 @@ class SubsectionGradeFactory(object):
if assume_zero_if_absent(self.course_data.course_key):
subsection_grade = ZeroSubsectionGrade(subsection, self.course_data)
else:
subsection_grade = SubsectionGrade.create(
subsection_grade = CreateSubsectionGrade(
subsection, self.course_data.structure, self._submissions_scores, self._csm_scores,
)
if should_persist_grades(self.course_data.course_key):
@@ -58,7 +58,7 @@ class SubsectionGradeFactory(object):
"""
Bulk creates all the unsaved subsection_grades to this point.
"""
SubsectionGrade.bulk_create_models(
CreateSubsectionGrade.bulk_create_models(
self.student, self._unsaved_subsection_grades.values(), self.course_data.course_key
)
self._unsaved_subsection_grades.clear()
@@ -69,7 +69,7 @@ class SubsectionGradeFactory(object):
"""
self._log_event(log.debug, u"update, subsection: {}".format(subsection.location), subsection)
calculated_grade = SubsectionGrade.create(
calculated_grade = CreateSubsectionGrade(
subsection, self.course_data.structure, self._submissions_scores, self._csm_scores,
)
@@ -80,7 +80,7 @@ class SubsectionGradeFactory(object):
except PersistentSubsectionGrade.DoesNotExist:
pass
else:
orig_subsection_grade = SubsectionGrade.read(
orig_subsection_grade = ReadSubsectionGrade(
subsection, grade_model, self.course_data.structure, self._submissions_scores, self._csm_scores,
)
if not is_score_higher_or_equal(
@@ -125,7 +125,7 @@ class SubsectionGradeFactory(object):
saved_subsection_grades = self._get_bulk_cached_subsection_grades()
grade = saved_subsection_grades.get(subsection.location)
if grade:
return SubsectionGrade.read(
return ReadSubsectionGrade(
subsection, grade, self.course_data.structure, self._submissions_scores, self._csm_scores,
)

View File

@@ -14,7 +14,7 @@ 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 import ReadSubsectionGrade, ZeroSubsectionGrade
from .base import GradeTestBase
from .utils import mock_get_score
@@ -131,7 +131,7 @@ class TestCourseGradeFactory(GradeTestBase):
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(subsection1_grade, ReadSubsectionGrade)
self.assertIsInstance(subsection2_grade, ZeroSubsectionGrade)
@ddt.data(True, False)

View File

@@ -1,5 +1,5 @@
from ..models import PersistentSubsectionGrade
from ..subsection_grade import SubsectionGrade
from ..subsection_grade import CreateSubsectionGrade, ReadSubsectionGrade
from .utils import mock_get_score
from .base import GradeTestBase
@@ -8,7 +8,7 @@ class SubsectionGradeTest(GradeTestBase):
def test_create_and_read(self):
with mock_get_score(1, 2):
# Create a grade that *isn't* saved to the database
created_grade = SubsectionGrade.create(
created_grade = CreateSubsectionGrade(
self.sequence,
self.course_structure,
self.subsection_grade_factory._submissions_scores,
@@ -25,7 +25,7 @@ class SubsectionGradeTest(GradeTestBase):
user_id=self.request.user.id,
usage_key=self.sequence.location,
)
read_grade = SubsectionGrade.read(
read_grade = ReadSubsectionGrade(
self.sequence,
saved_model,
self.course_structure,

View File

@@ -222,7 +222,7 @@ class CourseGradeReport(object):
Returns a list of all applicable column headers for this grade report.
"""
return (
["Student ID", "Email", "Username", "Grade"] +
["Student ID", "Email", "Username"] +
self._grades_header(context) +
(['Cohort Name'] if context.cohorts_enabled else []) +
[u'Experiment Group ({})'.format(partition.name) for partition in context.course_experiments] +
@@ -278,7 +278,7 @@ class CourseGradeReport(object):
Returns the applicable grades-related headers for this report.
"""
graded_assignments = context.graded_assignments
grades_header = []
grades_header = ["Grade"]
for assignment_info in graded_assignments.itervalues():
if assignment_info['separate_subsection_avg_headers']:
grades_header.extend(assignment_info['subsection_headers'].itervalues())