Commit 7899528b by Nimisha Asthagiri

Compute Attempted versus Grade of Zero

TNL-5953
parent 863aa78b
......@@ -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
......
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