""" A Grader grades a ThingToGrade, using a: -- WhoToGrade -- RepoHelper (for checking out files) -- WhereToGrade (where the checked-out files live) -- Tester -- Recorder. A ThingToGrade specifies the: -- Course (including Term) [and hence Course information like the students enrolled, etc] -- Project -- [optionally, NOT YET IMPLEMENTED] -- module(s) within the project, -- functions within module(s), and -- ??? Authors: David Mutchler, David Lam, Mark Hays and their colleagues. Version 1.1: May, 2015. """ import repo_helper import tester import recorder import main_for_testing class Grader(object): """ A Grader grades a ThingToGrade, for students expressed by a WhoToGrade, using a: -- RepoHelper (for checking out files) -- Tester -- Recorder. If who_to_grade_argument is None: Grade all students in the course. If this_tester is None: Use a StandardTester. If this_recorderr is None: Use a StandardRecorder. If this_repo_helper is None: Use a StandardRepoHelper. :type what_to_grade: ThingToGrade :type who_to_grade_argument: WhoToGrade :type tester: Tester :type recorder Recorder :type repo_helper RepoHelper """ def __init__(self, what_to_grade, who_to_grade=None, this_tester=None, this_recorder=None, this_repo_helper=None): self.what_to_grade = what_to_grade course = what_to_grade.course self.who_to_grade = WhoToGrade(who_to_grade, course) self.repo_helper = this_repo_helper or \ repo_helper.StandardRepoHelper(what_to_grade, who_to_grade) self.tester = this_tester or \ tester.StandardTester(self.what_to_grade, self.who_to_grade, self.repo_helper.get_grading_folder()) self.recorder = this_recorder or recorder.StandardRecorder() # TODO: Implement use of replace_existing_repos. def checkout(self, replace_existing_repos=True, more_usernames=None): """ Checkout the projects for the students to grade. """ usernames = self.who_to_grade.students if more_usernames: usernames = usernames + more_usernames result = self.repo_helper.checkout_for_grading( usernames, replace_existing_repos) self._display_result_of_checkout(result) # TODO: Should this always skip checkout, or allow as an option? def grade(self, do_checkout=False, replace_existing_repos=False, more_usernames=None): """ Grade this Grader's WhatToGrade, for the students/teams in this Grader's WhoToGrade, using this Grader's Tester, recording results using this Grader's Recorder. Optional arguments: -- replace_existing_repos: indicates whether or not to re-checkout projects (overwriting what was there). -- more_usernames: lists additional usernames whose project should be checked out (e.g. a SOLUTION username). """ # print(self.who_to_grade_argument.students) # Checkout the specified students (usernames): if do_checkout: self.checkout(replace_existing_repos, more_usernames) # TODO: Add the replace_existing_repos as an optional argument # to the RepoHelper's checkout_for_grading, # so that checkout can be on a student-by-student basis. # TODO: Embed the following in the type of Grader or ??? # # Checkout the original and solution projects: # if include_original: # original = self.what_to_grade.course.username_for_original # self.repo_helper.checkout_for_grading(what, [original]) # # if include_solution: # solution = self.what_to_grade.course.username_for_solution # self.repo_helper.checkout_for_grading(what, [solution]) # # Do the tests on the specified students (usernames) # and record the results: results = self.tester.do_tests_on_students() # self.recorder.record_all_results(results) # print('Failures:') # self.recorder.record_failures(results) @staticmethod def _display_result_of_checkout(result): """ Display the given result, which is a 2-tuple containing: -- the list of failed checkouts -- the list of skipped checkouts :type result: tuple(list(str)) """ failures, skipped, _ = result # Ignore the 3rd item: successes if failures: print('Checkout FAILED for the following students:') print(' ', *failures, sep=' ') if skipped: print('Checkout SKIPPED the following students') print('because their projects were already checked out:') print(' ', *skipped, sep=' ') class ProjectGrader(Grader): # TODO: Move the part of Grader (above) that is specific to # grading a project to this class. # CONSIDER: Should a ProjectGrader use ModuleGraders? def __init__(self, what_to_grade): """ :type what_to_grade: ThingToGrade """ super().__init__(what_to_grade) class ModuleGrader(Grader): # CONSIDER: Is this subclass a good thing to implement? # If so, implement this. pass class FunctionGrader(Grader): # CONSIDER: Is this subclass a good thing to implement? # If so, implement this. pass class StandardGrader(ProjectGrader): """ A StandardGrader is a ProjectGrader that uses a StandardTester and StandardRecorder to grade all students in the ThingToGrade's Course. """ def __init__(self, what_to_grade): """ :type what_to_grade: ThingToGrade """ super().__init__(what_to_grade) class ThingToGrade(object): """ A ThingToGrade specifies the: -- Course (including Term, and also course information like the students enrolled, etc) -- Project -- (optionally) module(s) within the project """ # CONSIDER: Perhaps allow grading of other things too, # e.g. listing the functions/methods/classes within a project # to grade. def __init__(self, course, project, modules=None): """ Stores the given course, project and (optionally) modules. The project can be specified as either: -- a positive integer that is the session number, or -- the project name itself. :type course: Course :type project (int, str) :type modules list((int, str)) """ # NOTE: Throughout I have used extensions of the hint-notation # despite them not yet being implemented in PyDev. # In particular, PyDev does not yet allow (int, str). self.course = course self.project = project try: # project can be a session number self.project_name = course.projects[project] except: # or the project name itself self.project_name = project self.modules = modules or course.get_modules(self.project_name) def __repr__(self): return 'ThingToGrade({}, {}, {})'.format(self.course, self.project, self.modules) class WhoToGrade(object): """ A list of usernames, or something that determines such a list. """ def __init__(self, course, students_or_sections=None): """ If students_or_sections is: -- None or []: Grade all students in the course. -- An integer: Grade all students in that section. -- A string: Grade all usernames listed in that file. -- Sequence: Grade all students listed. -- Filename (string): Grade all students listed in that file. :type who_to_grade_indicator: (list, tuple, int) :type course: Course """ # CONSIDER: Allow other types for who_to_grade_indicator ??? # For example, allow a single username? # If so, must distinguish from a file. # CONSIDER: Allow GROUP names in the sequence or file? # Keep the indicator and course, just in case they are needed: self.who_to_grade_indicator = who_to_grade_indicator self.course = course if not who_to_grade_indicator: # None or empty list - grade all the students self.students = course.get_usernames() elif type(who_to_grade_indicator) is int: # integer - grade all students in that section self.students = course.get_usernames(who_to_grade_indicator) elif type(who_to_grade_indicator) is str: # filename - grade all students in that file with open(who_to_grade_indicator, 'r') as file: self.students = file.read().split() else: # sequence - of students to grade self.students = who_to_grade_indicator # TODO: Error handling in the above. def main(): main_for_testing.main() if __name__ == '__main__': main() # ---------------------------------------------------------------------- # Code below here was once useful but is no longer correct. # # CONSIDER: Maybe rejuventate it. # ---------------------------------------------------------------------- # def choose_testers(self): # for thing_to_grade in self.things_to_grade: # self.tester[thing_to_grade] = tester.Tester(thing_to_grade) # # (self.grader[thing_to_grade]).grade(thing_to_grade) # if not self.students_to_grade: # self.get_students_to_grade() # if not self.thing_to_grade: # self.get_thing_to_grade() # results = [] # for student in self.thing_to_grade.students: # for tester in self.testers: # result = tester.run_tests(self.thing_to_grade, student) # results.append(result) # self.recorder.record(results) # class ThingToGrade: # """ # Something to be graded: # -- A project, or a module in the project, or a function # in a module in the project, or a collection of these, # along with # -- a list of students (usernames, or perhaps team names) # whose submissions are to be graded. # """ # def __init__(self, # name_of_thing_to_grade=None, course=None, term=None) # """ # If the thing_to_grade is: # -- a list => grade all the things in the list # -- a number N => grade all of the Session N project # -- a (N, M) pair => grade module M in the Session N project # -- a (N, M, s) triple => grade function s in module M # in the Session N project # -- ... # """ # # # TODO: Handle situations other than the above. # self.parse_name(name_of_thing_to_grade) # self.course = course if course else self.default_course() # self.term = term if term else self.current_term() # # # def parse_name(self, name_of_thing_to_grade): # # All these can be a list # self.session # 1, 2, ... # self.project # Session01_..., Session02_..., ... # self.modules # # def default_course(self): # # TODO: Get from a defaults.txt file. # return 'csse120' # # def current_term(self): # # TODO: Use the current date. # return '201510' # # # def all_students(self): # return # # self.students = students # self.function_name = function_name # if recheckout: # self.checkout(True) # # def checkout(self, overwrite=False): # svn.checkout(self.project, self.students)