From 3860f333d6a79492074b983590e8ddb8b7922b3e Mon Sep 17 00:00:00 2001 From: Bridger Maxwell Date: Mon, 16 Apr 2012 16:13:51 -0400 Subject: [PATCH] Added parser for creating graders from a dictionary representation. --- djangoapps/courseware/grades.py | 42 +++++++-- djangoapps/courseware/tests.py | 155 +++++++++++++++++++++----------- 2 files changed, 137 insertions(+), 60 deletions(-) diff --git a/djangoapps/courseware/grades.py b/djangoapps/courseware/grades.py index 0daa0a8940..cf675a7b02 100644 --- a/djangoapps/courseware/grades.py +++ b/djangoapps/courseware/grades.py @@ -59,6 +59,36 @@ class CourseGrader(object): def grade(self, grade_sheet): raise NotImplementedError + + @classmethod + def graderFromConf(cls, conf): + if isinstance(conf, CourseGrader): + return conf + + subgraders = [] + for subgraderconf in conf: + subgraderconf = subgraderconf.copy() + weight = subgraderconf.pop("weight", 0) + try: + if 'min_count' in subgraderconf: + #This is an AssignmentFormatGrader + subgrader = AssignmentFormatGrader(**subgraderconf) + subgraders.append( (subgrader, subgrader.category, weight) ) + elif 'section_name' in subgraderconf: + #This is an SingleSectionGrader + subgrader = SingleSectionGrader(**subgraderconf) + subgraders.append( (subgrader, subgrader.category, weight) ) + else: + raise ValueError("Configuration has no appropriate grader class.") + + except TypeError as error: + log.error("Unable to parse grader configuration:\n" + subgraderconf + "\nError was:\n" + error) + except ValueError as error: + log.error("Unable to parse grader configuration:\n" + subgraderconf + "\nError was:\n" + error) + + return WeightedSubsectionsGrader( subgraders ) + + class WeightedSubsectionsGrader(CourseGrader): """ This grader takes a list of tuples containing (grader, category_name, weight) and computes @@ -153,9 +183,9 @@ class AssignmentFormatGrader(CourseGrader): sections in this format must be specified (even if those sections haven't been written yet). - min_number defines how many assignments are expected throughout the course. Placeholder - scores (of 0) will be inserted if the number of matching sections in the course is < min_number. - If there number of matching sections in the course is > min_number, min_number will be ignored. + min_count defines how many assignments are expected throughout the course. Placeholder + scores (of 0) will be inserted if the number of matching sections in the course is < min_count. + If there number of matching sections in the course is > min_count, min_count will be ignored. category should be presentable to the user, but may not appear. When the grade breakdown is displayed, scores from the same category will be similar (for example, by color). @@ -167,9 +197,9 @@ class AssignmentFormatGrader(CourseGrader): "HW". """ - def __init__(self, course_format, min_number, drop_count, category = None, section_type = None, short_label = None): + def __init__(self, course_format, min_count, drop_count, category = None, section_type = None, short_label = None): self.course_format = course_format - self.min_number = min_number + self.min_count = min_count self.drop_count = drop_count self.category = category or self.course_format self.section_type = section_type or self.course_format @@ -196,7 +226,7 @@ class AssignmentFormatGrader(CourseGrader): #Figure the homework scores scores = grade_sheet.get(self.course_format, []) breakdown = [] - for i in range( max(self.min_number, len(scores)) ): + for i in range( max(self.min_count, len(scores)) ): if i < len(scores): percentage = scores[i].earned / float(scores[i].possible) summary = "{section_type} {index} - {name} - {percent:.0%} ({earned:.3n}/{possible:.3n})".format(index = i+1, diff --git a/djangoapps/courseware/tests.py b/djangoapps/courseware/tests.py index 8759b51c79..1785d348d8 100644 --- a/djangoapps/courseware/tests.py +++ b/djangoapps/courseware/tests.py @@ -4,7 +4,7 @@ import numpy import courseware.modules import courseware.capa.calc as calc -from grades import Score, aggregate_scores, WeightedSubsectionsGrader, SingleSectionGrader, AssignmentFormatGrader +from grades import Score, aggregate_scores, CourseGrader, WeightedSubsectionsGrader, SingleSectionGrader, AssignmentFormatGrader class ModelsTest(unittest.TestCase): def setUp(self): @@ -140,59 +140,6 @@ class GraderTest(unittest.TestCase): self.assertAlmostEqual( graded['percent'], 0.2 ) self.assertEqual( len(graded['section_breakdown']), 1 ) - def test_WeightedSubsectionsGrader(self): - #First, a few sub graders - homeworkGrader = AssignmentFormatGrader("Homework", 12, 2) - labGrader = AssignmentFormatGrader("Lab", 7, 3) - midtermGrader = SingleSectionGrader("Midterm", "Midterm Exam") - - weightedGrader = WeightedSubsectionsGrader( [(homeworkGrader, homeworkGrader.category, 0.25), (labGrader, labGrader.category, 0.25), - (midtermGrader, midtermGrader.category, 0.5)] ) - - overOneWeightsGrader = WeightedSubsectionsGrader( [(homeworkGrader, homeworkGrader.category, 0.5), (labGrader, labGrader.category, 0.5), - (midtermGrader, midtermGrader.category, 0.5)] ) - - #The midterm should have all weight on this one - zeroWeightsGrader = WeightedSubsectionsGrader( [(homeworkGrader, homeworkGrader.category, 0.0), (labGrader, labGrader.category, 0.0), - (midtermGrader, midtermGrader.category, 0.5)] ) - - #This should always have a final percent of zero - allZeroWeightsGrader = WeightedSubsectionsGrader( [(homeworkGrader, homeworkGrader.category, 0.0), (labGrader, labGrader.category, 0.0), - (midtermGrader, midtermGrader.category, 0.0)] ) - - - graded = weightedGrader.grade(self.test_gradesheet) - self.assertAlmostEqual( graded['percent'], 0.5106547619047619 ) - self.assertEqual( len(graded['section_breakdown']), (12 + 1) + (7+1) + 1 ) - self.assertEqual( len(graded['grade_breakdown']), 3 ) - - graded = overOneWeightsGrader.grade(self.test_gradesheet) - self.assertAlmostEqual( graded['percent'], 0.7688095238095238 ) - self.assertEqual( len(graded['section_breakdown']), (12 + 1) + (7+1) + 1 ) - self.assertEqual( len(graded['grade_breakdown']), 3 ) - - graded = zeroWeightsGrader.grade(self.test_gradesheet) - self.assertAlmostEqual( graded['percent'], 0.2525 ) - self.assertEqual( len(graded['section_breakdown']), (12 + 1) + (7+1) + 1 ) - self.assertEqual( len(graded['grade_breakdown']), 3 ) - - - graded = allZeroWeightsGrader.grade(self.test_gradesheet) - self.assertAlmostEqual( graded['percent'], 0.0 ) - self.assertEqual( len(graded['section_breakdown']), (12 + 1) + (7+1) + 1 ) - self.assertEqual( len(graded['grade_breakdown']), 3 ) - - for graded in [ weightedGrader.grade(self.empty_gradesheet), - weightedGrader.grade(self.incomplete_gradesheet), - zeroWeightsGrader.grade(self.empty_gradesheet), - allZeroWeightsGrader.grade(self.empty_gradesheet)]: - self.assertAlmostEqual( graded['percent'], 0.0 ) - #section_breakdown should have all subsections from before - self.assertEqual( len(graded['section_breakdown']), (12 + 1) + (7+1) + 1 ) - self.assertEqual( len(graded['grade_breakdown']), 3 ) - - - def test_assignmentFormatGrader(self): homeworkGrader = AssignmentFormatGrader("Homework", 12, 2) noDropGrader = AssignmentFormatGrader("Homework", 12, 0) @@ -228,12 +175,112 @@ class GraderTest(unittest.TestCase): self.assertEqual( len(graded['section_breakdown']), 7 + 1 ) + def test_WeightedSubsectionsGrader(self): + #First, a few sub graders + homeworkGrader = AssignmentFormatGrader("Homework", 12, 2) + labGrader = AssignmentFormatGrader("Lab", 7, 3) + midtermGrader = SingleSectionGrader("Midterm", "Midterm Exam") + + weightedGrader = WeightedSubsectionsGrader( [(homeworkGrader, homeworkGrader.category, 0.25), (labGrader, labGrader.category, 0.25), + (midtermGrader, midtermGrader.category, 0.5)] ) + + overOneWeightsGrader = WeightedSubsectionsGrader( [(homeworkGrader, homeworkGrader.category, 0.5), (labGrader, labGrader.category, 0.5), + (midtermGrader, midtermGrader.category, 0.5)] ) + + #The midterm should have all weight on this one + zeroWeightsGrader = WeightedSubsectionsGrader( [(homeworkGrader, homeworkGrader.category, 0.0), (labGrader, labGrader.category, 0.0), + (midtermGrader, midtermGrader.category, 0.5)] ) + + #This should always have a final percent of zero + allZeroWeightsGrader = WeightedSubsectionsGrader( [(homeworkGrader, homeworkGrader.category, 0.0), (labGrader, labGrader.category, 0.0), + (midtermGrader, midtermGrader.category, 0.0)] ) + + emptyGrader = WeightedSubsectionsGrader( [] ) + + graded = weightedGrader.grade(self.test_gradesheet) + self.assertAlmostEqual( graded['percent'], 0.5106547619047619 ) + self.assertEqual( len(graded['section_breakdown']), (12 + 1) + (7+1) + 1 ) + self.assertEqual( len(graded['grade_breakdown']), 3 ) + + graded = overOneWeightsGrader.grade(self.test_gradesheet) + self.assertAlmostEqual( graded['percent'], 0.7688095238095238 ) + self.assertEqual( len(graded['section_breakdown']), (12 + 1) + (7+1) + 1 ) + self.assertEqual( len(graded['grade_breakdown']), 3 ) + + graded = zeroWeightsGrader.grade(self.test_gradesheet) + self.assertAlmostEqual( graded['percent'], 0.2525 ) + self.assertEqual( len(graded['section_breakdown']), (12 + 1) + (7+1) + 1 ) + self.assertEqual( len(graded['grade_breakdown']), 3 ) + + + graded = allZeroWeightsGrader.grade(self.test_gradesheet) + self.assertAlmostEqual( graded['percent'], 0.0 ) + self.assertEqual( len(graded['section_breakdown']), (12 + 1) + (7+1) + 1 ) + self.assertEqual( len(graded['grade_breakdown']), 3 ) + + for graded in [ weightedGrader.grade(self.empty_gradesheet), + weightedGrader.grade(self.incomplete_gradesheet), + zeroWeightsGrader.grade(self.empty_gradesheet), + allZeroWeightsGrader.grade(self.empty_gradesheet)]: + self.assertAlmostEqual( graded['percent'], 0.0 ) + self.assertEqual( len(graded['section_breakdown']), (12 + 1) + (7+1) + 1 ) + self.assertEqual( len(graded['grade_breakdown']), 3 ) + + + graded = emptyGrader.grade(self.test_gradesheet) + self.assertAlmostEqual( graded['percent'], 0.0 ) + self.assertEqual( len(graded['section_breakdown']), 0 ) + self.assertEqual( len(graded['grade_breakdown']), 0 ) + + + def test_graderFromConf(self): + #Confs always produce a WeightedSubsectionsGrader, so we test this by repeating the test + #in test_WeightedSubsectionsGrader, but generate the graders with confs. + weightedGrader = CourseGrader.graderFromConf([ + { + 'course_format' : "Homework", + 'min_count' : 12, + 'drop_count' : 2, + 'short_label' : "HW", + 'weight' : 0.25, + }, + { + 'course_format' : "Lab", + 'min_count' : 7, + 'drop_count' : 3, + 'category' : "Labs", + 'weight' : 0.25 + }, + { + 'section_format' : "Midterm", + 'section_name' : "Midterm Exam", + 'short_label' : "Midterm", + 'weight' : 0.5, + }, + ]) + emptyGrader = CourseGrader.graderFromConf([]) + graded = weightedGrader.grade(self.test_gradesheet) + self.assertAlmostEqual( graded['percent'], 0.5106547619047619 ) + self.assertEqual( len(graded['section_breakdown']), (12 + 1) + (7+1) + 1 ) + self.assertEqual( len(graded['grade_breakdown']), 3 ) + graded = emptyGrader.grade(self.test_gradesheet) + self.assertAlmostEqual( graded['percent'], 0.0 ) + self.assertEqual( len(graded['section_breakdown']), 0 ) + self.assertEqual( len(graded['grade_breakdown']), 0 ) + #Test that graders can also be used instead of lists of dictionaries + homeworkGrader = AssignmentFormatGrader("Homework", 12, 2) + homeworkGrader2 = CourseGrader.graderFromConf(homeworkGrader) + graded = homeworkGrader2.grade(self.test_gradesheet) + self.assertAlmostEqual( graded['percent'], 0.11 ) + self.assertEqual( len(graded['section_breakdown']), 12 + 1 ) + + #TODO: How do we test failure cases? The parser only logs an error when it can't parse something. Maybe it should throw exceptions?