Commit fe7d4aed by Bridger Maxwell

Put graders into their own file. Incorporated other feedback from pull request.

parent 16c8d008
...@@ -15,15 +15,15 @@ from courseware.course_settings import GRADER # This won't work. ...@@ -15,15 +15,15 @@ from courseware.course_settings import GRADER # This won't work.
""" """
import courseware
import imp import imp
import logging import logging
import sys import sys
import types import types
from django.conf import settings from django.conf import settings
from django.utils.functional import SimpleLazyObject
from courseware import global_course_settings from courseware import global_course_settings
from courseware import graders
_log = logging.getLogger("mitx.courseware") _log = logging.getLogger("mitx.courseware")
...@@ -50,4 +50,7 @@ class Settings(object): ...@@ -50,4 +50,7 @@ class Settings(object):
setting_value = getattr(mod, setting) setting_value = getattr(mod, setting)
setattr(self, setting, setting_value) setattr(self, setting, setting_value)
# Here is where we should parse any configurations, so that we can fail early
self.GRADER = graders.grader_from_conf(self.GRADER)
course_settings = Settings() course_settings = Settings()
\ No newline at end of file
GRADER = [ GRADER = [
{ {
'course_format' : "Homework", 'type' : "Homework",
'min_count' : 12, 'min_count' : 12,
'drop_count' : 2, 'drop_count' : 2,
'short_label' : "HW", 'short_label' : "HW",
'weight' : 0.15, 'weight' : 0.15,
}, },
{ {
'course_format' : "Lab", 'type' : "Lab",
'min_count' : 12, 'min_count' : 12,
'drop_count' : 2, 'drop_count' : 2,
'category' : "Labs", 'category' : "Labs",
'weight' : 0.15 'weight' : 0.15
}, },
{ {
'section_format' : "Examination", 'type' : "Midterm",
'section_name' : "Midterm Exam", 'name' : "Midterm Exam",
'short_label' : "Midterm", 'short_label' : "Midterm",
'weight' : 0.3, 'weight' : 0.3,
}, },
{ {
'section_format' : "Examination", 'type' : "Final",
'section_name' : "Final Exam", 'name' : "Final Exam",
'short_label' : "Final", 'short_label' : "Final",
'weight' : 0.4, 'weight' : 0.4,
} }
......
import logging
from django.conf import settings
from collections import namedtuple
log = logging.getLogger("mitx.courseware")
# This is a tuple for holding scores, either from problems or sections.
# Section either indicates the name of the problem or the name of the section
Score = namedtuple("Score", "earned possible graded section")
def grader_from_conf(conf):
"""
This creates a CourseGrader from a configuration (such as in course_settings.py).
The conf can simply be an instance of CourseGrader, in which case no work is done.
More commonly, the conf is a list of dictionaries. A WeightedSubsectionsGrader
with AssignmentFormatGrader's or SingleSectionGrader's as subsections will be
generated. Every dictionary should contain the parameters for making either a
AssignmentFormatGrader or SingleSectionGrader, in addition to a 'weight' key.
"""
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 '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, ValueError) as error:
errorString = "Unable to parse grader configuration:\n " + str(subgraderconf) + "\n Error was:\n " + str(error)
log.critical(errorString)
raise ValueError(errorString)
return WeightedSubsectionsGrader( subgraders )
class CourseGrader(object):
"""
A course grader takes the totaled scores for each graded section (that a student has
started) in the course. From these scores, the grader calculates an overall percentage
grade. The grader should also generate information about how that score was calculated,
to be displayed in graphs or charts.
A grader has one required method, grade(), which is passed a grade_sheet. The grade_sheet
contains scores for all graded section that the student has started. If a student has
a score of 0 for that section, it may be missing from the grade_sheet. The grade_sheet
is keyed by section format. Each value is a list of Score namedtuples for each section
that has the matching section format.
The grader outputs a dictionary with the following keys:
- percent: Contaisn a float value, which is the final percentage score for the student.
- section_breakdown: This is a list of dictionaries which provide details on sections
that were graded. These are used for display in a graph or chart. The format for a
section_breakdown dictionary is explained below.
- grade_breakdown: This is a list of dictionaries which provide details on the contributions
of the final percentage grade. This is a higher level breakdown, for when the grade is constructed
of a few very large sections (such as Homeworks, Labs, a Midterm, and a Final). The format for
a grade_breakdown is explained below. This section is optional.
A dictionary in the section_breakdown list has the following keys:
percent: A float percentage for the section.
label: A short string identifying the section. Preferably fixed-length. E.g. "HW 3".
detail: A string explanation of the score. E.g. "Homework 1 - Ohms Law - 83% (5/6)"
category: A string identifying the category. Items with the same category are grouped together
in the display (for example, by color).
prominent: A boolean value indicating that this section should be displayed as more prominent
than other items.
A dictionary in the grade_breakdown list has the following keys:
percent: A float percentage in the breakdown. All percents should add up to the final percentage.
detail: A string explanation of this breakdown. E.g. "Homework - 10% of a possible 15%"
category: A string identifying the category. Items with the same category are grouped together
in the display (for example, by color).
"""
def grade(self, grade_sheet):
raise NotImplementedError
class WeightedSubsectionsGrader(CourseGrader):
"""
This grader takes a list of tuples containing (grader, category_name, weight) and computes
a final grade by totalling the contribution of each sub grader and multiplying it by the
given weight. For example, the sections may be
[ (homeworkGrader, "Homework", 0.15), (labGrader, "Labs", 0.15), (midtermGrader, "Midterm", 0.30), (finalGrader, "Final", 0.40) ]
All items in section_breakdown for each subgrader will be combined. A grade_breakdown will be
composed using the score from each grader.
Note that the sum of the weights is not take into consideration. If the weights add up to
a value > 1, the student may end up with a percent > 100%. This allows for sections that
are extra credit.
"""
def __init__(self, sections):
self.sections = sections
def grade(self, grade_sheet):
total_percent = 0.0
section_breakdown = []
grade_breakdown = []
for subgrader, category, weight in self.sections:
subgrade_result = subgrader.grade(grade_sheet)
weightedPercent = subgrade_result['percent'] * weight
section_detail = "{0} = {1:.1%} of a possible {2:.0%}".format(category, weightedPercent, weight)
total_percent += weightedPercent
section_breakdown += subgrade_result['section_breakdown']
grade_breakdown.append( {'percent' : weightedPercent, 'detail' : section_detail, 'category' : category} )
return {'percent' : total_percent,
'section_breakdown' : section_breakdown,
'grade_breakdown' : grade_breakdown}
class SingleSectionGrader(CourseGrader):
"""
This grades a single section with the format 'type' and the name 'name'.
If the name is not appropriate for the short short_label or category, they each may
be specified individually.
"""
def __init__(self, type, name, short_label = None, category = None):
self.type = type
self.name = name
self.short_label = short_label or name
self.category = category or name
def grade(self, grade_sheet):
foundScore = None
if self.type in grade_sheet:
for score in grade_sheet[self.type]:
if score.section == self.name:
foundScore = score
break
if foundScore:
percent = foundScore.earned / float(foundScore.possible)
detail = "{name} - {percent:.0%} ({earned:.3n}/{possible:.3n})".format( name = self.name,
percent = percent,
earned = float(foundScore.earned),
possible = float(foundScore.possible))
else:
percent = 0.0
detail = "{name} - 0% (?/?)".format(name = self.name)
if settings.GENERATE_PROFILE_SCORES:
points_possible = random.randrange(50, 100)
points_earned = random.randrange(40, points_possible)
percent = points_earned / float(points_possible)
detail = "{name} - {percent:.0%} ({earned:.3n}/{possible:.3n})".format( name = self.name,
percent = percent,
earned = float(points_earned),
possible = float(points_possible))
breakdown = [{'percent': percent, 'label': self.short_label, 'detail': detail, 'category': self.category, 'prominent': True}]
return {'percent' : percent,
'section_breakdown' : breakdown,
#No grade_breakdown here
}
class AssignmentFormatGrader(CourseGrader):
"""
Grades all sections matching the format 'type' with an equal weight. A specified
number of lowest scores can be dropped from the calculation. The minimum number of
sections in this format must be specified (even if those sections haven't been
written yet).
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).
section_type is a string that is the type of a singular section. For example, for Labs it
would be "Lab". This defaults to be the same as category.
short_label is similar to section_type, but shorter. For example, for Homework it would be
"HW".
"""
def __init__(self, type, min_count, drop_count, category = None, section_type = None, short_label = None):
self.type = type
self.min_count = min_count
self.drop_count = drop_count
self.category = category or self.type
self.section_type = section_type or self.type
self.short_label = short_label or self.type
def grade(self, grade_sheet):
def totalWithDrops(breakdown, drop_count):
#create an array of tuples with (index, mark), sorted by mark['percent'] descending
sorted_breakdown = sorted( enumerate(breakdown), key=lambda x: -x[1]['percent'] )
# A list of the indices of the dropped scores
dropped_indices = []
if drop_count > 0:
dropped_indices = [x[0] for x in sorted_breakdown[-drop_count:]]
aggregate_score = 0
for index, mark in enumerate(breakdown):
if index not in dropped_indices:
aggregate_score += mark['percent']
if (len(breakdown) - drop_count > 0):
aggregate_score /= len(breakdown) - drop_count
return aggregate_score, dropped_indices
#Figure the homework scores
scores = grade_sheet.get(self.type, [])
breakdown = []
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,
section_type = self.section_type,
name = scores[i].section,
percent = percentage,
earned = float(scores[i].earned),
possible = float(scores[i].possible) )
else:
percentage = 0
summary = "{section_type} {index} Unreleased - 0% (?/?)".format(index = i+1, section_type = self.section_type)
if settings.GENERATE_PROFILE_SCORES:
points_possible = random.randrange(10, 50)
points_earned = random.randrange(5, points_possible)
percentage = points_earned / float(points_possible)
summary = "{section_type} {index} - {name} - {percent:.0%} ({earned:.3n}/{possible:.3n})".format(index = i+1,
section_type = self.section_type,
name = "Randomly Generated",
percent = percentage,
earned = float(points_earned),
possible = float(points_possible) )
short_label = "{short_label} {index:02d}".format(index = i+1, short_label = self.short_label)
breakdown.append( {'percent': percentage, 'label': short_label, 'detail': summary, 'category': self.category} )
total_percent, dropped_indices = totalWithDrops(breakdown, self.drop_count)
for dropped_index in dropped_indices:
breakdown[dropped_index]['mark'] = {'detail': "The lowest {drop_count} {section_type} scores are dropped.".format(drop_count = self.drop_count, section_type=self.section_type) }
total_detail = "{section_type} Average = {percent:.0%}".format(percent = total_percent, section_type = self.section_type)
total_label = "{short_label} Avg".format(short_label = self.short_label)
breakdown.append( {'percent': total_percent, 'label': total_label, 'detail': total_detail, 'category': self.category, 'prominent': True} )
return {'percent' : total_percent,
'section_breakdown' : breakdown,
#No grade_breakdown here
}
import courseware.content_parser as content_parser from lxml import etree
import courseware.modules
import logging
import random import random
import urllib
from collections import namedtuple
from courseware import course_settings
from django.conf import settings from django.conf import settings
from lxml import etree
from models import StudentModule
from student.models import UserProfile
log = logging.getLogger("mitx.courseware")
Score = namedtuple("Score", "earned possible graded section")
SectionPercentage = namedtuple("SectionPercentage", "percentage label summary")
class CourseGrader(object):
"""
A course grader takes the totaled scores for each graded section (that a student has
started) in the course. From these scores, the grader calculates an overall percentage
grade. The grader should also generate information about how that score was calculated,
to be displayed in graphs or charts.
A grader has one required method, grade(), which is passed a grade_sheet. The grade_sheet
contains scores for all graded section that the student has started. If a student has
a score of 0 for that section, it may be missing from the grade_sheet. The grade_sheet
is keyed by section format. Each value is a list of Score namedtuples for each section
that has the matching section format.
The grader outputs a dictionary with the following keys:
- percent: Contaisn a float value, which is the final percentage score for the student.
- section_breakdown: This is a list of dictionaries which provide details on sections
that were graded. These are used for display in a graph or chart. The format for a
section_breakdown dictionary is explained below.
- grade_breakdown: This is a list of dictionaries which provide details on the contributions
of the final percentage grade. This is a higher level breakdown, for when the grade is constructed
of a few very large sections (such as Homeworks, Labs, a Midterm, and a Final). The format for
a grade_breakdown is explained below. This section is optional.
A dictionary in the section_breakdown list has the following keys:
percent: A float percentage for the section.
label: A short string identifying the section. Preferably fixed-length. E.g. "HW 3".
detail: A string explanation of the score. E.g. "Homework 1 - Ohms Law - 83% (5/6)"
category: A string identifying the category. Items with the same category are grouped together
in the display (for example, by color).
prominent: A boolean value indicating that this section should be displayed as more prominent
than other items.
A dictionary in the grade_breakdown list has the following keys:
percent: A float percentage in the breakdown. All percents should add up to the final percentage.
detail: A string explanation of this breakdown. E.g. "Homework - 10% of a possible 15%"
category: A string identifying the category. Items with the same category are grouped together
in the display (for example, by color).
"""
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
a final grade by totalling the contribution of each sub grader and multiplying it by the
given weight. For example, the sections may be
[ (homeworkGrader, "Homework", 0.15), (labGrader, "Labs", 0.15), (midtermGrader, "Midterm", 0.30), (finalGrader, "Final", 0.40) ]
All items in section_breakdown for each subgrader will be combined. A grade_breakdown will be
composed using the score from each grader.
Note that the sum of the weights is not take into consideration. If the weights add up to
a value > 1, the student may end up with a percent > 100%. This allows for sections that
are extra credit.
"""
def __init__(self, sections):
self.sections = sections
def grade(self, grade_sheet):
total_percent = 0.0
section_breakdown = []
grade_breakdown = []
for subgrader, section_name, weight in self.sections:
subgrade_result = subgrader.grade(grade_sheet)
weightedPercent = subgrade_result['percent'] * weight
section_detail = "{0} = {1:.1%} of a possible {2:.0%}".format(section_name, weightedPercent, weight)
total_percent += weightedPercent
section_breakdown += subgrade_result['section_breakdown']
grade_breakdown.append( {'percent' : weightedPercent, 'detail' : section_detail, 'category' : section_name} )
return {'percent' : total_percent,
'section_breakdown' : section_breakdown,
'grade_breakdown' : grade_breakdown}
class SingleSectionGrader(CourseGrader):
"""
This grades a single section with the format section_format and the name section_name.
If the section_name is not appropriate for the short short_label or category, they each may
be specified individually.
"""
def __init__(self, section_format, section_name, short_label = None, category = None):
self.section_format = section_format
self.section_name = section_name
self.short_label = short_label or section_name
self.category = category or section_name
def grade(self, grade_sheet):
foundScore = None
if self.section_format in grade_sheet:
for score in grade_sheet[self.section_format]:
if score.section == self.section_name:
foundScore = score
break
if foundScore: from courseware import course_settings
percent = foundScore.earned / float(foundScore.possible) import courseware.content_parser as content_parser
detail = "{name} - {percent:.0%} ({earned:.3n}/{possible:.3n})".format( name = self.section_name, from courseware.graders import Score
percent = percent, import courseware.modules
earned = float(foundScore.earned), from models import StudentModule
possible = float(foundScore.possible))
else:
percent = 0.0
detail = "{name} - 0% (?/?)".format(name = self.section_name)
if settings.GENERATE_PROFILE_SCORES:
points_possible = random.randrange(50, 100)
points_earned = random.randrange(40, points_possible)
percent = points_earned / float(points_possible)
detail = "{name} - {percent:.0%} ({earned:.3n}/{possible:.3n})".format( name = self.section_name,
percent = percent,
earned = float(points_earned),
possible = float(points_possible))
breakdown = [{'percent': percent, 'label': self.short_label, 'detail': detail, 'category': self.category, 'prominent': True}]
return {'percent' : percent,
'section_breakdown' : breakdown,
#No grade_breakdown here
}
class AssignmentFormatGrader(CourseGrader):
"""
Grades all sections specified in course_format with an equal weight. A specified
number of lowest scores can be dropped from the calculation. The minimum number of
sections in this format must be specified (even if those sections haven't been
written yet).
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).
section_type is a string that is the type of a singular section. For example, for Labs it
would be "Lab". This defaults to be the same as category.
short_label is similar to section_type, but shorter. For example, for Homework it would be
"HW".
"""
def __init__(self, course_format, min_count, drop_count, category = None, section_type = None, short_label = None):
self.course_format = course_format
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
self.short_label = short_label or self.course_format
def grade(self, grade_sheet):
def totalWithDrops(breakdown, drop_count):
#create an array of tuples with (index, mark), sorted by mark['percent'] descending
sorted_breakdown = sorted( enumerate(breakdown), key=lambda x: -x[1]['percent'] )
# A list of the indices of the dropped scores
dropped_indices = []
if drop_count > 0:
dropped_indices = [x[0] for x in sorted_breakdown[-drop_count:]]
aggregate_score = 0
for index, mark in enumerate(breakdown):
if index not in dropped_indices:
aggregate_score += mark['percent']
if (len(breakdown) - drop_count > 0):
aggregate_score /= len(breakdown) - drop_count
return aggregate_score, dropped_indices
#Figure the homework scores
scores = grade_sheet.get(self.course_format, [])
breakdown = []
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,
section_type = self.section_type,
name = scores[i].section,
percent = percentage,
earned = float(scores[i].earned),
possible = float(scores[i].possible) )
else:
percentage = 0
summary = "{section_type} {index} Unreleased - 0% (?/?)".format(index = i+1, section_type = self.section_type)
if settings.GENERATE_PROFILE_SCORES:
points_possible = random.randrange(10, 50)
points_earned = random.randrange(5, points_possible)
percentage = points_earned / float(points_possible)
summary = "{section_type} {index} - {name} - {percent:.0%} ({earned:.3n}/{possible:.3n})".format(index = i+1,
section_type = self.section_type,
name = "Randomly Generated",
percent = percentage,
earned = float(points_earned),
possible = float(points_possible) )
short_label = "{short_label} {index:02d}".format(index = i+1, short_label = self.short_label)
breakdown.append( {'percent': percentage, 'label': short_label, 'detail': summary, 'category': self.category} )
total_percent, dropped_indices = totalWithDrops(breakdown, self.drop_count)
for dropped_index in dropped_indices:
breakdown[dropped_index]['mark'] = {'detail': "The lowest {drop_count} {section_type} scores are dropped.".format(drop_count = self.drop_count, section_type=self.section_type) }
total_detail = "{section_type} Average = {percent:.0%}".format(percent = total_percent, section_type = self.section_type)
total_label = "{short_label} Avg".format(short_label = self.short_label)
breakdown.append( {'percent': total_percent, 'label': total_label, 'detail': total_detail, 'category': self.category, 'prominent': True} )
return {'percent' : total_percent,
'section_breakdown' : breakdown,
#No grade_breakdown here
}
def grade_sheet(student): def grade_sheet(student):
""" """
...@@ -343,8 +82,7 @@ def grade_sheet(student): ...@@ -343,8 +82,7 @@ def grade_sheet(student):
'sections' : sections,}) 'sections' : sections,})
grader = CourseGrader.graderFromConf(course_settings.GRADER) grader = course_settings.GRADER
#TODO: We should cache this grader object
grade_summary = grader.grade(totaled_scores) grade_summary = grader.grade(totaled_scores)
return {'courseware_summary' : chapters, return {'courseware_summary' : chapters,
......
...@@ -4,7 +4,9 @@ import numpy ...@@ -4,7 +4,9 @@ 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, CourseGrader, WeightedSubsectionsGrader, SingleSectionGrader, AssignmentFormatGrader import courseware.graders as graders
from courseware.graders import Score, CourseGrader, WeightedSubsectionsGrader, SingleSectionGrader, AssignmentFormatGrader
from courseware.grades import aggregate_scores
class ModelsTest(unittest.TestCase): class ModelsTest(unittest.TestCase):
def setUp(self): def setUp(self):
...@@ -107,9 +109,9 @@ class GraderTest(unittest.TestCase): ...@@ -107,9 +109,9 @@ class GraderTest(unittest.TestCase):
} }
def test_SingleSectionGrader(self): def test_SingleSectionGrader(self):
midtermGrader = SingleSectionGrader("Midterm", "Midterm Exam") midtermGrader = graders.SingleSectionGrader("Midterm", "Midterm Exam")
lab4Grader = SingleSectionGrader("Lab", "lab4") lab4Grader = graders.SingleSectionGrader("Lab", "lab4")
badLabGrader = SingleSectionGrader("Lab", "lab42") badLabGrader = graders.SingleSectionGrader("Lab", "lab42")
for graded in [midtermGrader.grade(self.empty_gradesheet), for graded in [midtermGrader.grade(self.empty_gradesheet),
midtermGrader.grade(self.incomplete_gradesheet), midtermGrader.grade(self.incomplete_gradesheet),
...@@ -125,12 +127,12 @@ class GraderTest(unittest.TestCase): ...@@ -125,12 +127,12 @@ 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): def test_AssignmentFormatGrader(self):
homeworkGrader = AssignmentFormatGrader("Homework", 12, 2) homeworkGrader = graders.AssignmentFormatGrader("Homework", 12, 2)
noDropGrader = AssignmentFormatGrader("Homework", 12, 0) noDropGrader = graders.AssignmentFormatGrader("Homework", 12, 0)
#Even though the minimum number is 3, this should grade correctly when 7 assignments are found #Even though the minimum number is 3, this should grade correctly when 7 assignments are found
overflowGrader = AssignmentFormatGrader("Lab", 3, 2) overflowGrader = graders.AssignmentFormatGrader("Lab", 3, 2)
labGrader = AssignmentFormatGrader("Lab", 7, 3) labGrader = graders.AssignmentFormatGrader("Lab", 7, 3)
#Test the grading of an empty gradesheet #Test the grading of an empty gradesheet
...@@ -162,25 +164,25 @@ class GraderTest(unittest.TestCase): ...@@ -162,25 +164,25 @@ class GraderTest(unittest.TestCase):
def test_WeightedSubsectionsGrader(self): def test_WeightedSubsectionsGrader(self):
#First, a few sub graders #First, a few sub graders
homeworkGrader = AssignmentFormatGrader("Homework", 12, 2) homeworkGrader = graders.AssignmentFormatGrader("Homework", 12, 2)
labGrader = AssignmentFormatGrader("Lab", 7, 3) labGrader = graders.AssignmentFormatGrader("Lab", 7, 3)
midtermGrader = SingleSectionGrader("Midterm", "Midterm Exam") midtermGrader = graders.SingleSectionGrader("Midterm", "Midterm Exam")
weightedGrader = WeightedSubsectionsGrader( [(homeworkGrader, homeworkGrader.category, 0.25), (labGrader, labGrader.category, 0.25), weightedGrader = graders.WeightedSubsectionsGrader( [(homeworkGrader, homeworkGrader.category, 0.25), (labGrader, labGrader.category, 0.25),
(midtermGrader, midtermGrader.category, 0.5)] ) (midtermGrader, midtermGrader.category, 0.5)] )
overOneWeightsGrader = WeightedSubsectionsGrader( [(homeworkGrader, homeworkGrader.category, 0.5), (labGrader, labGrader.category, 0.5), overOneWeightsGrader = graders.WeightedSubsectionsGrader( [(homeworkGrader, homeworkGrader.category, 0.5), (labGrader, labGrader.category, 0.5),
(midtermGrader, midtermGrader.category, 0.5)] ) (midtermGrader, midtermGrader.category, 0.5)] )
#The midterm should have all weight on this one #The midterm should have all weight on this one
zeroWeightsGrader = WeightedSubsectionsGrader( [(homeworkGrader, homeworkGrader.category, 0.0), (labGrader, labGrader.category, 0.0), zeroWeightsGrader = graders.WeightedSubsectionsGrader( [(homeworkGrader, homeworkGrader.category, 0.0), (labGrader, labGrader.category, 0.0),
(midtermGrader, midtermGrader.category, 0.5)] ) (midtermGrader, midtermGrader.category, 0.5)] )
#This should always have a final percent of zero #This should always have a final percent of zero
allZeroWeightsGrader = WeightedSubsectionsGrader( [(homeworkGrader, homeworkGrader.category, 0.0), (labGrader, labGrader.category, 0.0), allZeroWeightsGrader = graders.WeightedSubsectionsGrader( [(homeworkGrader, homeworkGrader.category, 0.0), (labGrader, labGrader.category, 0.0),
(midtermGrader, midtermGrader.category, 0.0)] ) (midtermGrader, midtermGrader.category, 0.0)] )
emptyGrader = WeightedSubsectionsGrader( [] ) emptyGrader = graders.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 )
...@@ -221,33 +223,33 @@ class GraderTest(unittest.TestCase): ...@@ -221,33 +223,33 @@ class GraderTest(unittest.TestCase):
def test_graderFromConf(self): def test_graderFromConf(self):
#Confs always produce a WeightedSubsectionsGrader, so we test this by repeating the test #Confs always produce a graders.WeightedSubsectionsGrader, so we test this by repeating the test
#in test_WeightedSubsectionsGrader, but generate the graders with confs. #in test_graders.WeightedSubsectionsGrader, but generate the graders with confs.
weightedGrader = CourseGrader.graderFromConf([ weightedGrader = graders.grader_from_conf([
{ {
'course_format' : "Homework", 'type' : "Homework",
'min_count' : 12, 'min_count' : 12,
'drop_count' : 2, 'drop_count' : 2,
'short_label' : "HW", 'short_label' : "HW",
'weight' : 0.25, 'weight' : 0.25,
}, },
{ {
'course_format' : "Lab", 'type' : "Lab",
'min_count' : 7, 'min_count' : 7,
'drop_count' : 3, 'drop_count' : 3,
'category' : "Labs", 'category' : "Labs",
'weight' : 0.25 'weight' : 0.25
}, },
{ {
'section_format' : "Midterm", 'type' : "Midterm",
'section_name' : "Midterm Exam", 'name' : "Midterm Exam",
'short_label' : "Midterm", 'short_label' : "Midterm",
'weight' : 0.5, 'weight' : 0.5,
}, },
]) ])
emptyGrader = CourseGrader.graderFromConf([]) emptyGrader = graders.grader_from_conf([])
graded = weightedGrader.grade(self.test_gradesheet) graded = weightedGrader.grade(self.test_gradesheet)
self.assertAlmostEqual( graded['percent'], 0.5106547619047619 ) self.assertAlmostEqual( graded['percent'], 0.5106547619047619 )
...@@ -260,8 +262,8 @@ class GraderTest(unittest.TestCase): ...@@ -260,8 +262,8 @@ class GraderTest(unittest.TestCase):
self.assertEqual( len(graded['grade_breakdown']), 0 ) self.assertEqual( len(graded['grade_breakdown']), 0 )
#Test that graders can also be used instead of lists of dictionaries #Test that graders can also be used instead of lists of dictionaries
homeworkGrader = AssignmentFormatGrader("Homework", 12, 2) homeworkGrader = graders.AssignmentFormatGrader("Homework", 12, 2)
homeworkGrader2 = CourseGrader.graderFromConf(homeworkGrader) homeworkGrader2 = graders.grader_from_conf(homeworkGrader)
graded = homeworkGrader2.grade(self.test_gradesheet) graded = homeworkGrader2.grade(self.test_gradesheet)
self.assertAlmostEqual( graded['percent'], 0.11 ) self.assertAlmostEqual( graded['percent'], 0.11 )
......
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