Commit ba7d29b3 by Victor Shnayder

Merge pull request #1763 from MITx/fix/brian/single_section_grading

modify AssignmentFormatGrader to act like a SingleSectionGrader on single sections
parents 53cf8aa7 50fd7ee7
......@@ -45,8 +45,9 @@ def invalid_args(func, argdict):
Given a function and a dictionary of arguments, returns a set of arguments
from argdict that aren't accepted by func
"""
args, varargs, keywords, defaults = inspect.getargspec(func)
if keywords: return set() # All accepted
args, _, keywords, _ = inspect.getargspec(func)
if keywords:
return set() # All accepted
return set(argdict) - set(args)
......@@ -119,7 +120,7 @@ class CourseGrader(object):
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.
- percent: Contains 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.
......@@ -150,6 +151,7 @@ class CourseGrader(object):
@abc.abstractmethod
def grade(self, grade_sheet, generate_random_scores=False):
'''Given a grade sheet, return a dict containing grading information'''
raise NotImplementedError
......@@ -158,7 +160,10 @@ 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) ]
[ (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.
......@@ -177,12 +182,12 @@ class WeightedSubsectionsGrader(CourseGrader):
for subgrader, category, weight in self.sections:
subgrade_result = subgrader.grade(grade_sheet, generate_random_scores)
weightedPercent = subgrade_result['percent'] * weight
section_detail = "{0} = {1:.1%} of a possible {2:.0%}".format(category, weightedPercent, weight)
weighted_percent = subgrade_result['percent'] * weight
section_detail = "{0} = {1:.1%} of a possible {2:.0%}".format(category, weighted_percent, weight)
total_percent += weightedPercent
total_percent += weighted_percent
section_breakdown += subgrade_result['section_breakdown']
grade_breakdown.append({'percent': weightedPercent, 'detail': section_detail, 'category': category})
grade_breakdown.append({'percent': weighted_percent, 'detail': section_detail, 'category': category})
return {'percent': total_percent,
'section_breakdown': section_breakdown,
......@@ -203,20 +208,20 @@ class SingleSectionGrader(CourseGrader):
self.category = category or name
def grade(self, grade_sheet, generate_random_scores=False):
foundScore = None
found_score = None
if self.type in grade_sheet:
for score in grade_sheet[self.type]:
if score.section == self.name:
foundScore = score
found_score = score
break
if foundScore or generate_random_scores:
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 = foundScore.earned
possible = foundScore.possible
earned = found_score.earned
possible = found_score.possible
percent = earned / float(possible)
detail = "{name} - {percent:.0%} ({earned:.3n}/{possible:.3n})".format(name=self.name,
......@@ -228,7 +233,8 @@ class SingleSectionGrader(CourseGrader):
percent = 0.0
detail = "{name} - 0% (?/?)".format(name=self.name)
breakdown = [{'percent': percent, 'label': self.short_label, 'detail': detail, 'category': self.category, 'prominent': True}]
breakdown = [{'percent': percent, 'label': self.short_label,
'detail': detail, 'category': self.category, 'prominent': True}]
return {'percent': percent,
'section_breakdown': breakdown,
......@@ -250,6 +256,13 @@ class AssignmentFormatGrader(CourseGrader):
show_only_average is to suppress the display of each assignment in this grader and instead
only show the total score of this grader in the breakdown.
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.
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).
......@@ -263,7 +276,8 @@ class AssignmentFormatGrader(CourseGrader):
min_count = 2 would produce the labels "Assignment 3", "Assignment 4"
"""
def __init__(self, type, min_count, drop_count, category=None, section_type=None, short_label=None, show_only_average=False, hide_average=False, starting_index=1):
def __init__(self, type, min_count, drop_count, category=None, section_type=None, short_label=None,
show_only_average=False, hide_average=False, starting_index=1):
self.type = type
self.min_count = min_count
self.drop_count = drop_count
......@@ -275,7 +289,8 @@ class AssignmentFormatGrader(CourseGrader):
self.hide_average = hide_average
def grade(self, grade_sheet, generate_random_scores=False):
def totalWithDrops(breakdown, drop_count):
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
sorted_breakdown = sorted(enumerate(breakdown), key=lambda x: -x[1]['percent'])
# A list of the indices of the dropped scores
......@@ -308,7 +323,8 @@ class AssignmentFormatGrader(CourseGrader):
section_name = scores[i].section
percentage = earned / float(possible)
summary = "{section_type} {index} - {name} - {percent:.0%} ({earned:.3n}/{possible:.3n})".format(index=i + self.starting_index,
summary_format = "{section_type} {index} - {name} - {percent:.0%} ({earned:.3n}/{possible:.3n})"
summary = summary_format.format(index=i + self.starting_index,
section_type=self.section_type,
name=section_name,
percent=percentage,
......@@ -316,25 +332,41 @@ class AssignmentFormatGrader(CourseGrader):
possible=float(possible))
else:
percentage = 0
summary = "{section_type} {index} Unreleased - 0% (?/?)".format(index=i + self.starting_index, section_type=self.section_type)
summary = "{section_type} {index} Unreleased - 0% (?/?)".format(index=i + self.starting_index,
section_type=self.section_type)
short_label = "{short_label} {index:02d}".format(index=i + self.starting_index, short_label=self.short_label)
short_label = "{short_label} {index:02d}".format(index=i + self.starting_index,
short_label=self.short_label)
breakdown.append({'percent': percentage, 'label': short_label, 'detail': summary, 'category': self.category})
breakdown.append({'percent': percentage, 'label': short_label,
'detail': summary, 'category': self.category})
total_percent, dropped_indices = totalWithDrops(breakdown, self.drop_count)
total_percent, dropped_indices = total_with_drops(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)
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)}
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.
total_detail = "{section_type} = {percent:.0%}".format(percent=total_percent,
section_type=self.section_type)
total_label = "{short_label}".format(short_label=self.short_label)
breakdown = [{'percent': total_percent, 'label': total_label,
'detail': total_detail, 'category': self.category, 'prominent': True}, ]
else:
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)
if self.show_only_average:
breakdown = []
if not self.hide_average:
breakdown.append({'percent': total_percent, 'label': total_label, 'detail': total_detail, 'category': self.category, 'prominent': True})
breakdown.append({'percent': total_percent, 'label': total_label,
'detail': total_detail, 'category': self.category, 'prominent': True})
return {'percent': total_percent,
'section_breakdown': breakdown,
......
......@@ -6,32 +6,34 @@ from xmodule.graders import Score, aggregate_scores
class GradesheetTest(unittest.TestCase):
'''Tests the aggregate_scores method'''
def test_weighted_grading(self):
scores = []
Score.__sub__ = lambda me, other: (me.earned - other.earned) + (me.possible - other.possible)
all, graded = aggregate_scores(scores)
self.assertEqual(all, Score(earned=0, possible=0, graded=False, section="summary"))
self.assertEqual(graded, Score(earned=0, possible=0, graded=True, section="summary"))
all_total, graded_total = aggregate_scores(scores)
self.assertEqual(all_total, Score(earned=0, possible=0, graded=False, section="summary"))
self.assertEqual(graded_total, Score(earned=0, possible=0, graded=True, section="summary"))
scores.append(Score(earned=0, possible=5, graded=False, section="summary"))
all, graded = aggregate_scores(scores)
self.assertEqual(all, Score(earned=0, possible=5, graded=False, section="summary"))
self.assertEqual(graded, Score(earned=0, possible=0, graded=True, section="summary"))
all_total, graded_total = aggregate_scores(scores)
self.assertEqual(all_total, Score(earned=0, possible=5, graded=False, section="summary"))
self.assertEqual(graded_total, Score(earned=0, possible=0, graded=True, section="summary"))
scores.append(Score(earned=3, possible=5, graded=True, section="summary"))
all, graded = aggregate_scores(scores)
self.assertAlmostEqual(all, Score(earned=3, possible=10, graded=False, section="summary"))
self.assertAlmostEqual(graded, Score(earned=3, possible=5, graded=True, section="summary"))
all_total, graded_total = aggregate_scores(scores)
self.assertAlmostEqual(all_total, Score(earned=3, possible=10, graded=False, section="summary"))
self.assertAlmostEqual(graded_total, Score(earned=3, possible=5, graded=True, section="summary"))
scores.append(Score(earned=2, possible=5, graded=True, section="summary"))
all, graded = aggregate_scores(scores)
self.assertAlmostEqual(all, Score(earned=5, possible=15, graded=False, section="summary"))
self.assertAlmostEqual(graded, Score(earned=5, possible=10, graded=True, section="summary"))
all_total, graded_total = aggregate_scores(scores)
self.assertAlmostEqual(all_total, Score(earned=5, possible=15, graded=False, section="summary"))
self.assertAlmostEqual(graded_total, Score(earned=5, possible=10, graded=True, section="summary"))
class GraderTest(unittest.TestCase):
'''Tests grader implementations'''
empty_gradesheet = {
}
......@@ -45,7 +47,7 @@ class GraderTest(unittest.TestCase):
test_gradesheet = {
'Homework': [Score(earned=2, possible=20.0, graded=True, section='hw1'),
Score(earned=16, possible=16.0, graded=True, section='hw2')],
#The dropped scores should be from the assignments that don't exist yet
# The dropped scores should be from the assignments that don't exist yet
'Lab': [Score(earned=1, possible=2.0, graded=True, section='lab1'), # Dropped
Score(earned=1, possible=1.0, graded=True, section='lab2'),
......@@ -58,122 +60,138 @@ class GraderTest(unittest.TestCase):
'Midterm': [Score(earned=50.5, possible=100, graded=True, section="Midterm Exam"), ],
}
def test_SingleSectionGrader(self):
midtermGrader = graders.SingleSectionGrader("Midterm", "Midterm Exam")
lab4Grader = graders.SingleSectionGrader("Lab", "lab4")
badLabGrader = graders.SingleSectionGrader("Lab", "lab42")
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 [midtermGrader.grade(self.empty_gradesheet),
midtermGrader.grade(self.incomplete_gradesheet),
badLabGrader.grade(self.test_gradesheet)]:
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 = midtermGrader.grade(self.test_gradesheet)
graded = midterm_grader.grade(self.test_gradesheet)
self.assertAlmostEqual(graded['percent'], 0.505)
self.assertEqual(len(graded['section_breakdown']), 1)
graded = lab4Grader.grade(self.test_gradesheet)
graded = lab4_grader.grade(self.test_gradesheet)
self.assertAlmostEqual(graded['percent'], 0.2)
self.assertEqual(len(graded['section_breakdown']), 1)
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 = graders.AssignmentFormatGrader("Lab", 3, 2)
labGrader = graders.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)]:
def test_assignment_format_grader(self):
homework_grader = graders.AssignmentFormatGrader("Homework", 12, 2)
no_drop_grader = graders.AssignmentFormatGrader("Homework", 12, 0)
# Even though the minimum number is 3, this should grade correctly when 7 assignments are found
overflow_grader = graders.AssignmentFormatGrader("Lab", 3, 2)
lab_grader = graders.AssignmentFormatGrader("Lab", 7, 3)
# Test the grading of an empty gradesheet
for graded in [homework_grader.grade(self.empty_gradesheet),
no_drop_grader.grade(self.empty_gradesheet),
homework_grader.grade(self.incomplete_gradesheet),
no_drop_grader.grade(self.incomplete_gradesheet)]:
self.assertAlmostEqual(graded['percent'], 0.0)
#Make sure the breakdown includes 12 sections, plus one summary
# Make sure the breakdown includes 12 sections, plus one summary
self.assertEqual(len(graded['section_breakdown']), 12 + 1)
graded = homeworkGrader.grade(self.test_gradesheet)
graded = homework_grader.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)
graded = no_drop_grader.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)
graded = overflow_grader.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)
graded = lab_grader.grade(self.test_gradesheet)
self.assertAlmostEqual(graded['percent'], 0.9226190476190477)
self.assertEqual(len(graded['section_breakdown']), 7 + 1)
def test_WeightedSubsectionsGrader(self):
#First, a few sub graders
homeworkGrader = graders.AssignmentFormatGrader("Homework", 12, 2)
labGrader = graders.AssignmentFormatGrader("Lab", 7, 3)
midtermGrader = graders.SingleSectionGrader("Midterm", "Midterm Exam")
def test_assignment_format_grader_on_single_section_entry(self):
midterm_grader = graders.AssignmentFormatGrader("Midterm", 1, 0)
# Test the grading on a section with one item:
for graded in [midterm_grader.grade(self.empty_gradesheet),
midterm_grader.grade(self.incomplete_gradesheet)]:
self.assertAlmostEqual(graded['percent'], 0.0)
# Make sure the breakdown includes just the one summary
self.assertEqual(len(graded['section_breakdown']), 0 + 1)
self.assertEqual(graded['section_breakdown'][0]['label'], 'Midterm')
graded = midterm_grader.grade(self.test_gradesheet)
self.assertAlmostEqual(graded['percent'], 0.505)
self.assertEqual(len(graded['section_breakdown']), 0 + 1)
def test_weighted_subsections_grader(self):
# 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)
weightedGrader = graders.WeightedSubsectionsGrader([(homeworkGrader, homeworkGrader.category, 0.25),
(labGrader, labGrader.category, 0.25),
(midtermGrader, midtermGrader.category, 0.5)])
weighted_grader = graders.WeightedSubsectionsGrader([(homework_grader, homework_grader.category, 0.25),
(lab_grader, lab_grader.category, 0.25),
(midterm_grader, midterm_grader.category, 0.5)])
overOneWeightsGrader = graders.WeightedSubsectionsGrader([(homeworkGrader, homeworkGrader.category, 0.5),
(labGrader, labGrader.category, 0.5),
(midtermGrader, midtermGrader.category, 0.5)])
over_one_weights_grader = graders.WeightedSubsectionsGrader([(homework_grader, homework_grader.category, 0.5),
(lab_grader, lab_grader.category, 0.5),
(midterm_grader, midterm_grader.category, 0.5)])
#The midterm should have all weight on this one
zeroWeightsGrader = graders.WeightedSubsectionsGrader([(homeworkGrader, homeworkGrader.category, 0.0),
(labGrader, labGrader.category, 0.0),
(midtermGrader, midtermGrader.category, 0.5)])
# The midterm should have all weight on this one
zero_weights_grader = graders.WeightedSubsectionsGrader([(homework_grader, homework_grader.category, 0.0),
(lab_grader, lab_grader.category, 0.0),
(midterm_grader, midterm_grader.category, 0.5)])
#This should always have a final percent of zero
allZeroWeightsGrader = graders.WeightedSubsectionsGrader([(homeworkGrader, homeworkGrader.category, 0.0),
(labGrader, labGrader.category, 0.0),
(midtermGrader, midtermGrader.category, 0.0)])
# This should always have a final percent of zero
all_zero_weights_grader = graders.WeightedSubsectionsGrader([(homework_grader, homework_grader.category, 0.0),
(lab_grader, lab_grader.category, 0.0),
(midterm_grader, midterm_grader.category, 0.0)])
emptyGrader = graders.WeightedSubsectionsGrader([])
empty_grader = graders.WeightedSubsectionsGrader([])
graded = weightedGrader.grade(self.test_gradesheet)
graded = weighted_grader.grade(self.test_gradesheet)
self.assertAlmostEqual(graded['percent'], 0.5106547619047619)
self.assertEqual(len(graded['section_breakdown']), (12 + 1) + (7 + 1) + 1)
self.assertEqual(len(graded['grade_breakdown']), 3)
graded = overOneWeightsGrader.grade(self.test_gradesheet)
graded = over_one_weights_grader.grade(self.test_gradesheet)
self.assertAlmostEqual(graded['percent'], 0.7688095238095238)
self.assertEqual(len(graded['section_breakdown']), (12 + 1) + (7 + 1) + 1)
self.assertEqual(len(graded['grade_breakdown']), 3)
graded = zeroWeightsGrader.grade(self.test_gradesheet)
graded = zero_weights_grader.grade(self.test_gradesheet)
self.assertAlmostEqual(graded['percent'], 0.2525)
self.assertEqual(len(graded['section_breakdown']), (12 + 1) + (7 + 1) + 1)
self.assertEqual(len(graded['grade_breakdown']), 3)
graded = allZeroWeightsGrader.grade(self.test_gradesheet)
graded = all_zero_weights_grader.grade(self.test_gradesheet)
self.assertAlmostEqual(graded['percent'], 0.0)
self.assertEqual(len(graded['section_breakdown']), (12 + 1) + (7 + 1) + 1)
self.assertEqual(len(graded['grade_breakdown']), 3)
for graded in [weightedGrader.grade(self.empty_gradesheet),
weightedGrader.grade(self.incomplete_gradesheet),
zeroWeightsGrader.grade(self.empty_gradesheet),
allZeroWeightsGrader.grade(self.empty_gradesheet)]:
for graded in [weighted_grader.grade(self.empty_gradesheet),
weighted_grader.grade(self.incomplete_gradesheet),
zero_weights_grader.grade(self.empty_gradesheet),
all_zero_weights_grader.grade(self.empty_gradesheet)]:
self.assertAlmostEqual(graded['percent'], 0.0)
self.assertEqual(len(graded['section_breakdown']), (12 + 1) + (7 + 1) + 1)
self.assertEqual(len(graded['grade_breakdown']), 3)
graded = emptyGrader.grade(self.test_gradesheet)
graded = empty_grader.grade(self.test_gradesheet)
self.assertAlmostEqual(graded['percent'], 0.0)
self.assertEqual(len(graded['section_breakdown']), 0)
self.assertEqual(len(graded['grade_breakdown']), 0)
def test_graderFromConf(self):
def test_grader_from_conf(self):
#Confs always produce a graders.WeightedSubsectionsGrader, so we test this by repeating the test
#in test_graders.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 = graders.grader_from_conf([
weighted_grader = graders.grader_from_conf([
{
'type': "Homework",
'min_count': 12,
......@@ -196,25 +214,25 @@ class GraderTest(unittest.TestCase):
},
])
emptyGrader = graders.grader_from_conf([])
empty_grader = graders.grader_from_conf([])
graded = weightedGrader.grade(self.test_gradesheet)
graded = weighted_grader.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)
graded = empty_grader.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 = graders.AssignmentFormatGrader("Homework", 12, 2)
homeworkGrader2 = graders.grader_from_conf(homeworkGrader)
# Test that graders can also be used instead of lists of dictionaries
homework_grader = graders.AssignmentFormatGrader("Homework", 12, 2)
homework_grader2 = graders.grader_from_conf(homework_grader)
graded = homeworkGrader2.grade(self.test_gradesheet)
graded = homework_grader2.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?
# TODO: How do we test failure cases? The parser only logs an error when
# it can't parse something. Maybe it should throw exceptions?
......@@ -74,8 +74,8 @@ class TestInstructorDashboardGradeDownloadCSV(LoginEnrollmentTestCase):
# All the not-actually-in-the-course hw and labs come from the
# default grading policy string in graders.py
expected_body = '''"ID","Username","Full Name","edX email","External email","HW 01","HW 02","HW 03","HW 04","HW 05","HW 06","HW 07","HW 08","HW 09","HW 10","HW 11","HW 12","HW Avg","Lab 01","Lab 02","Lab 03","Lab 04","Lab 05","Lab 06","Lab 07","Lab 08","Lab 09","Lab 10","Lab 11","Lab 12","Lab Avg","Midterm 01","Midterm Avg","Final 01","Final Avg"
"2","u2","Fred Weasley","view2@test.com","","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0"
expected_body = '''"ID","Username","Full Name","edX email","External email","HW 01","HW 02","HW 03","HW 04","HW 05","HW 06","HW 07","HW 08","HW 09","HW 10","HW 11","HW 12","HW Avg","Lab 01","Lab 02","Lab 03","Lab 04","Lab 05","Lab 06","Lab 07","Lab 08","Lab 09","Lab 10","Lab 11","Lab 12","Lab Avg","Midterm","Final"
"2","u2","Fred Weasley","view2@test.com","","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0"
'''
self.assertEqual(body, expected_body, msg)
......
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