Commit 09b235bc by Nimisha Asthagiri Committed by GitHub

Merge pull request #16162 from edx/naa/course-grade-report

Have SubsectionGrade compute problem_scores lazily
parents f0f5ebb3 33428519
""" """
SubsectionGrade Class SubsectionGrade Class
""" """
from abc import ABCMeta
from collections import OrderedDict from collections import OrderedDict
from logging import getLogger from logging import getLogger
...@@ -17,8 +18,10 @@ log = getLogger(__name__) ...@@ -17,8 +18,10 @@ log = getLogger(__name__)
class SubsectionGradeBase(object): class SubsectionGradeBase(object):
""" """
Class for Subsection Grades. Abstract base class for Subsection Grades.
""" """
__metaclass__ = ABCMeta
def __init__(self, subsection): def __init__(self, subsection):
self.location = subsection.location self.location = subsection.location
self.display_name = block_metadata_utils.display_name_with_default_escaped(subsection) self.display_name = block_metadata_utils.display_name_with_default_escaped(subsection)
...@@ -108,47 +111,53 @@ class ZeroSubsectionGrade(SubsectionGradeBase): ...@@ -108,47 +111,53 @@ class ZeroSubsectionGrade(SubsectionGradeBase):
return locations 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): __metaclass__ = ABCMeta
super(SubsectionGrade, self).__init__(subsection)
self.problem_scores = problem_scores def __init__(self, subsection, all_total, graded_total, override=None):
super(NonZeroSubsectionGrade, self).__init__(subsection)
self.all_total = all_total self.all_total = all_total
self.graded_total = graded_total self.graded_total = graded_total
self.override = override self.override = override
@classmethod @property
def create(cls, subsection, course_structure, submissions_scores, csm_scores): def attempted_graded(self):
""" return self.graded_total.first_attempted is not None
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) @staticmethod
def _compute_block_score(
block_key,
course_structure,
submissions_scores,
csm_scores,
persisted_block=None,
):
try:
block = course_structure[block_key]
except KeyError:
# It's possible that the user's access to that
# block has changed since the subsection grade
# was last persisted.
pass
else:
if getattr(block, 'has_score', False):
return get_score(
submissions_scores,
csm_scores,
persisted_block,
block,
)
@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
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( all_total = AggregatedScore(
tw_earned=model.earned_all, tw_earned=model.earned_all,
tw_possible=model.possible_all, tw_possible=model.possible_all,
...@@ -162,7 +171,51 @@ class SubsectionGrade(SubsectionGradeBase): ...@@ -162,7 +171,51 @@ class SubsectionGrade(SubsectionGradeBase):
first_attempted=model.first_attempted, first_attempted=model.first_attempted,
) )
override = model.override if hasattr(model, 'override') else None override = model.override if hasattr(model, 'override') else None
return cls(subsection, problem_scores, all_total, graded_total, override)
# 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 @classmethod
def bulk_create_models(cls, student, subsection_grades, course_key): def bulk_create_models(cls, student, subsection_grades, course_key):
...@@ -177,18 +230,6 @@ class SubsectionGrade(SubsectionGradeBase): ...@@ -177,18 +230,6 @@ class SubsectionGrade(SubsectionGradeBase):
] ]
return PersistentSubsectionGrade.bulk_create_grades(params, student.id, course_key) 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): def _should_persist_per_attempted(self, score_deleted=False):
""" """
Returns whether the SubsectionGrade's model should be Returns whether the SubsectionGrade's model should be
...@@ -202,34 +243,6 @@ class SubsectionGrade(SubsectionGradeBase): ...@@ -202,34 +243,6 @@ class SubsectionGrade(SubsectionGradeBase):
score_deleted score_deleted
) )
@staticmethod
def _compute_block_score(
block_key,
course_structure,
submissions_scores,
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:
# It's possible that the user's access to that
# block has changed since the subsection grade
# was last persisted.
pass
else:
if getattr(block, 'has_score', False):
return get_score(
submissions_scores,
csm_scores,
persisted_block,
block,
)
def _persisted_model_params(self, student): def _persisted_model_params(self, student):
""" """
Returns the parameters for creating/updating the Returns the parameters for creating/updating the
...@@ -258,25 +271,3 @@ class SubsectionGrade(SubsectionGradeBase): ...@@ -258,25 +271,3 @@ class SubsectionGrade(SubsectionGradeBase):
for location, score in for location, score in
self.problem_scores.iteritems() 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,
)
)
...@@ -12,7 +12,7 @@ from student.models import anonymous_id_for_user ...@@ -12,7 +12,7 @@ from student.models import anonymous_id_for_user
from submissions import api as submissions_api from submissions import api as submissions_api
from .course_data import CourseData from .course_data import CourseData
from .subsection_grade import SubsectionGrade, ZeroSubsectionGrade from .subsection_grade import CreateSubsectionGrade, ReadSubsectionGrade, ZeroSubsectionGrade
log = getLogger(__name__) log = getLogger(__name__)
...@@ -43,7 +43,7 @@ class SubsectionGradeFactory(object): ...@@ -43,7 +43,7 @@ class SubsectionGradeFactory(object):
if assume_zero_if_absent(self.course_data.course_key): if assume_zero_if_absent(self.course_data.course_key):
subsection_grade = ZeroSubsectionGrade(subsection, self.course_data) subsection_grade = ZeroSubsectionGrade(subsection, self.course_data)
else: else:
subsection_grade = SubsectionGrade.create( subsection_grade = CreateSubsectionGrade(
subsection, self.course_data.structure, self._submissions_scores, self._csm_scores, subsection, self.course_data.structure, self._submissions_scores, self._csm_scores,
) )
if should_persist_grades(self.course_data.course_key): if should_persist_grades(self.course_data.course_key):
...@@ -58,7 +58,7 @@ class SubsectionGradeFactory(object): ...@@ -58,7 +58,7 @@ class SubsectionGradeFactory(object):
""" """
Bulk creates all the unsaved subsection_grades to this point. 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.student, self._unsaved_subsection_grades.values(), self.course_data.course_key
) )
self._unsaved_subsection_grades.clear() self._unsaved_subsection_grades.clear()
...@@ -69,7 +69,7 @@ class SubsectionGradeFactory(object): ...@@ -69,7 +69,7 @@ class SubsectionGradeFactory(object):
""" """
self._log_event(log.debug, u"update, subsection: {}".format(subsection.location), subsection) 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, subsection, self.course_data.structure, self._submissions_scores, self._csm_scores,
) )
...@@ -80,7 +80,7 @@ class SubsectionGradeFactory(object): ...@@ -80,7 +80,7 @@ class SubsectionGradeFactory(object):
except PersistentSubsectionGrade.DoesNotExist: except PersistentSubsectionGrade.DoesNotExist:
pass pass
else: else:
orig_subsection_grade = SubsectionGrade.read( orig_subsection_grade = ReadSubsectionGrade(
subsection, grade_model, self.course_data.structure, self._submissions_scores, self._csm_scores, subsection, grade_model, self.course_data.structure, self._submissions_scores, self._csm_scores,
) )
if not is_score_higher_or_equal( if not is_score_higher_or_equal(
...@@ -125,7 +125,7 @@ class SubsectionGradeFactory(object): ...@@ -125,7 +125,7 @@ class SubsectionGradeFactory(object):
saved_subsection_grades = self._get_bulk_cached_subsection_grades() saved_subsection_grades = self._get_bulk_cached_subsection_grades()
grade = saved_subsection_grades.get(subsection.location) grade = saved_subsection_grades.get(subsection.location)
if grade: if grade:
return SubsectionGrade.read( return ReadSubsectionGrade(
subsection, grade, self.course_data.structure, self._submissions_scores, self._csm_scores, subsection, grade, self.course_data.structure, self._submissions_scores, self._csm_scores,
) )
......
...@@ -14,7 +14,7 @@ from xmodule.modulestore.tests.factories import CourseFactory ...@@ -14,7 +14,7 @@ from xmodule.modulestore.tests.factories import CourseFactory
from ..config.waffle import ASSUME_ZERO_GRADE_IF_ABSENT, waffle from ..config.waffle import ASSUME_ZERO_GRADE_IF_ABSENT, waffle
from ..course_grade import CourseGrade, ZeroCourseGrade from ..course_grade import CourseGrade, ZeroCourseGrade
from ..course_grade_factory import CourseGradeFactory from ..course_grade_factory import CourseGradeFactory
from ..subsection_grade import SubsectionGrade, ZeroSubsectionGrade from ..subsection_grade import ReadSubsectionGrade, ZeroSubsectionGrade
from .base import GradeTestBase from .base import GradeTestBase
from .utils import mock_get_score from .utils import mock_get_score
...@@ -131,7 +131,7 @@ class TestCourseGradeFactory(GradeTestBase): ...@@ -131,7 +131,7 @@ class TestCourseGradeFactory(GradeTestBase):
course_grade = CourseGradeFactory().update(self.request.user, self.course) course_grade = CourseGradeFactory().update(self.request.user, self.course)
subsection1_grade = course_grade.subsection_grades[self.sequence.location] subsection1_grade = course_grade.subsection_grades[self.sequence.location]
subsection2_grade = course_grade.subsection_grades[self.sequence2.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) self.assertIsInstance(subsection2_grade, ZeroSubsectionGrade)
@ddt.data(True, False) @ddt.data(True, False)
......
from ..models import PersistentSubsectionGrade from ..models import PersistentSubsectionGrade
from ..subsection_grade import SubsectionGrade from ..subsection_grade import CreateSubsectionGrade, ReadSubsectionGrade
from .utils import mock_get_score from .utils import mock_get_score
from .base import GradeTestBase from .base import GradeTestBase
...@@ -8,7 +8,7 @@ class SubsectionGradeTest(GradeTestBase): ...@@ -8,7 +8,7 @@ class SubsectionGradeTest(GradeTestBase):
def test_create_and_read(self): def test_create_and_read(self):
with mock_get_score(1, 2): with mock_get_score(1, 2):
# Create a grade that *isn't* saved to the database # Create a grade that *isn't* saved to the database
created_grade = SubsectionGrade.create( created_grade = CreateSubsectionGrade(
self.sequence, self.sequence,
self.course_structure, self.course_structure,
self.subsection_grade_factory._submissions_scores, self.subsection_grade_factory._submissions_scores,
...@@ -25,7 +25,7 @@ class SubsectionGradeTest(GradeTestBase): ...@@ -25,7 +25,7 @@ class SubsectionGradeTest(GradeTestBase):
user_id=self.request.user.id, user_id=self.request.user.id,
usage_key=self.sequence.location, usage_key=self.sequence.location,
) )
read_grade = SubsectionGrade.read( read_grade = ReadSubsectionGrade(
self.sequence, self.sequence,
saved_model, saved_model,
self.course_structure, self.course_structure,
......
...@@ -222,7 +222,7 @@ class CourseGradeReport(object): ...@@ -222,7 +222,7 @@ class CourseGradeReport(object):
Returns a list of all applicable column headers for this grade report. Returns a list of all applicable column headers for this grade report.
""" """
return ( return (
["Student ID", "Email", "Username", "Grade"] + ["Student ID", "Email", "Username"] +
self._grades_header(context) + self._grades_header(context) +
(['Cohort Name'] if context.cohorts_enabled else []) + (['Cohort Name'] if context.cohorts_enabled else []) +
[u'Experiment Group ({})'.format(partition.name) for partition in context.course_experiments] + [u'Experiment Group ({})'.format(partition.name) for partition in context.course_experiments] +
...@@ -278,7 +278,7 @@ class CourseGradeReport(object): ...@@ -278,7 +278,7 @@ class CourseGradeReport(object):
Returns the applicable grades-related headers for this report. Returns the applicable grades-related headers for this report.
""" """
graded_assignments = context.graded_assignments graded_assignments = context.graded_assignments
grades_header = [] grades_header = ["Grade"]
for assignment_info in graded_assignments.itervalues(): for assignment_info in graded_assignments.itervalues():
if assignment_info['separate_subsection_avg_headers']: if assignment_info['separate_subsection_avg_headers']:
grades_header.extend(assignment_info['subsection_headers'].itervalues()) grades_header.extend(assignment_info['subsection_headers'].itervalues())
......
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