def add_to_classroom(self, course_num, assignment_num, results, return_grade=False): if len(results) > 0: for obj in results: grade = obj['details']['score'] user_id = obj['details']['userId'] sub_id = self.submissions.get(user_id) or obj['sub'] course_id = obj.get( 'courseId', sorted(list(self.course_names.values()))[int(course_num) - 1]) assg_name = self.get_names_from_number( assignment=assignment_num)['assignment'] # Get dict of assignment names and assignment id if self.assignments.get( assg_name ) == None and self.teacher == None: # We're probably running locally self.teacher = Teacher() self.assignments = self.teacher.get_all_assignment_ids( course_id) assg_id = self.assignments.get(assg_name) try: click.echo("Sending results to Classroom..") body = {'draftGrade': grade} if return_grade: body['assignedGrade'] = grade results = self.teacher.grade_submissions( course_id, assg_id, sub_id, body) if results: click.echo( 'Posted in classroom for {}'.format(user_id)) except Exception as e: with open(os.path.join(self.BASE_DIR, "upload_errors.txt"), 'a') as f: f.writelines([user_id, '\n']) print("Could not add to classroom : " + str(e)) return True
def boot(self): """ Connect to API services """ # create a required instances self.teacher = Teacher()
class AutoGrad(object): """ AutoGrad """ def __init__(self): """ Make initialisations here """ cwd = os.getcwd() self.BASE_DIR = cwd self.code_dir = os.path.join(self.BASE_DIR, 'assets', 'code') self.teacher = None self.drive = None self.mailer = None self.sheet = None self.TURNED_IN = "TURNED_IN" self.current_assg = "" self.file_path = os.path.join(self.code_dir, 'temp') self.attachments = [] self.course_ids = [] self.submissions = {} self.assignments = {} self.get_code_assgs = ['3', '4', '5'] self.course_names = { "SuaCode Africa 1": "", "SuaCode Africa 2": "", "SuaCode Africa 3": "", "Coding 2019": "", } self.names_vs_ids = dict.fromkeys(self.course_names.keys(), {}) self.assg_names = { 'Assignment 1 - Make Pong Interface': '', 'Assignment 2 - Move Ball': '', 'Assignment 3 - Bounce Ball': '', 'Assignment 4 - Move Paddles': '', 'Assignment 5 - Add Extra Ball': '', 'Assignment 6 - Add More Balls': '' } self.sub_order = {'1': 0, '2': 1} # self.boot() def __str__(self): click.echo("Starting AutoGrad in this dir: {}".format(self.BASE_DIR)) def boot(self): """ Connect to API services """ # create a required instances self.teacher = Teacher() # self.drive = Drive() # self.mailer = Mail() # self.sheet = Sheets() def download_files(self, files_info): """ Download necessary files to be graded """ click.echo("Downloading assignment files ...") self.drive = Drive() # Connect to Drive c = n = len(files_info) keys = list(self.course_names.keys()) vals = sorted(list(self.course_names.values())) for i, f in enumerate(files_info): while True: self.file_path = self.code_dir + '/%s/%s/%s' % ( self.current_assg, keys[vals.index( f['courseId'])], f['submissionNo']) self.file_path.replace('/', os.sep) os.chdir( self.BASE_DIR ) # Get into root dir everytime you have to save a file to prevent saving deep down a tree if not os.path.exists( self.file_path) and os.getcwd() != self.file_path: os.makedirs(self.file_path) os.chdir(self.file_path) try: new_file_name = f['userId'] + '_' + str( f['submissionNo']) + '_' + f['title'] new_file_dir = new_file_name.split('.pde')[0] old_file_name = f['title'] if os.path.exists( new_file_dir ): # If the file has not been downloaded create a new folder, otherwise recycle the old self.recycle( os.path.join(self.file_path, new_file_dir)) # Clear folder first os.makedirs(new_file_dir) os.chdir(new_file_dir) self.drive.get_file(f['id'], f['title']) os.rename(old_file_name, new_file_name) print("Progress: Downloaded {} out of {} file(s) ".format( i + 1, n), end="\r\n", flush=True) except Exception as e: c -= 1 click.echo( "There was an error downloading. Error: {}".format(e)) break click.echo("Finished downloading {}/{} files".format(c, n)) return c def get_submissions_for_assignment(self, course_number, assignment_number, submission_number): """ Returns TURNED_IN submissions from one assignment of specified course/classroom(s) """ subs_list = [] def has_state_history(s, submission_number): count = 0 eligible = len(s['submissionHistory']) > 1 for history in s['submissionHistory']: if history.get('stateHistory', {}).get('state') == self.TURNED_IN: count += 1 if eligible: # EDITED: Return based on specified if submission_number == count: #== 1: # Return only first submissions. s['submissionNo'] = count return True # elif submission_number > 1 and count >= submission_number: # Return any other submissions made later # s['submissionNo'] = count # return True return False # type cast id assignment_number = int(assignment_number) submission_number = int(submission_number) # Get dict of course name and course id courses = self.teacher.get_all_courses() # get multiple course numbers if given if "," in course_number: course_number = tuple( int(n.strip()) for n in course_number.split(',')) else: course_number = tuple(course_number) # Get course ids for selected courses eg. suacode 1 and 2 try: for ck in courses.keys(): if ck in self.course_names: self.course_names[ck] = courses[ck] # Keeping ids for later vals = sorted(list(self.course_names.values())) course_number = sorted(course_number) if len(course_number) > 1: res = zip(course_number, vals) cn, cv = zip(*res) else: cv = (vals[int(course_number[0]) - 1], ) self.course_ids = cv print(self.course_ids) except Exception as e: click.echo( "Could not get courses. Make sure you type a correct course number.\nTry 'autograd.py --help' for details" ) click.echo("Exception: {}".format(e)) raise for id in self.course_ids: # Get dict of assignment names and assignment id self.assignments = self.teacher.get_all_assignment_ids(id) try: # Get assignment id for selected assignment to be graded assg_name = sorted(list( self.assg_names.keys()))[assignment_number - 1] assg_id = self.assignments[assg_name] self.assg_names[ assg_name] = assg_id # add it to the dict beside the name # This is name of current assignment being graded self.current_assg = assg_name except Exception as e: click.echo( "Could not get assignment {}".format(assignment_number)) print(e) #sys.exit() subs_list += self.teacher.get_student_submissions( id, assg_id, states=[self.TURNED_IN]).get('studentSubmissions', []) turned_in_subs = list( filter(lambda s: has_state_history(s, submission_number), subs_list)) return turned_in_subs def get_files_for_download(self, subs): click.echo("Getting filenames from Google for download ...") def get_PDE(sub): user_id = sub["userId"] course_id = sub["courseId"] sub_no = sub["submissionNo"] sub_id = sub["id"] a = sub["assignmentSubmission"]["attachments"] if a: # Loop through attachments for f in a: final = {} drive_file = f["driveFile"] if drive_file["title"][-4:] == ".pde": self.submissions[ user_id] = sub_id # Store a ref to sub_ids. This came later final["submissionNo"] = sub_no final["subId"] = sub_id final["userId"] = user_id final["courseId"] = course_id final["id"] = drive_file["id"] final["title"] = drive_file["title"] final["driveFile"] = drive_file["alternateLink"] return final else: return {} attachments = list(map(lambda sub: get_PDE(sub), subs)) attachments = list(filter(lambda s: s != None, attachments)) self.attachments = attachments # Attach to instance click.echo("Retrieved {} filename(s)".format(len(attachments))) return attachments def log_to_file(self, info, file_name="temporary.json"): try: f = open(file_name, "w") json.dump(info, f) f.close() return True except: return False def load_names_from_csv(self, course_name): os.chdir(self.BASE_DIR) click.echo("No info for {}. Loading info..\n".format(course_name)) try: with open('assets/students-details/%s.csv' % (course_name)) as csvfile: readCSV = csv.reader(csvfile, delimiter=',') for row in readCSV: student_firstname = f'{row[0]}'.strip() student_lastname = f'{row[1]}'.strip() student_name = f'{row[2]}'.strip() student_id = f'{row[3]}'.strip() student_email = f'{row[4]}'.strip() self.names_vs_ids[course_name][student_id] = { 'student_firstname': student_firstname, 'student_name': student_firstname, 'student_email': student_email } return True except Exception as e: click.echo( "There was an error loading the student details. Error: {}". format(str(e))) return False def get_names_from_number(self, **kwargs): values = {} # Get the assignment name from self.assg_names if "assignment" in list(kwargs.keys()): assignment = int(kwargs.get("assignment")) values["assignment"] = sorted(list( self.assg_names.keys()))[assignment - 1] # Get course name from self.course_names if "course" in list(kwargs.keys()): cn = int(kwargs.get("course")) values["course"] = list(self.course_names.keys())[cn - 1] # Get the user details from the csv if "user_details" in list(kwargs.keys()): user_details = kwargs.get('user_details') user_course = user_details.get('user_course') user_id = user_details.get('user_id') names_loaded_for_particular_course = len( self.names_vs_ids.get(user_course)) > 1 if not names_loaded_for_particular_course: # This runs only the first time in a grading cycle. Subsequent times loads names from memory. self.load_names_from_csv(user_course) def get_detail(course, id, fields): d = {} ids_vs_fields = self.names_vs_ids.get(user_course, {}).get(id) for item in fields: d[item] = ids_vs_fields.get(item) return d details = get_detail(user_course, user_id, ['student_firstname', 'student_email']) values = { 'student_firstname': details['student_firstname'], 'student_email': details['student_email'] } return values def recycle(self, src): r = str(random.randint(0, 1000)) dst = os.path.join(self.BASE_DIR, 'assets', 'recycle', r) try: # There is code to be recycled from last grading if not os.path.exists(dst) and os.path.exists(src): os.makedirs(dst) shutil.move(src, dst) except: pass def grade_files(self, course_num, assignment_num, submission_num, move=True, only=[]): # Run grading for a file click.echo("Grading files...") print("Move after grading is set to {}".format(move)) final = {} all = [] # If there are multiple course numbers get them out into a list if "," in course_num: cns = course_num.split(",") else: cns = list(course_num) # Loop through course numbers and grade for course_number in cns: while True: info = self.get_names_from_number(assignment=assignment_num, course=course_number) os.chdir(self.BASE_DIR) code_dir = 'assets{}code'.format(os.sep) if not os.path.exists(code_dir): os.makedirs(code_dir) fp = os.path.join( code_dir, info['assignment'], info['course'], submission_num) # '115758346753638427969_1_Assignment3') # List all files in dir for root, dirs, files in os.walk(fp): for name in dirs: self.file_path = os.path.join(fp, name) click.echo("Grading {}".format(self.file_path)) assignment_sketch = name + '.pde' sketch_dir = os.path.join( os.getcwd(), 'pong_{}'.format(assignment_num) ) # os.path.join(os.getcwd(), self.file_path) build_dir = os.path.join(sketch_dir, 'build') get_code_dir = os.path.join(sketch_dir, 'get_code') get_code_build = os.path.join(get_code_dir, 'build') sketch_path = os.path.join(self.file_path, assignment_sketch) get_code_cmd = 'processing-java --sketch="{}" --output="{}" --force --run "{}"'\ .format(get_code_dir , get_code_build, sketch_path) processing_cmd = 'processing-java --sketch="{}" --output="{}" --force --run "{}"'\ .format(sketch_dir, build_dir, sketch_path) # Remove first pong_X directory later on when old autograd is phased out # Run getcode to make getters and setters if assignment_num in self.get_code_assgs: print("Parsing and Preparing Code.pde file") results = subprocess.check_output(get_code_cmd, shell=True) output = results.decode("UTF-8") print(output) # Run Processing test and grade try: results = subprocess.check_output(processing_cmd, shell=True) output = results.decode("UTF-8") res = output.replace('Finished.', '') score = res.split()[0] errors = res[3:].replace('[', '').replace(']', '') when = datetime.today().strftime( '%Y-%m-%d-%H:%M:%S') user_id = name.split('_')[0] final = { 'sub': submission_num, 'when': when, 'assignment': info['assignment'], 'details': { 'course': info['course'], 'userId': user_id, 'score': score, 'errors': errors } } all.append(final) p = '_'.join( (course_num, assignment_num, submission_num)) graded = os.path.join(self.BASE_DIR, 'assets', 'graded', p) if not os.path.exists(graded): os.makedirs(graded) if move.lower() == 'true': click.echo( "Moving {} to /assets/graded".format( self.file_path)) shutil.move(self.file_path, graded) except Exception as e: with open( os.path.join(self.BASE_DIR, "grading_errors.txt"), 'a') as f: f.writelines([name.strip('"'), '\n']) print( "There was an error processing the script. Error:" + str(e)) break if all: logged = self.log_to_file(all, "results.json") if logged: self.save_grading_info(course_num, assignment_num, submission_num, only=only) return all def attach_ids(self, *args): p = '_'.join(args) dir = os.path.join(self.BASE_DIR, 'logs', p) results_json = os.path.join(dir, 'results.json') temporary_json = os.path.join(dir, 'temporary.json') if not os.path.exists(temporary_json) or not os.path.exists( results_json): click.echo( "Path: {} or {} may not exist. You must have graded submissions before submitting locally stored results.\ Run auto.py with grade-download".format( temporary_json, results)) return False with open(results_json, 'r') as f, open(temporary_json, 'r') as g: results = json.load(f) temporary = json.load(g) for obj in results: userId = obj['details']['userId'] for item in temporary: if userId == item['userId']: obj['submissionId'] = item['subId'] obj['courseId'] = item['courseId'] del item return results def add_to_classroom(self, course_num, assignment_num, results, return_grade=False): if len(results) > 0: for obj in results: grade = obj['details']['score'] user_id = obj['details']['userId'] sub_id = self.submissions.get(user_id) or obj['sub'] course_id = obj.get( 'courseId', sorted(list(self.course_names.values()))[int(course_num) - 1]) assg_name = self.get_names_from_number( assignment=assignment_num)['assignment'] # Get dict of assignment names and assignment id if self.assignments.get( assg_name ) == None and self.teacher == None: # We're probably running locally self.teacher = Teacher() self.assignments = self.teacher.get_all_assignment_ids( course_id) assg_id = self.assignments.get(assg_name) try: click.echo("Sending results to Classroom..") body = {'draftGrade': grade} if return_grade: body['assignedGrade'] = grade results = self.teacher.grade_submissions( course_id, assg_id, sub_id, body) if results: click.echo( 'Posted in classroom for {}'.format(user_id)) except Exception as e: with open(os.path.join(self.BASE_DIR, "upload_errors.txt"), 'a') as f: f.writelines([user_id, '\n']) print("Could not add to classroom : " + str(e)) return True def add_to_sheets(self): # Return results to sheets pass def send_mail(self, results=[], sub_limit=10): """ Sends mail to the student. Has to be supplied the results object from grade_files() """ if (results and len(results) > 0): # Connect to Mail self.mailer = Mail() for obj in results: while True: #prepare mail message message = "Hi {},\n\nGood job!\nYou can can check your grade now.\n Grade: {}\n\nSee below the things you missed, if any. You can fix them and resubmit only one more time for a better grade by the deadline posted on the classroom page.\nAsk any questions if they aren’t clear. \n\nPlease correct the following mistakes \n\n" grade = int(obj['details']['score']) user_id = obj['details']['userId'] user_course = obj['details']['course'] errors = obj['details']['errors'] assignment = obj['assignment'] user_details = { 'user_id': user_id, 'user_course': user_course } student_details = self.get_names_from_number( user_details=user_details) student_firstname = student_details[ 'student_firstname'] or user_id message = message.format(student_firstname, grade) errors = errors.split(',') message += ''.join( list(map(lambda err: err + '\n', errors))) # upload the results click.echo("Sending mail...") try: no_of_submissions = int(obj.get('sub')) if no_of_submissions < sub_limit: student_email = student_details['student_email'] title = "Results for {}".format(assignment) self.mailer.send_message(student_email, title, message) print("Mail sent successfully to {}".format( student_email)) else: print( "This is a second submission. Did not send mail." ) except HttpError as e: print("Sending mail unsuccessful") error = json.loads(e.content).get('error') print(error) break else: click.echo("Done. There were no results to mail.") return False return True def submit(self, course, assignment, results, return_grade=False): """ Returns draftGrade to classroom and sends mail to the student TODO: Submit to Sheets """ # Return draft grade to classroom if len(results) > 0: added = self.add_to_classroom(course, assignment, results, return_grade) else: return False # Send mail if added: self.send_mail(results=results) else: click.echo("could not add to classroom") return False return True def retrieve(self, course, assignment, submission): """ Connects to Google Drive and downloads files for submissions that have been turned in. Submission number supplied to this method will determine which submission is retrieved eg. '1' for first submission '2' for second submission and '3' , '4',etc for subsequent ones. Additionally it logs to temporary.json the files that were discovered. """ self.boot() # Connect to Google APIs. This is not needed when testing subs = self.get_submissions_for_assignment( course, assignment, submission) # Get turned in submissions at = self.get_files_for_download(subs) # Get the .pde file attachments self.log_to_file( at ) # Logs to temporary.json. You can provide a file name as the second argument for a different file. eg. log_to_file(at, "kofi.json") downloaded = self.download_files( at) # Download the files to assets/code click.echo("Files to be graded are in {}".format(self.file_path)) return True def save_grading_info(self, *args, **kwargs): # when = datetime.today().strftime('%Y-%m-%d-%H:%M:%S').replace(':','-') to_save = ['grading_errors.txt', 'results.json', 'temporary.json'] if kwargs.get('only'): only = kwargs.get('only') to_save = only if args: p = '_'.join(args) else: p = 'temp' dst = os.path.join(self.BASE_DIR, 'logs', p) for file in os.listdir(self.BASE_DIR): if file in to_save and os.path.exists( os.path.join(self.BASE_DIR, file)): src = os.path.join(self.BASE_DIR, file) print("Found " + src) final = os.path.join(dst, file) try: if not os.path.exists(dst): os.makedirs(dst) print("Moving from {} to {}".format(src, dst)) shutil.move(src, dst) elif file == 'results.json' and os.path.exists(final): print("Appending results..") buffer = os.path.join(dst, 'temp.json') with open(src, 'r') as s, open(final, 'r+') as d, open( buffer, 'w') as t: new_results = json.loads(s.read()) old_results = json.loads(d.read()) for obj in new_results: old_results.append(obj) json.dump(old_results, t) shutil.move(buffer, final) self.recycle(src) # Recycle the results.json else: print("Dir exists. Overwriting {} to {}".format( src, dst)) shutil.move(src, final) self.recycle(src) except Exception as e: print("There was an error moving/appending results. {}". format(str(e))) def deploy(self, course, assignment, submission, move, return_grade=False): # Download and store files self.retrieve(course, assignment, submission) # Results of grading. Stored in results.json also results = self.grade_files(course, assignment, submission, move, only=['grading_errors.txt']) # Submit results submitted = self.submit(course, assignment, results, return_grade) # Remove results and temporary, not needed if submitted: self.recycle('results.json') self.recycle('temporary.json')
from classroom.modules.Teacher import Teacher COURSE_TEMPLATE = { 'name': '', 'section': '', 'descriptionHeading': '', 'description': "", 'room': '', 'ownerId': 'me', 'courseState': 'PROVISIONED' } print("[SCRIPTS] Create new course \n") name = input("Enter course name: ") description = input("Enter description: ") COURSE_TEMPLATE['name'] = name COURSE_TEMPLATE['description'] = description try: teacher = Teacher() done = teacher.create_new_course(COURSE_TEMPLATE) print("Course created successfully. \n{}".format(done)) except Exception as e: print("Unable to create course: ", e)
def test_get_all_courses(): return load_json('courses.json') def get_course_name(index, courses={}): names = list(courses.keys()) return names[int(index)] def get_course_id(index, courses={}): ids = list(courses.values()) return ids[int(index)] teacher = Teacher() courses_info = teacher.get_all_courses() save_json(courses_info) # test method # courses_info = test_get_all_courses() # test method while response != NO: print_list_names(courses_info) index = input("Enter index of course: ") chosen = get_course_name(index, courses_info) print("You chose {}\n".format(chosen)) TO.append(chosen) response = input( "Do you want to enter another course name? ({} for yes, {} for no) :". format(YES, NO)) print("Creating assignments for {} using template from {} : ", TO, FROM)
print("Done") """ Things that need to be done """ #map pong code to corres ponding assignment pong_code = { 'Assignment 1 - Make Pong Interface': 'pong_1', 'Assignment 2 - Move ball': 'pong_2', 'Assignment 3 - Bounce Ball': 'pong_3', 'Assignment 4 - Move Paddles': 'pong_4', 'Assignment 5 - Add Extra Ball': 'pong_5', 'Assignment 6 - Add More Balls': 'pong_6' } # create a required instances teacher = Teacher() drive = Drive() mailer = Mail() sheet = Sheets() # Get dict of course name and course id courses = teacher.get_all_courses() """ Grade assignment for each student TODO: optimise the process """ for course in my_courses: course_name = course # Get course id
surname = f'{row[0]}'.strip() firstname = f'{row[1]}'.strip() email = f'{row[2]}'.strip() if (firstname + ' ' + surname) == fullname: f_path = "assets/students-details/%s.csv" % course f = open(f_path, "a+") f.write( "%s, %s, %s, %s, %s, \n" % (firstname, surname, fullname, stu_id, email)) f.close() if __name__ == '__main__': # Login to Classroom teacher = Teacher() courses_info = teacher.get_all_courses() courses = [] YES = "y" NO = "n" response = YES while response != NO: print_list_names(courses_info) index = input("Enter index of course: ") chosen = get_course_name(index, courses_info) print("You chose {}\n".format(chosen)) courses.append(chosen) response = input( "Do you want to enter another course name? ({} for yes, {} for no) :"