diff --git a/djangoapps/courseware/__init__.py b/djangoapps/courseware/__init__.py index b8782ab30c..85e7242e3d 100644 --- a/djangoapps/courseware/__init__.py +++ b/djangoapps/courseware/__init__.py @@ -15,15 +15,15 @@ from courseware.course_settings import GRADER # This won't work. """ -import courseware import imp import logging import sys import types from django.conf import settings -from django.utils.functional import SimpleLazyObject + from courseware import global_course_settings +from courseware import graders _log = logging.getLogger("mitx.courseware") @@ -49,5 +49,8 @@ class Settings(object): if setting == setting.upper(): setting_value = getattr(mod, setting) 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() \ No newline at end of file diff --git a/djangoapps/courseware/global_course_settings.py b/djangoapps/courseware/global_course_settings.py index b75ff6ee78..f4e9696d1d 100644 --- a/djangoapps/courseware/global_course_settings.py +++ b/djangoapps/courseware/global_course_settings.py @@ -1,27 +1,27 @@ GRADER = [ { - 'course_format' : "Homework", + 'type' : "Homework", 'min_count' : 12, 'drop_count' : 2, 'short_label' : "HW", 'weight' : 0.15, }, { - 'course_format' : "Lab", + 'type' : "Lab", 'min_count' : 12, 'drop_count' : 2, 'category' : "Labs", 'weight' : 0.15 }, { - 'section_format' : "Examination", - 'section_name' : "Midterm Exam", + 'type' : "Midterm", + 'name' : "Midterm Exam", 'short_label' : "Midterm", 'weight' : 0.3, }, { - 'section_format' : "Examination", - 'section_name' : "Final Exam", + 'type' : "Final", + 'name' : "Final Exam", 'short_label' : "Final", 'weight' : 0.4, } diff --git a/djangoapps/courseware/graders.py b/djangoapps/courseware/graders.py new file mode 100644 index 0000000000..e195dd3f5b --- /dev/null +++ b/djangoapps/courseware/graders.py @@ -0,0 +1,271 @@ +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 + } diff --git a/djangoapps/courseware/grades.py b/djangoapps/courseware/grades.py index b50ef15669..8b4cb22c2f 100644 --- a/djangoapps/courseware/grades.py +++ b/djangoapps/courseware/grades.py @@ -1,274 +1,13 @@ -import courseware.content_parser as content_parser -import courseware.modules -import logging -import random -import urllib - -from collections import namedtuple -from courseware import course_settings -from django.conf import settings from lxml import etree +import random + +from django.conf import settings + +from courseware import course_settings +import courseware.content_parser as content_parser +from courseware.graders import Score +import courseware.modules 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: - percent = foundScore.earned / float(foundScore.possible) - detail = "{name} - {percent:.0%} ({earned:.3n}/{possible:.3n})".format( name = self.section_name, - percent = percent, - earned = float(foundScore.earned), - 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): """ @@ -343,8 +82,7 @@ def grade_sheet(student): 'sections' : sections,}) - grader = CourseGrader.graderFromConf(course_settings.GRADER) - #TODO: We should cache this grader object + grader = course_settings.GRADER grade_summary = grader.grade(totaled_scores) return {'courseware_summary' : chapters, diff --git a/djangoapps/courseware/tests.py b/djangoapps/courseware/tests.py index 529d7bb473..db07e36e04 100644 --- a/djangoapps/courseware/tests.py +++ b/djangoapps/courseware/tests.py @@ -4,7 +4,9 @@ import numpy import courseware.modules 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): def setUp(self): @@ -107,9 +109,9 @@ class GraderTest(unittest.TestCase): } def test_SingleSectionGrader(self): - midtermGrader = SingleSectionGrader("Midterm", "Midterm Exam") - lab4Grader = SingleSectionGrader("Lab", "lab4") - badLabGrader = SingleSectionGrader("Lab", "lab42") + midtermGrader = graders.SingleSectionGrader("Midterm", "Midterm Exam") + lab4Grader = graders.SingleSectionGrader("Lab", "lab4") + badLabGrader = graders.SingleSectionGrader("Lab", "lab42") for graded in [midtermGrader.grade(self.empty_gradesheet), midtermGrader.grade(self.incomplete_gradesheet), @@ -125,12 +127,12 @@ class GraderTest(unittest.TestCase): self.assertAlmostEqual( graded['percent'], 0.2 ) self.assertEqual( len(graded['section_breakdown']), 1 ) - def test_assignmentFormatGrader(self): - homeworkGrader = AssignmentFormatGrader("Homework", 12, 2) - noDropGrader = AssignmentFormatGrader("Homework", 12, 0) + def test_AssignmentFormatGrader(self): + homeworkGrader = graders.AssignmentFormatGrader("Homework", 12, 2) + noDropGrader = graders.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) + overflowGrader = graders.AssignmentFormatGrader("Lab", 3, 2) + labGrader = graders.AssignmentFormatGrader("Lab", 7, 3) #Test the grading of an empty gradesheet @@ -162,25 +164,25 @@ class GraderTest(unittest.TestCase): def test_WeightedSubsectionsGrader(self): #First, a few sub graders - homeworkGrader = AssignmentFormatGrader("Homework", 12, 2) - labGrader = AssignmentFormatGrader("Lab", 7, 3) - midtermGrader = SingleSectionGrader("Midterm", "Midterm Exam") + homeworkGrader = graders.AssignmentFormatGrader("Homework", 12, 2) + labGrader = graders.AssignmentFormatGrader("Lab", 7, 3) + 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)] ) - 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)] ) #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)] ) #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)] ) - emptyGrader = WeightedSubsectionsGrader( [] ) + emptyGrader = graders.WeightedSubsectionsGrader( [] ) graded = weightedGrader.grade(self.test_gradesheet) self.assertAlmostEqual( graded['percent'], 0.5106547619047619 ) @@ -221,33 +223,33 @@ class GraderTest(unittest.TestCase): 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. + #Confs always produce a graders.WeightedSubsectionsGrader, so we test this by repeating the test + #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, 'drop_count' : 2, 'short_label' : "HW", 'weight' : 0.25, }, { - 'course_format' : "Lab", + 'type' : "Lab", 'min_count' : 7, 'drop_count' : 3, 'category' : "Labs", 'weight' : 0.25 }, { - 'section_format' : "Midterm", - 'section_name' : "Midterm Exam", + 'type' : "Midterm", + 'name' : "Midterm Exam", 'short_label' : "Midterm", 'weight' : 0.5, }, ]) - emptyGrader = CourseGrader.graderFromConf([]) + emptyGrader = graders.grader_from_conf([]) graded = weightedGrader.grade(self.test_gradesheet) self.assertAlmostEqual( graded['percent'], 0.5106547619047619 ) @@ -260,8 +262,8 @@ class GraderTest(unittest.TestCase): 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) + homeworkGrader = graders.AssignmentFormatGrader("Homework", 12, 2) + homeworkGrader2 = graders.grader_from_conf(homeworkGrader) graded = homeworkGrader2.grade(self.test_gradesheet) self.assertAlmostEqual( graded['percent'], 0.11 )