Commit 3860f333 by Bridger Maxwell

Added parser for creating graders from a dictionary representation.

parent 795fba04
...@@ -59,6 +59,36 @@ class CourseGrader(object): ...@@ -59,6 +59,36 @@ class CourseGrader(object):
def grade(self, grade_sheet): def grade(self, grade_sheet):
raise NotImplementedError 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): class WeightedSubsectionsGrader(CourseGrader):
""" """
This grader takes a list of tuples containing (grader, category_name, weight) and computes This grader takes a list of tuples containing (grader, category_name, weight) and computes
...@@ -153,9 +183,9 @@ class AssignmentFormatGrader(CourseGrader): ...@@ -153,9 +183,9 @@ class AssignmentFormatGrader(CourseGrader):
sections in this format must be specified (even if those sections haven't been sections in this format must be specified (even if those sections haven't been
written yet). written yet).
min_number defines how many assignments are expected throughout the course. Placeholder 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_number. 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_number, min_number will be ignored. 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 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). displayed, scores from the same category will be similar (for example, by color).
...@@ -167,9 +197,9 @@ class AssignmentFormatGrader(CourseGrader): ...@@ -167,9 +197,9 @@ class AssignmentFormatGrader(CourseGrader):
"HW". "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.course_format = course_format
self.min_number = min_number self.min_count = min_count
self.drop_count = drop_count self.drop_count = drop_count
self.category = category or self.course_format self.category = category or self.course_format
self.section_type = section_type or self.course_format self.section_type = section_type or self.course_format
...@@ -196,7 +226,7 @@ class AssignmentFormatGrader(CourseGrader): ...@@ -196,7 +226,7 @@ class AssignmentFormatGrader(CourseGrader):
#Figure the homework scores #Figure the homework scores
scores = grade_sheet.get(self.course_format, []) scores = grade_sheet.get(self.course_format, [])
breakdown = [] 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): if i < len(scores):
percentage = scores[i].earned / float(scores[i].possible) percentage = scores[i].earned / float(scores[i].possible)
summary = "{section_type} {index} - {name} - {percent:.0%} ({earned:.3n}/{possible:.3n})".format(index = i+1, summary = "{section_type} {index} - {name} - {percent:.0%} ({earned:.3n}/{possible:.3n})".format(index = i+1,
......
...@@ -4,7 +4,7 @@ import numpy ...@@ -4,7 +4,7 @@ import numpy
import courseware.modules import courseware.modules
import courseware.capa.calc as calc 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): class ModelsTest(unittest.TestCase):
def setUp(self): def setUp(self):
...@@ -140,6 +140,41 @@ class GraderTest(unittest.TestCase): ...@@ -140,6 +140,41 @@ class GraderTest(unittest.TestCase):
self.assertAlmostEqual( graded['percent'], 0.2 ) self.assertAlmostEqual( graded['percent'], 0.2 )
self.assertEqual( len(graded['section_breakdown']), 1 ) self.assertEqual( len(graded['section_breakdown']), 1 )
def test_assignmentFormatGrader(self):
homeworkGrader = AssignmentFormatGrader("Homework", 12, 2)
noDropGrader = AssignmentFormatGrader("Homework", 12, 0)
#Even though the minimum number is 3, this should grade correctly when 7 assignments are found
overflowGrader = AssignmentFormatGrader("Lab", 3, 2)
labGrader = AssignmentFormatGrader("Lab", 7, 3)
#Test the grading of an empty gradesheet
for graded in [ homeworkGrader.grade(self.empty_gradesheet),
noDropGrader.grade(self.empty_gradesheet),
homeworkGrader.grade(self.incomplete_gradesheet),
noDropGrader.grade(self.incomplete_gradesheet) ]:
self.assertAlmostEqual( graded['percent'], 0.0 )
#Make sure the breakdown includes 12 sections, plus one summary
self.assertEqual( len(graded['section_breakdown']), 12 + 1 )
graded = homeworkGrader.grade(self.test_gradesheet)
self.assertAlmostEqual( graded['percent'], 0.11 ) # 100% + 10% / 10 assignments
self.assertEqual( len(graded['section_breakdown']), 12 + 1 )
graded = noDropGrader.grade(self.test_gradesheet)
self.assertAlmostEqual( graded['percent'], 0.0916666666666666 ) # 100% + 10% / 12 assignments
self.assertEqual( len(graded['section_breakdown']), 12 + 1 )
graded = overflowGrader.grade(self.test_gradesheet)
self.assertAlmostEqual( graded['percent'], 0.8880952380952382 ) # 100% + 10% / 5 assignments
self.assertEqual( len(graded['section_breakdown']), 7 + 1 )
graded = labGrader.grade(self.test_gradesheet)
self.assertAlmostEqual( graded['percent'], 0.9226190476190477 )
self.assertEqual( len(graded['section_breakdown']), 7 + 1 )
def test_WeightedSubsectionsGrader(self): def test_WeightedSubsectionsGrader(self):
#First, a few sub graders #First, a few sub graders
homeworkGrader = AssignmentFormatGrader("Homework", 12, 2) homeworkGrader = AssignmentFormatGrader("Homework", 12, 2)
...@@ -160,6 +195,7 @@ class GraderTest(unittest.TestCase): ...@@ -160,6 +195,7 @@ class GraderTest(unittest.TestCase):
allZeroWeightsGrader = WeightedSubsectionsGrader( [(homeworkGrader, homeworkGrader.category, 0.0), (labGrader, labGrader.category, 0.0), allZeroWeightsGrader = WeightedSubsectionsGrader( [(homeworkGrader, homeworkGrader.category, 0.0), (labGrader, labGrader.category, 0.0),
(midtermGrader, midtermGrader.category, 0.0)] ) (midtermGrader, midtermGrader.category, 0.0)] )
emptyGrader = WeightedSubsectionsGrader( [] )
graded = weightedGrader.grade(self.test_gradesheet) graded = weightedGrader.grade(self.test_gradesheet)
self.assertAlmostEqual( graded['percent'], 0.5106547619047619 ) self.assertAlmostEqual( graded['percent'], 0.5106547619047619 )
...@@ -187,53 +223,64 @@ class GraderTest(unittest.TestCase): ...@@ -187,53 +223,64 @@ class GraderTest(unittest.TestCase):
zeroWeightsGrader.grade(self.empty_gradesheet), zeroWeightsGrader.grade(self.empty_gradesheet),
allZeroWeightsGrader.grade(self.empty_gradesheet)]: allZeroWeightsGrader.grade(self.empty_gradesheet)]:
self.assertAlmostEqual( graded['percent'], 0.0 ) 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['section_breakdown']), (12 + 1) + (7+1) + 1 )
self.assertEqual( len(graded['grade_breakdown']), 3 ) 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_assignmentFormatGrader(self):
homeworkGrader = AssignmentFormatGrader("Homework", 12, 2)
noDropGrader = AssignmentFormatGrader("Homework", 12, 0)
#Even though the minimum number is 3, this should grade correctly when 7 assignments are found
overflowGrader = AssignmentFormatGrader("Lab", 3, 2)
labGrader = AssignmentFormatGrader("Lab", 7, 3)
#Test the grading of an empty gradesheet
for graded in [ homeworkGrader.grade(self.empty_gradesheet),
noDropGrader.grade(self.empty_gradesheet),
homeworkGrader.grade(self.incomplete_gradesheet),
noDropGrader.grade(self.incomplete_gradesheet) ]:
self.assertAlmostEqual( graded['percent'], 0.0 )
#Make sure the breakdown includes 12 sections, plus one summary
self.assertEqual( len(graded['section_breakdown']), 12 + 1 )
graded = homeworkGrader.grade(self.test_gradesheet)
self.assertAlmostEqual( graded['percent'], 0.11 ) # 100% + 10% / 10 assignments
self.assertEqual( len(graded['section_breakdown']), 12 + 1 )
graded = noDropGrader.grade(self.test_gradesheet)
self.assertAlmostEqual( graded['percent'], 0.0916666666666666 ) # 100% + 10% / 12 assignments
self.assertEqual( len(graded['section_breakdown']), 12 + 1 )
graded = overflowGrader.grade(self.test_gradesheet)
self.assertAlmostEqual( graded['percent'], 0.8880952380952382 ) # 100% + 10% / 5 assignments
self.assertEqual( len(graded['section_breakdown']), 7 + 1 )
graded = labGrader.grade(self.test_gradesheet)
self.assertAlmostEqual( graded['percent'], 0.9226190476190477 )
self.assertEqual( len(graded['section_breakdown']), 7 + 1 )
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?
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