Merge pull request #14032 from edx/beryl/unattempted-grade-report
Update grade report to distinguish between Not Attempted and 0
This commit is contained in:
@@ -404,7 +404,7 @@ def _cert_info(user, course_overview, cert_status, course_mode): # pylint: disa
|
||||
)
|
||||
|
||||
if status in {'generating', 'ready', 'notpassing', 'restricted', 'auditing', 'unverified'}:
|
||||
persisted_grade = CourseGradeFactory(user).get_persisted(course_overview)
|
||||
persisted_grade = CourseGradeFactory().get_persisted(user, course_overview)
|
||||
if persisted_grade is not None:
|
||||
status_dict['grade'] = unicode(persisted_grade.percent)
|
||||
elif 'grade' in cert_status:
|
||||
|
||||
@@ -10,7 +10,7 @@ class CorrectMap(object):
|
||||
in a capa problem. The response evaluation result for each answer_id includes
|
||||
(correctness, npoints, msg, hint, hintmode).
|
||||
|
||||
- correctness : 'correct', 'incorrect', or 'partially-correct'
|
||||
- correctness : 'correct', 'incorrect', 'partially-correct', or 'incomplete'
|
||||
- npoints : None, or integer specifying number of points awarded for this answer_id
|
||||
- msg : string (may have HTML) giving extra message response
|
||||
(displayed below textline or textbox)
|
||||
|
||||
@@ -5,6 +5,7 @@ Code used to calculate learner grades.
|
||||
from __future__ import division
|
||||
|
||||
import abc
|
||||
from collections import OrderedDict
|
||||
import inspect
|
||||
import logging
|
||||
import random
|
||||
@@ -18,17 +19,13 @@ class ScoreBase(object):
|
||||
"""
|
||||
Abstract base class for encapsulating fields of values scores.
|
||||
Field common to all scores include:
|
||||
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, attempted):
|
||||
def __init__(self, graded, attempted):
|
||||
self.graded = graded
|
||||
self.display_name = display_name
|
||||
self.module_id = module_id
|
||||
self.attempted = attempted
|
||||
|
||||
def __eq__(self, other):
|
||||
@@ -55,10 +52,10 @@ class ProblemScore(ScoreBase):
|
||||
"""
|
||||
def __init__(self, raw_earned, raw_possible, weighted_earned, weighted_possible, weight, *args, **kwargs):
|
||||
super(ProblemScore, self).__init__(*args, **kwargs)
|
||||
self.raw_earned = raw_earned
|
||||
self.raw_possible = raw_possible
|
||||
self.earned = weighted_earned
|
||||
self.possible = weighted_possible
|
||||
self.raw_earned = float(raw_earned) if raw_earned is not None else None
|
||||
self.raw_possible = float(raw_possible) if raw_possible is not None else None
|
||||
self.earned = float(weighted_earned) if weighted_earned is not None else None
|
||||
self.possible = float(weighted_possible) if weighted_possible is not None else None
|
||||
self.weight = weight
|
||||
|
||||
|
||||
@@ -71,8 +68,8 @@ class AggregatedScore(ScoreBase):
|
||||
"""
|
||||
def __init__(self, tw_earned, tw_possible, *args, **kwargs):
|
||||
super(AggregatedScore, self).__init__(*args, **kwargs)
|
||||
self.earned = tw_earned
|
||||
self.possible = tw_possible
|
||||
self.earned = float(tw_earned) if tw_earned is not None else None
|
||||
self.possible = float(tw_possible) if tw_possible is not None else None
|
||||
|
||||
|
||||
def float_sum(iterable):
|
||||
@@ -82,11 +79,9 @@ def float_sum(iterable):
|
||||
return float(sum(iterable))
|
||||
|
||||
|
||||
def aggregate_scores(scores, display_name="summary", location=None):
|
||||
def aggregate_scores(scores):
|
||||
"""
|
||||
scores: A list of ScoreBase objects
|
||||
display_name: The display name for the score object
|
||||
location: The location under which all objects in scores are located
|
||||
returns: A tuple (all_total, graded_total).
|
||||
all_total: A ScoreBase representing the total score summed over all input scores
|
||||
graded_total: A ScoreBase representing the score summed over all graded input scores
|
||||
@@ -100,11 +95,11 @@ def aggregate_scores(scores, display_name="summary", location=None):
|
||||
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, any_attempted)
|
||||
all_total = AggregatedScore(total_correct, total_possible, False, any_attempted)
|
||||
|
||||
# selecting only graded things
|
||||
graded_total = AggregatedScore(
|
||||
total_correct_graded, total_possible_graded, True, display_name, location, any_attempted_graded,
|
||||
total_correct_graded, total_possible_graded, True, any_attempted_graded,
|
||||
)
|
||||
|
||||
return all_total, graded_total
|
||||
@@ -126,9 +121,8 @@ 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.
|
||||
with AssignmentFormatGraders will be generated. Every dictionary should contain
|
||||
the parameters for making an AssignmentFormatGrader, in addition to a 'weight' key.
|
||||
"""
|
||||
if isinstance(conf, CourseGrader):
|
||||
return conf
|
||||
@@ -137,27 +131,14 @@ def grader_from_conf(conf):
|
||||
for subgraderconf in conf:
|
||||
subgraderconf = subgraderconf.copy()
|
||||
weight = subgraderconf.pop("weight", 0)
|
||||
# NOTE: 'name' used to exist in SingleSectionGrader. We are deprecating SingleSectionGrader
|
||||
# and converting everything into an AssignmentFormatGrader by adding 'min_count' and
|
||||
# 'drop_count'. AssignmentFormatGrader does not expect 'name', so if it appears
|
||||
# in bad_args, go ahead remove it (this causes no errors). Eventually, SingleSectionGrader
|
||||
# should be completely removed.
|
||||
name = 'name'
|
||||
try:
|
||||
if 'min_count' in subgraderconf:
|
||||
#This is an AssignmentFormatGrader
|
||||
subgrader_class = AssignmentFormatGrader
|
||||
elif name in subgraderconf:
|
||||
#This is an SingleSectionGrader
|
||||
subgrader_class = SingleSectionGrader
|
||||
else:
|
||||
raise ValueError("Configuration has no appropriate grader class.")
|
||||
|
||||
bad_args = invalid_args(subgrader_class.__init__, subgraderconf)
|
||||
# See note above concerning 'name'.
|
||||
if bad_args.issuperset({name}):
|
||||
bad_args = bad_args - {name}
|
||||
del subgraderconf[name]
|
||||
if len(bad_args) > 0:
|
||||
log.warning("Invalid arguments for a subgrader: %s", bad_args)
|
||||
for key in bad_args:
|
||||
@@ -194,10 +175,10 @@ class CourseGrader(object):
|
||||
- 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.
|
||||
- grade_breakdown: This is a dict of dictionaries, keyed by category, 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.
|
||||
@@ -208,7 +189,7 @@ class CourseGrader(object):
|
||||
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:
|
||||
A dictionary in the grade_breakdown dict 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
|
||||
@@ -241,77 +222,32 @@ class WeightedSubsectionsGrader(CourseGrader):
|
||||
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 __init__(self, subgraders):
|
||||
self.subgraders = subgraders
|
||||
|
||||
def grade(self, grade_sheet, generate_random_scores=False):
|
||||
total_percent = 0.0
|
||||
section_breakdown = []
|
||||
grade_breakdown = []
|
||||
grade_breakdown = OrderedDict()
|
||||
|
||||
for subgrader, category, weight in self.sections:
|
||||
for subgrader, assignment_type, weight in self.subgraders:
|
||||
subgrade_result = subgrader.grade(grade_sheet, generate_random_scores)
|
||||
|
||||
weighted_percent = subgrade_result['percent'] * weight
|
||||
section_detail = u"{0} = {1:.2%} of a possible {2:.2%}".format(category, weighted_percent, weight)
|
||||
section_detail = u"{0} = {1:.2%} of a possible {2:.2%}".format(assignment_type, weighted_percent, weight)
|
||||
|
||||
total_percent += weighted_percent
|
||||
section_breakdown += subgrade_result['section_breakdown']
|
||||
grade_breakdown.append({'percent': weighted_percent, '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): # pylint: disable=redefined-builtin
|
||||
self.type = type
|
||||
self.name = name
|
||||
self.short_label = short_label or name
|
||||
self.category = category or name
|
||||
|
||||
def grade(self, grade_sheet, generate_random_scores=False):
|
||||
found_score = None
|
||||
if self.type in grade_sheet:
|
||||
for score in grade_sheet[self.type]:
|
||||
if score.display_name == self.name:
|
||||
found_score = score
|
||||
break
|
||||
|
||||
if found_score or generate_random_scores:
|
||||
if generate_random_scores: # for debugging!
|
||||
earned = random.randint(2, 15)
|
||||
possible = random.randint(earned, 15)
|
||||
else: # We found the score
|
||||
earned = found_score.earned
|
||||
possible = found_score.possible
|
||||
|
||||
percent = earned / possible
|
||||
detail = u"{name} - {percent:.0%} ({earned:.3n}/{possible:.3n})".format(
|
||||
name=self.name,
|
||||
percent=percent,
|
||||
earned=float(earned),
|
||||
possible=float(possible)
|
||||
)
|
||||
|
||||
else:
|
||||
percent = 0.0
|
||||
detail = u"{name} - 0% (?/?)".format(name=self.name)
|
||||
|
||||
breakdown = [{'percent': percent, 'label': self.short_label,
|
||||
'detail': detail, 'category': self.category, 'prominent': True}]
|
||||
grade_breakdown[assignment_type] = {
|
||||
'percent': weighted_percent,
|
||||
'detail': section_detail,
|
||||
'category': assignment_type,
|
||||
}
|
||||
|
||||
return {
|
||||
'percent': percent,
|
||||
'section_breakdown': breakdown,
|
||||
#No grade_breakdown here
|
||||
'percent': total_percent,
|
||||
'section_breakdown': section_breakdown,
|
||||
'grade_breakdown': grade_breakdown
|
||||
}
|
||||
|
||||
|
||||
@@ -332,9 +268,9 @@ class AssignmentFormatGrader(CourseGrader):
|
||||
hide_average is to suppress the display of the total score in this grader and instead
|
||||
only show each assignment in this grader in the breakdown.
|
||||
|
||||
If there is only a single assignment in this grader, then it acts like a SingleSectionGrader
|
||||
and returns only one entry for the grader. Since the assignment and the total are the same,
|
||||
the total is returned but is not labeled as an average.
|
||||
If there is only a single assignment in this grader, then it returns only one entry for the
|
||||
grader. Since the assignment and the total are the same, the total is returned but is not
|
||||
labeled as an average.
|
||||
|
||||
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).
|
||||
@@ -373,9 +309,12 @@ class AssignmentFormatGrader(CourseGrader):
|
||||
|
||||
def grade(self, grade_sheet, generate_random_scores=False):
|
||||
def total_with_drops(breakdown, drop_count):
|
||||
'''calculates total score for a section while dropping lowest scores'''
|
||||
#create an array of tuples with (index, mark), sorted by mark['percent'] descending
|
||||
"""
|
||||
Calculates total score for a section while dropping lowest scores
|
||||
"""
|
||||
# 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:
|
||||
@@ -390,8 +329,7 @@ class AssignmentFormatGrader(CourseGrader):
|
||||
|
||||
return aggregate_score, dropped_indices
|
||||
|
||||
#Figure the homework scores
|
||||
scores = grade_sheet.get(self.type, [])
|
||||
scores = grade_sheet.get(self.type, {}).values()
|
||||
breakdown = []
|
||||
for i in range(max(self.min_count, len(scores))):
|
||||
if i < len(scores) or generate_random_scores:
|
||||
@@ -401,8 +339,8 @@ class AssignmentFormatGrader(CourseGrader):
|
||||
section_name = "Generated"
|
||||
|
||||
else:
|
||||
earned = scores[i].earned
|
||||
possible = scores[i].possible
|
||||
earned = scores[i].graded_total.earned
|
||||
possible = scores[i].graded_total.possible
|
||||
section_name = scores[i].display_name
|
||||
|
||||
percentage = earned / possible
|
||||
@@ -442,8 +380,7 @@ class AssignmentFormatGrader(CourseGrader):
|
||||
|
||||
if len(breakdown) == 1:
|
||||
# if there is only one entry in a section, suppress the existing individual entry and the average,
|
||||
# and just display a single entry for the section. That way it acts automatically like a
|
||||
# SingleSectionGrader.
|
||||
# and just display a single entry for the section.
|
||||
total_detail = u"{section_type} = {percent:.0%}".format(
|
||||
percent=total_percent,
|
||||
section_type=self.section_type,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Grading tests"""
|
||||
import ddt
|
||||
import unittest
|
||||
|
||||
from xmodule import graders
|
||||
@@ -12,13 +13,11 @@ class GradesheetTest(unittest.TestCase):
|
||||
|
||||
def test_weighted_grading(self):
|
||||
scores = []
|
||||
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,
|
||||
)
|
||||
agg_fields = dict(attempted=False)
|
||||
prob_fields = dict(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'])
|
||||
all_total, graded_total = aggregate_scores(scores)
|
||||
self.assertEqual(
|
||||
all_total,
|
||||
AggregatedScore(tw_earned=0, tw_possible=0, graded=False, **agg_fields),
|
||||
@@ -30,7 +29,7 @@ class GradesheetTest(unittest.TestCase):
|
||||
|
||||
# (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'])
|
||||
all_total, graded_total = aggregate_scores(scores)
|
||||
self.assertEqual(
|
||||
all_total,
|
||||
AggregatedScore(tw_earned=0, tw_possible=5, graded=False, **agg_fields),
|
||||
@@ -44,7 +43,7 @@ class GradesheetTest(unittest.TestCase):
|
||||
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'])
|
||||
all_total, graded_total = aggregate_scores(scores)
|
||||
self.assertAlmostEqual(
|
||||
all_total,
|
||||
AggregatedScore(tw_earned=3, tw_possible=10, graded=False, **agg_fields),
|
||||
@@ -56,7 +55,7 @@ class GradesheetTest(unittest.TestCase):
|
||||
|
||||
# (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'])
|
||||
all_total, graded_total = aggregate_scores(scores)
|
||||
self.assertAlmostEqual(
|
||||
all_total,
|
||||
AggregatedScore(tw_earned=5, tw_possible=15, graded=False, **agg_fields),
|
||||
@@ -67,6 +66,7 @@ class GradesheetTest(unittest.TestCase):
|
||||
)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class GraderTest(unittest.TestCase):
|
||||
"""
|
||||
Tests grader implementations
|
||||
@@ -76,55 +76,48 @@ class GraderTest(unittest.TestCase):
|
||||
}
|
||||
|
||||
incomplete_gradesheet = {
|
||||
'Homework': [],
|
||||
'Lab': [],
|
||||
'Midterm': [],
|
||||
'Homework': {},
|
||||
'Lab': {},
|
||||
'Midterm': {},
|
||||
}
|
||||
|
||||
common_fields = dict(graded=True, module_id=None, attempted=True)
|
||||
class MockGrade(object):
|
||||
"""
|
||||
Mock class for SubsectionGrade object.
|
||||
"""
|
||||
def __init__(self, graded_total, display_name):
|
||||
self.graded_total = graded_total
|
||||
self.display_name = display_name
|
||||
|
||||
common_fields = dict(graded=True, attempted=True)
|
||||
test_gradesheet = {
|
||||
'Homework': [
|
||||
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),
|
||||
],
|
||||
'Homework': {
|
||||
'hw1': MockGrade(AggregatedScore(tw_earned=2, tw_possible=20.0, **common_fields), display_name='hw1'),
|
||||
'hw2': MockGrade(AggregatedScore(tw_earned=16, tw_possible=16.0, **common_fields), display_name='hw2'),
|
||||
},
|
||||
|
||||
# The dropped scores should be from the assignments that don't exist yet
|
||||
'Lab': [
|
||||
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),
|
||||
],
|
||||
'Lab': {
|
||||
# Dropped
|
||||
'lab1': MockGrade(AggregatedScore(tw_earned=1, tw_possible=2.0, **common_fields), display_name='lab1'),
|
||||
'lab2': MockGrade(AggregatedScore(tw_earned=1, tw_possible=1.0, **common_fields), display_name='lab2'),
|
||||
'lab3': MockGrade(AggregatedScore(tw_earned=1, tw_possible=1.0, **common_fields), display_name='lab3'),
|
||||
# Dropped
|
||||
'lab4': MockGrade(AggregatedScore(tw_earned=5, tw_possible=25.0, **common_fields), display_name='lab4'),
|
||||
# Dropped
|
||||
'lab5': MockGrade(AggregatedScore(tw_earned=3, tw_possible=4.0, **common_fields), display_name='lab5'),
|
||||
'lab6': MockGrade(AggregatedScore(tw_earned=6, tw_possible=7.0, **common_fields), display_name='lab6'),
|
||||
'lab7': MockGrade(AggregatedScore(tw_earned=5, tw_possible=6.0, **common_fields), display_name='lab7'),
|
||||
},
|
||||
|
||||
'Midterm': [
|
||||
AggregatedScore(tw_earned=50.5, tw_possible=100, display_name="Midterm Exam", **common_fields),
|
||||
],
|
||||
'Midterm': {
|
||||
'midterm': MockGrade(
|
||||
AggregatedScore(tw_earned=50.5, tw_possible=100, **common_fields),
|
||||
display_name="Midterm Exam",
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
def test_single_section_grader(self):
|
||||
midterm_grader = graders.SingleSectionGrader("Midterm", "Midterm Exam")
|
||||
lab4_grader = graders.SingleSectionGrader("Lab", "lab4")
|
||||
bad_lab_grader = graders.SingleSectionGrader("Lab", "lab42")
|
||||
|
||||
for graded in [
|
||||
midterm_grader.grade(self.empty_gradesheet),
|
||||
midterm_grader.grade(self.incomplete_gradesheet),
|
||||
bad_lab_grader.grade(self.test_gradesheet),
|
||||
]:
|
||||
self.assertEqual(len(graded['section_breakdown']), 1)
|
||||
self.assertEqual(graded['percent'], 0.0)
|
||||
|
||||
graded = midterm_grader.grade(self.test_gradesheet)
|
||||
self.assertAlmostEqual(graded['percent'], 0.505)
|
||||
self.assertEqual(len(graded['section_breakdown']), 1)
|
||||
|
||||
graded = lab4_grader.grade(self.test_gradesheet)
|
||||
self.assertAlmostEqual(graded['percent'], 0.2)
|
||||
self.assertEqual(len(graded['section_breakdown']), 1)
|
||||
|
||||
def test_assignment_format_grader(self):
|
||||
homework_grader = graders.AssignmentFormatGrader("Homework", 12, 2)
|
||||
no_drop_grader = graders.AssignmentFormatGrader("Homework", 12, 0)
|
||||
@@ -179,8 +172,6 @@ class GraderTest(unittest.TestCase):
|
||||
# First, a few sub graders
|
||||
homework_grader = graders.AssignmentFormatGrader("Homework", 12, 2)
|
||||
lab_grader = graders.AssignmentFormatGrader("Lab", 7, 3)
|
||||
# phasing out the use of SingleSectionGraders, and instead using AssignmentFormatGraders that
|
||||
# will act like SingleSectionGraders on single sections.
|
||||
midterm_grader = graders.AssignmentFormatGrader("Midterm", 1, 0)
|
||||
|
||||
weighted_grader = graders.WeightedSubsectionsGrader([
|
||||
@@ -268,6 +259,8 @@ class GraderTest(unittest.TestCase):
|
||||
},
|
||||
{
|
||||
'type': "Midterm",
|
||||
'min_count': 0,
|
||||
'drop_count': 0,
|
||||
'name': "Midterm Exam",
|
||||
'short_label': "Midterm",
|
||||
'weight': 0.5,
|
||||
@@ -294,5 +287,25 @@ class GraderTest(unittest.TestCase):
|
||||
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?
|
||||
@ddt.data(
|
||||
(
|
||||
# empty
|
||||
{},
|
||||
u"Configuration has no appropriate grader class."
|
||||
),
|
||||
(
|
||||
# no min_count
|
||||
{'type': "Homework", 'drop_count': 0},
|
||||
u"Configuration has no appropriate grader class."
|
||||
),
|
||||
(
|
||||
# no drop_count
|
||||
{'type': "Homework", 'min_count': 0},
|
||||
u"__init__() takes at least 4 arguments (3 given)"
|
||||
),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_grader_with_invalid_conf(self, invalid_conf, expected_error_message):
|
||||
with self.assertRaises(ValueError) as error:
|
||||
graders.grader_from_conf([invalid_conf])
|
||||
self.assertIn(expected_error_message, error.exception.message)
|
||||
|
||||
Reference in New Issue
Block a user