# grading.py # ---------- # Licensing Information: You are free to use or extend these projects for # educational purposes provided that (1) you do not distribute or publish # solutions, (2) you retain this notice, and (3) you provide clear # attribution to UC Berkeley, including a link to http://ai.berkeley.edu. # # Attribution Information: The Pacman AI projects were developed at UC Berkeley. # The core projects and autograders were primarily created by John DeNero # (denero@cs.berkeley.edu) and Dan Klein (klein@cs.berkeley.edu). # Student side autograding was added by Brad Miller, Nick Hay, and # Pieter Abbeel (pabbeel@cs.berkeley.edu). "Common code for autograders" import cgi import time import sys import json import traceback import pdb from collections import defaultdict import util class Grades: "A data structure for project grades, along with formatting code to display them" def __init__(self, projectName, questionsAndMaxesList, gsOutput=False, edxOutput=False, muteOutput=False): """ Defines the grading scheme for a project projectName: project name questionsAndMaxesDict: a list of (question name, max points per question) """ self.questions = [el[0] for el in questionsAndMaxesList] self.maxes = dict(questionsAndMaxesList) self.points = Counter() self.messages = dict([(q, []) for q in self.questions]) self.project = projectName self.start = time.localtime()[1:6] self.sane = True # Sanity checks self.currentQuestion = None # Which question we're grading self.edxOutput = edxOutput self.gsOutput = gsOutput # GradeScope output self.mute = muteOutput self.prereqs = defaultdict(set) # print 'Autograder transcript for %s' % self.project print('Starting on %d-%d at %d:%02d:%02d' % self.start) def addPrereq(self, question, prereq): self.prereqs[question].add(prereq) def grade(self, gradingModule, exceptionMap={}, bonusPic=False): """ Grades each question gradingModule: the module with all the grading functions (pass in with sys.modules[__name__]) """ completedQuestions = set([]) for q in self.questions: print('\nQuestion %s' % q) print('=' * (9 + len(q))) print() self.currentQuestion = q incompleted = self.prereqs[q].difference(completedQuestions) if len(incompleted) > 0: prereq = incompleted.pop() print("""*** NOTE: Make sure to complete Question %s before working on Question %s, *** because Question %s builds upon your answer for Question %s. """ % (prereq, q, q, prereq)) continue if self.mute: util.mutePrint() try: util.TimeoutFunction(getattr(gradingModule, q), 1800)( self) # Call the question's function # TimeoutFunction(getattr(gradingModule, q),1200)(self) # Call the question's function except Exception as inst: self.addExceptionMessage(q, inst, traceback) self.addErrorHints(exceptionMap, inst, q[1]) except: self.fail('FAIL: Terminated with a string exception.') finally: if self.mute: util.unmutePrint() if self.points[q] >= self.maxes[q]: completedQuestions.add(q) print('\n### Question %s: %d/%d ###\n' % (q, self.points[q], self.maxes[q])) print('\nFinished at %d:%02d:%02d' % time.localtime()[3:6]) print("\nProvisional grades\n==================") for q in self.questions: print('Question %s: %d/%d' % (q, self.points[q], self.maxes[q])) print('------------------') print('Total: %d/%d' % (self.points.totalCount(), sum(self.maxes.values()))) if bonusPic and self.points.totalCount() == 25: print(""" ALL HAIL GRANDPAC. LONG LIVE THE GHOSTBUSTING KING. --- ---- --- | \ / + \ / | | + \--/ \--/ + | | + + | | + + + | @@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ \ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ \ / @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ V \ @@@@@@@@@@@@@@@@@@@@@@@@@@@@ \ / @@@@@@@@@@@@@@@@@@@@@@@@@@ V @@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@ /\ @@@@@@@@@@@@@@@@@@@@@@ / \ @@@@@@@@@@@@@@@@@@@@@@@@@ /\ / @@@@@@@@@@@@@@@@@@@@@@@@@@@ / \ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ / @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@ """) print(""" Your grades are NOT yet registered. To register your grades, make sure to follow your instructor's guidelines to receive credit on your project. """) if self.edxOutput: self.produceOutput() if self.gsOutput: self.produceGradeScopeOutput() def addExceptionMessage(self, q, inst, traceback): """ Method to format the exception message, this is more complicated because we need to cgi.escape the traceback but wrap the exception in a
 tag
        """
        self.fail('FAIL: Exception raised: %s' % inst)
        self.addMessage('')
        for line in traceback.format_exc().split('\n'):
            self.addMessage(line)

    def addErrorHints(self, exceptionMap, errorInstance, questionNum):
        typeOf = str(type(errorInstance))
        questionName = 'q' + questionNum
        errorHint = ''

        # question specific error hints
        if exceptionMap.get(questionName):
            questionMap = exceptionMap.get(questionName)
            if (questionMap.get(typeOf)):
                errorHint = questionMap.get(typeOf)
        # fall back to general error messages if a question specific
        # one does not exist
        if (exceptionMap.get(typeOf)):
            errorHint = exceptionMap.get(typeOf)

        # dont include the HTML if we have no error hint
        if not errorHint:
            return ''

        for line in errorHint.split('\n'):
            self.addMessage(line)

    def produceGradeScopeOutput(self):
        out_dct = {}

        # total of entire submission
        total_possible = sum(self.maxes.values())
        total_score = sum(self.points.values())
        out_dct['score'] = total_score
        out_dct['max_score'] = total_possible
        out_dct['output'] = "Total score (%d / %d)" % (
            total_score, total_possible)

        # individual tests
        tests_out = []
        for name in self.questions:
            test_out = {}
            # test name
            test_out['name'] = name
            # test score
            test_out['score'] = self.points[name]
            test_out['max_score'] = self.maxes[name]
            # others
            is_correct = self.points[name] >= self.maxes[name]
            test_out['output'] = "  Question {num} ({points}/{max}) {correct}".format(
                num=(name[1] if len(name) == 2 else name),
                points=test_out['score'],
                max=test_out['max_score'],
                correct=('X' if not is_correct else ''),
            )
            test_out['tags'] = []
            tests_out.append(test_out)
        out_dct['tests'] = tests_out

        # file output
        with open('gradescope_response.json', 'w') as outfile:
            json.dump(out_dct, outfile)
        return

    def produceOutput(self):
        edxOutput = open('edx_response.html', 'w')
        edxOutput.write("
") # first sum total_possible = sum(self.maxes.values()) total_score = sum(self.points.values()) checkOrX = '' if (total_score >= total_possible): checkOrX = '' header = """

Total score ({total_score} / {total_possible})

""".format(total_score=total_score, total_possible=total_possible, checkOrX=checkOrX ) edxOutput.write(header) for q in self.questions: if len(q) == 2: name = q[1] else: name = q checkOrX = '' if (self.points[q] >= self.maxes[q]): checkOrX = '' #messages = '\n
\n'.join(self.messages[q]) messages = "
%s
" % '\n'.join(self.messages[q]) output = """
Question {q} ({points}/{max}) {checkOrX}
{messages}
""".format(q=name, max=self.maxes[q], messages=messages, checkOrX=checkOrX, points=self.points[q] ) # print "*** output for Question %s " % q[1] # print output edxOutput.write(output) edxOutput.write("
") edxOutput.close() edxOutput = open('edx_grade', 'w') edxOutput.write(str(self.points.totalCount())) edxOutput.close() def fail(self, message, raw=False): "Sets sanity check bit to false and outputs a message" self.sane = False self.assignZeroCredit() self.addMessage(message, raw) def assignZeroCredit(self): self.points[self.currentQuestion] = 0 def addPoints(self, amt): self.points[self.currentQuestion] += amt def deductPoints(self, amt): self.points[self.currentQuestion] -= amt def assignFullCredit(self, message="", raw=False): self.points[self.currentQuestion] = self.maxes[self.currentQuestion] if message != "": self.addMessage(message, raw) def addMessage(self, message, raw=False): if not raw: # We assume raw messages, formatted for HTML, are printed separately if self.mute: util.unmutePrint() print('*** ' + message) if self.mute: util.mutePrint() message = cgi.escape(message) self.messages[self.currentQuestion].append(message) def addMessageToEmail(self, message): print("WARNING**** addMessageToEmail is deprecated %s" % message) for line in message.split('\n'): pass # print '%%% ' + line + ' %%%' # self.messages[self.currentQuestion].append(line) class Counter(dict): """ Dict with default 0 """ def __getitem__(self, idx): try: return dict.__getitem__(self, idx) except KeyError: return 0 def totalCount(self): """ Returns the sum of counts for all keys. """ return sum(self.values())