Compute Attempted versus Grade of Zero
TNL-5953
This commit is contained in:
@@ -21,13 +21,15 @@ class ScoreBase(object):
|
||||
display_name (string) - the display name of the module
|
||||
module_id (UsageKey) - the location of the module
|
||||
graded (boolean) - whether or not this module is graded
|
||||
attempted (boolean) - whether the module was attempted
|
||||
"""
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
def __init__(self, graded, display_name, module_id):
|
||||
def __init__(self, graded, display_name, module_id, attempted):
|
||||
self.graded = graded
|
||||
self.display_name = display_name
|
||||
self.module_id = module_id
|
||||
self.attempted = attempted
|
||||
|
||||
def __eq__(self, other):
|
||||
if type(other) is type(self):
|
||||
@@ -91,15 +93,19 @@ def aggregate_scores(scores, display_name="summary", location=None):
|
||||
"""
|
||||
total_correct_graded = float_sum(score.earned for score in scores if score.graded)
|
||||
total_possible_graded = float_sum(score.possible for score in scores if score.graded)
|
||||
any_attempted_graded = any(score.attempted for score in scores if score.graded)
|
||||
|
||||
total_correct = float_sum(score.earned for score in scores)
|
||||
total_possible = float_sum(score.possible for score in scores)
|
||||
any_attempted = any(score.attempted for score in scores)
|
||||
|
||||
#regardless of whether it is graded
|
||||
all_total = AggregatedScore(total_correct, total_possible, False, display_name, location)
|
||||
# regardless of whether it is graded
|
||||
all_total = AggregatedScore(total_correct, total_possible, False, display_name, location, any_attempted)
|
||||
|
||||
#selecting only graded things
|
||||
graded_total = AggregatedScore(total_correct_graded, total_possible_graded, True, display_name, location)
|
||||
# selecting only graded things
|
||||
graded_total = AggregatedScore(
|
||||
total_correct_graded, total_possible_graded, True, display_name, location, any_attempted_graded,
|
||||
)
|
||||
|
||||
return all_total, graded_total
|
||||
|
||||
|
||||
@@ -12,9 +12,12 @@ class GradesheetTest(unittest.TestCase):
|
||||
|
||||
def test_weighted_grading(self):
|
||||
scores = []
|
||||
agg_fields = dict(display_name="aggregated_score", module_id=None)
|
||||
prob_fields = dict(display_name="problem_score", module_id=None, raw_earned=0, raw_possible=0, weight=0)
|
||||
agg_fields = dict(display_name="aggregated_score", module_id=None, attempted=False)
|
||||
prob_fields = dict(
|
||||
display_name="problem_score", module_id=None, raw_earned=0, raw_possible=0, weight=0, attempted=False,
|
||||
)
|
||||
|
||||
# No scores
|
||||
all_total, graded_total = aggregate_scores(scores, display_name=agg_fields['display_name'])
|
||||
self.assertEqual(
|
||||
all_total,
|
||||
@@ -25,6 +28,7 @@ class GradesheetTest(unittest.TestCase):
|
||||
AggregatedScore(tw_earned=0, tw_possible=0, graded=True, **agg_fields),
|
||||
)
|
||||
|
||||
# (0/5 non-graded)
|
||||
scores.append(ProblemScore(weighted_earned=0, weighted_possible=5, graded=False, **prob_fields))
|
||||
all_total, graded_total = aggregate_scores(scores, display_name=agg_fields['display_name'])
|
||||
self.assertEqual(
|
||||
@@ -36,6 +40,9 @@ class GradesheetTest(unittest.TestCase):
|
||||
AggregatedScore(tw_earned=0, tw_possible=0, graded=True, **agg_fields),
|
||||
)
|
||||
|
||||
# (0/5 non-graded) + (3/5 graded) = 3/10 total, 3/5 graded
|
||||
prob_fields['attempted'] = True
|
||||
agg_fields['attempted'] = True
|
||||
scores.append(ProblemScore(weighted_earned=3, weighted_possible=5, graded=True, **prob_fields))
|
||||
all_total, graded_total = aggregate_scores(scores, display_name=agg_fields['display_name'])
|
||||
self.assertAlmostEqual(
|
||||
@@ -47,6 +54,7 @@ class GradesheetTest(unittest.TestCase):
|
||||
AggregatedScore(tw_earned=3, tw_possible=5, graded=True, **agg_fields),
|
||||
)
|
||||
|
||||
# (0/5 non-graded) + (3/5 graded) + (2/5 graded) = 5/15 total, 5/10 graded
|
||||
scores.append(ProblemScore(weighted_earned=2, weighted_possible=5, graded=True, **prob_fields))
|
||||
all_total, graded_total = aggregate_scores(scores, display_name=agg_fields['display_name'])
|
||||
self.assertAlmostEqual(
|
||||
@@ -73,25 +81,26 @@ class GraderTest(unittest.TestCase):
|
||||
'Midterm': [],
|
||||
}
|
||||
|
||||
common_fields = dict(graded=True, module_id=None, attempted=True)
|
||||
test_gradesheet = {
|
||||
'Homework': [
|
||||
AggregatedScore(tw_earned=2, tw_possible=20.0, graded=True, display_name='hw1', module_id=None),
|
||||
AggregatedScore(tw_earned=16, tw_possible=16.0, graded=True, display_name='hw2', module_id=None)
|
||||
AggregatedScore(tw_earned=2, tw_possible=20.0, display_name='hw1', **common_fields),
|
||||
AggregatedScore(tw_earned=16, tw_possible=16.0, display_name='hw2', **common_fields),
|
||||
],
|
||||
|
||||
# The dropped scores should be from the assignments that don't exist yet
|
||||
'Lab': [
|
||||
AggregatedScore(tw_earned=1, tw_possible=2.0, graded=True, display_name='lab1', module_id=None), # Dropped
|
||||
AggregatedScore(tw_earned=1, tw_possible=1.0, graded=True, display_name='lab2', module_id=None),
|
||||
AggregatedScore(tw_earned=1, tw_possible=1.0, graded=True, display_name='lab3', module_id=None),
|
||||
AggregatedScore(tw_earned=5, tw_possible=25.0, graded=True, display_name='lab4', module_id=None), # Dropped
|
||||
AggregatedScore(tw_earned=3, tw_possible=4.0, graded=True, display_name='lab5', module_id=None), # Dropped
|
||||
AggregatedScore(tw_earned=6, tw_possible=7.0, graded=True, display_name='lab6', module_id=None),
|
||||
AggregatedScore(tw_earned=5, tw_possible=6.0, graded=True, display_name='lab7', module_id=None),
|
||||
AggregatedScore(tw_earned=1, tw_possible=2.0, display_name='lab1', **common_fields), # Dropped
|
||||
AggregatedScore(tw_earned=1, tw_possible=1.0, display_name='lab2', **common_fields),
|
||||
AggregatedScore(tw_earned=1, tw_possible=1.0, display_name='lab3', **common_fields),
|
||||
AggregatedScore(tw_earned=5, tw_possible=25.0, display_name='lab4', **common_fields), # Dropped
|
||||
AggregatedScore(tw_earned=3, tw_possible=4.0, display_name='lab5', **common_fields), # Dropped
|
||||
AggregatedScore(tw_earned=6, tw_possible=7.0, display_name='lab6', **common_fields),
|
||||
AggregatedScore(tw_earned=5, tw_possible=6.0, display_name='lab7', **common_fields),
|
||||
],
|
||||
|
||||
'Midterm': [
|
||||
AggregatedScore(tw_earned=50.5, tw_possible=100, graded=True, display_name="Midterm Exam", module_id=None),
|
||||
AggregatedScore(tw_earned=50.5, tw_possible=100, display_name="Midterm Exam", **common_fields),
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -63,6 +63,14 @@ class SubsectionGrade(object):
|
||||
"""
|
||||
return self.locations_to_scores.values()
|
||||
|
||||
@property
|
||||
def attempted(self):
|
||||
"""
|
||||
Returns whether any problem in this subsection
|
||||
was attempted by the student.
|
||||
"""
|
||||
return self.all_total.attempted
|
||||
|
||||
def init_from_structure(self, student, course_structure, submissions_scores, csm_scores):
|
||||
"""
|
||||
Compute the grade of this subsection for the given student and course.
|
||||
@@ -90,6 +98,7 @@ class SubsectionGrade(object):
|
||||
graded=True,
|
||||
display_name=self.display_name,
|
||||
module_id=self.location,
|
||||
attempted=True, # TODO TNL-5930
|
||||
)
|
||||
self.all_total = AggregatedScore(
|
||||
tw_earned=model.earned_all,
|
||||
@@ -97,6 +106,7 @@ class SubsectionGrade(object):
|
||||
graded=False,
|
||||
display_name=self.display_name,
|
||||
module_id=self.location,
|
||||
attempted=True, # TODO TNL-5930
|
||||
)
|
||||
self._log_event(log.debug, u"init_from_model", student)
|
||||
return self
|
||||
|
||||
@@ -102,7 +102,7 @@ def get_score(submissions_scores, csm_scores, persisted_block, block):
|
||||
|
||||
# Priority order for retrieving the scores:
|
||||
# submissions API -> CSM -> grades persisted block -> latest block content
|
||||
raw_earned, raw_possible, weighted_earned, weighted_possible = (
|
||||
raw_earned, raw_possible, weighted_earned, weighted_possible, attempted = (
|
||||
_get_score_from_submissions(submissions_scores, block) or
|
||||
_get_score_from_csm(csm_scores, block, weight) or
|
||||
_get_score_from_persisted_or_latest_block(persisted_block, block, weight)
|
||||
@@ -124,6 +124,7 @@ def get_score(submissions_scores, csm_scores, persisted_block, block):
|
||||
graded,
|
||||
display_name=display_name_with_default_escaped(block),
|
||||
module_id=block.location,
|
||||
attempted=attempted,
|
||||
)
|
||||
|
||||
|
||||
@@ -151,9 +152,10 @@ def _get_score_from_submissions(submissions_scores, block):
|
||||
if submissions_scores:
|
||||
submission_value = submissions_scores.get(unicode(block.location))
|
||||
if submission_value:
|
||||
attempted = True
|
||||
weighted_earned, weighted_possible = submission_value
|
||||
assert weighted_earned >= 0.0 and weighted_possible > 0.0 # per contract from submissions API
|
||||
return (None, None) + (weighted_earned, weighted_possible)
|
||||
return (None, None) + (weighted_earned, weighted_possible) + (attempted,)
|
||||
|
||||
|
||||
def _get_score_from_csm(csm_scores, block, weight):
|
||||
@@ -175,9 +177,14 @@ def _get_score_from_csm(csm_scores, block, weight):
|
||||
score = csm_scores.get(block.location)
|
||||
has_valid_score = score and score.total is not None
|
||||
if has_valid_score:
|
||||
raw_earned = score.correct if score.correct is not None else 0.0
|
||||
if score.correct is not None:
|
||||
attempted = True
|
||||
raw_earned = score.correct
|
||||
else:
|
||||
attempted = False
|
||||
raw_earned = 0.0
|
||||
raw_possible = score.total
|
||||
return (raw_earned, raw_possible) + weighted_score(raw_earned, raw_possible, weight)
|
||||
return (raw_earned, raw_possible) + weighted_score(raw_earned, raw_possible, weight) + (attempted,)
|
||||
|
||||
|
||||
def _get_score_from_persisted_or_latest_block(persisted_block, block, weight):
|
||||
@@ -188,16 +195,20 @@ def _get_score_from_persisted_or_latest_block(persisted_block, block, weight):
|
||||
the latest block content.
|
||||
"""
|
||||
raw_earned = 0.0
|
||||
attempted = False
|
||||
|
||||
if persisted_block:
|
||||
raw_possible = persisted_block.raw_possible
|
||||
else:
|
||||
raw_possible = block.transformer_data[GradesTransformer].max_score
|
||||
|
||||
# TODO TNL-5982 remove defensive code for scorables without max_score
|
||||
if raw_possible is None:
|
||||
return (raw_earned, raw_possible) + (None, None)
|
||||
weighted_scores = (None, None)
|
||||
else:
|
||||
return (raw_earned, raw_possible) + weighted_score(raw_earned, raw_possible, weight)
|
||||
weighted_scores = weighted_score(raw_earned, raw_possible, weight)
|
||||
|
||||
return (raw_earned, raw_possible) + weighted_scores + (attempted,)
|
||||
|
||||
|
||||
def _get_weight_from_block(persisted_block, block):
|
||||
|
||||
@@ -248,6 +248,7 @@ class TestWeightedProblems(SharedModuleStoreTestCase):
|
||||
graded=expected_graded,
|
||||
display_name=None, # problem-specific, filled in by _verify_grades
|
||||
module_id=None, # problem-specific, filled in by _verify_grades
|
||||
attempted=True,
|
||||
)
|
||||
self._verify_grades(raw_earned, raw_possible, weight, expected_score)
|
||||
|
||||
|
||||
@@ -228,6 +228,7 @@ class TestSubsectionGradeFactory(ProblemSubmissionTestMixin, GradeTestBase):
|
||||
self.assertFalse(mock_create_grade.called)
|
||||
|
||||
self.assertEqual(grade_a.url_name, grade_b.url_name)
|
||||
grade_b.all_total.attempted = False # TODO TNL-5930
|
||||
self.assertEqual(grade_a.all_total, grade_b.all_total)
|
||||
|
||||
def test_update(self):
|
||||
@@ -342,6 +343,7 @@ class SubsectionGradeTest(GradeTestBase):
|
||||
)
|
||||
|
||||
self.assertEqual(input_grade.url_name, loaded_grade.url_name)
|
||||
loaded_grade.all_total.attempted = False # TODO TNL-5930
|
||||
self.assertEqual(input_grade.all_total, loaded_grade.all_total)
|
||||
|
||||
|
||||
@@ -409,7 +411,7 @@ class TestMultipleProblemTypesSubsectionScores(SharedModuleStoreTestCase):
|
||||
# Configure one block to return no possible score, the rest to return 3.0 earned / 7.0 possible
|
||||
block_count = self.SCORED_BLOCK_COUNT - 1
|
||||
mock_score.side_effect = itertools.chain(
|
||||
[(earned_per_block, None, earned_per_block, None)],
|
||||
[(earned_per_block, None, earned_per_block, None, True)],
|
||||
itertools.repeat(mock_score.return_value)
|
||||
)
|
||||
score = subsection_factory.update(self.seq1)
|
||||
|
||||
@@ -52,7 +52,7 @@ class TestGetScore(TestCase):
|
||||
PersistedBlockValue = namedtuple('PersistedBlockValue', 'exists, raw_possible, weight, graded')
|
||||
ContentBlockValue = namedtuple('ContentBlockValue', 'raw_possible, weight, explicit_graded')
|
||||
ExpectedResult = namedtuple(
|
||||
'ExpectedResult', 'raw_earned, raw_possible, weighted_earned, weighted_possible, weight, graded'
|
||||
'ExpectedResult', 'raw_earned, raw_possible, weighted_earned, weighted_possible, weight, graded, attempted'
|
||||
)
|
||||
|
||||
def _create_submissions_scores(self, submission_value):
|
||||
@@ -113,7 +113,9 @@ class TestGetScore(TestCase):
|
||||
PersistedBlockValue(exists=True, raw_possible=5, weight=40, graded=True),
|
||||
ContentBlockValue(raw_possible=1, weight=20, explicit_graded=False),
|
||||
ExpectedResult(
|
||||
raw_earned=None, raw_possible=None, weighted_earned=50, weighted_possible=100, weight=40, graded=True
|
||||
raw_earned=None, raw_possible=None,
|
||||
weighted_earned=50, weighted_possible=100,
|
||||
weight=40, graded=True, attempted=True,
|
||||
),
|
||||
),
|
||||
# same as above, except submissions doesn't exist; CSM values used
|
||||
@@ -123,7 +125,21 @@ class TestGetScore(TestCase):
|
||||
PersistedBlockValue(exists=True, raw_possible=5, weight=40, graded=True),
|
||||
ContentBlockValue(raw_possible=1, weight=20, explicit_graded=False),
|
||||
ExpectedResult(
|
||||
raw_earned=10, raw_possible=40, weighted_earned=10, weighted_possible=40, weight=40, graded=True
|
||||
raw_earned=10, raw_possible=40,
|
||||
weighted_earned=10, weighted_possible=40,
|
||||
weight=40, graded=True, attempted=True,
|
||||
),
|
||||
),
|
||||
# CSM values exist, but with NULL earned score treated as not-attempted
|
||||
(
|
||||
SubmissionValue(exists=False, weighted_earned=50, weighted_possible=100),
|
||||
CSMValue(exists=True, raw_earned=None, raw_possible=40),
|
||||
PersistedBlockValue(exists=True, raw_possible=5, weight=40, graded=True),
|
||||
ContentBlockValue(raw_possible=1, weight=20, explicit_graded=False),
|
||||
ExpectedResult(
|
||||
raw_earned=0, raw_possible=40,
|
||||
weighted_earned=0, weighted_possible=40,
|
||||
weight=40, graded=True, attempted=False,
|
||||
),
|
||||
),
|
||||
# neither submissions nor CSM exist; Persisted values used
|
||||
@@ -133,7 +149,9 @@ class TestGetScore(TestCase):
|
||||
PersistedBlockValue(exists=True, raw_possible=5, weight=40, graded=True),
|
||||
ContentBlockValue(raw_possible=1, weight=20, explicit_graded=False),
|
||||
ExpectedResult(
|
||||
raw_earned=0, raw_possible=5, weighted_earned=0, weighted_possible=40, weight=40, graded=True
|
||||
raw_earned=0, raw_possible=5,
|
||||
weighted_earned=0, weighted_possible=40,
|
||||
weight=40, graded=True, attempted=False,
|
||||
),
|
||||
),
|
||||
# none of submissions, CSM, or persisted exist; Latest content values used
|
||||
@@ -143,7 +161,9 @@ class TestGetScore(TestCase):
|
||||
PersistedBlockValue(exists=False, raw_possible=5, weight=40, graded=True),
|
||||
ContentBlockValue(raw_possible=1, weight=20, explicit_graded=False),
|
||||
ExpectedResult(
|
||||
raw_earned=0, raw_possible=1, weighted_earned=0, weighted_possible=20, weight=20, graded=False
|
||||
raw_earned=0, raw_possible=1,
|
||||
weighted_earned=0, weighted_possible=20,
|
||||
weight=20, graded=False, attempted=False,
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -259,9 +279,10 @@ class TestInternalGetScoreFromBlock(TestCase):
|
||||
Verifies the result of _get_score_from_persisted_or_latest_block is as expected.
|
||||
"""
|
||||
# pylint: disable=unbalanced-tuple-unpacking
|
||||
raw_earned, raw_possible, weighted_earned, weighted_possible = scores._get_score_from_persisted_or_latest_block(
|
||||
persisted_block, block, weight,
|
||||
)
|
||||
(
|
||||
raw_earned, raw_possible, weighted_earned, weighted_possible, attempted
|
||||
) = scores._get_score_from_persisted_or_latest_block(persisted_block, block, weight)
|
||||
|
||||
self.assertEquals(raw_earned, 0.0)
|
||||
self.assertEquals(raw_possible, expected_r_possible)
|
||||
self.assertEquals(weighted_earned, 0.0)
|
||||
@@ -269,6 +290,7 @@ class TestInternalGetScoreFromBlock(TestCase):
|
||||
self.assertEquals(weighted_possible, expected_r_possible)
|
||||
else:
|
||||
self.assertEquals(weighted_possible, weight)
|
||||
self.assertFalse(attempted)
|
||||
|
||||
@ddt.data(
|
||||
*itertools.product((0, 1, 5), (None, 0, 1, 5))
|
||||
|
||||
@@ -24,17 +24,27 @@ def mock_get_score(earned=0, possible=1):
|
||||
Mocks the get_score function to return a valid grade.
|
||||
"""
|
||||
with patch('lms.djangoapps.grades.new.subsection_grade.get_score') as mock_score:
|
||||
mock_score.return_value = ProblemScore(earned, possible, earned, possible, 1, True, None, None)
|
||||
mock_score.return_value = ProblemScore(
|
||||
raw_earned=earned,
|
||||
raw_possible=possible,
|
||||
weighted_earned=earned,
|
||||
weighted_possible=possible,
|
||||
weight=1,
|
||||
graded=True,
|
||||
display_name=None,
|
||||
module_id=None,
|
||||
attempted=True,
|
||||
)
|
||||
yield mock_score
|
||||
|
||||
|
||||
@contextmanager
|
||||
def mock_get_submissions_score(earned=0, possible=1):
|
||||
def mock_get_submissions_score(earned=0, possible=1, attempted=True):
|
||||
"""
|
||||
Mocks the _get_submissions_score function to return the specified values
|
||||
"""
|
||||
with patch('lms.djangoapps.grades.scores._get_score_from_submissions') as mock_score:
|
||||
mock_score.return_value = (earned, possible, earned, possible)
|
||||
mock_score.return_value = (earned, possible, earned, possible, attempted)
|
||||
yield mock_score
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user