diff --git a/Slides/scripts/validate_groups.py b/Slides/scripts/validate_groups.py index 9f01a8e..834394c 100755 --- a/Slides/scripts/validate_groups.py +++ b/Slides/scripts/validate_groups.py @@ -1,341 +1,341 @@ #!/usr/bin/env python3 import os import subprocess import Slides.mail_helper as mailer import argparse import getpass import random import Slides.class_helper as ch def main(): config = ch.get_class_config() parser = argparse.ArgumentParser( description='Validate groups for homeworks') parser.add_argument('-n', '--homework', type=str, help='specify the homework label', required=True) parser.add_argument('-t', '--target', type=str, help='Specify the name of a mail box' ' to send test messages, ' f'default : {config["teachers"][0]}', default=config['teachers'][0]) parser.add_argument('-r', '--report', action='store_true', help='Generate a org-mode file ready to grade') parser.add_argument('-u', '--username', type=str, help='Username to log to the SMTP server', default=None) parser.add_argument('-c', '--clone', action='store_true', help='Tries to clone the repository of the group', default=None) args = parser.parse_args() students_list = config['students'] students_list.rename(columns={"e-Mail": "email"}, inplace=True) students_list = students_list.applymap( lambda x: x.strip() if isinstance(x, str) else x) students_list["in_group"] = False students_list.set_index("email", inplace=True) homework = config['homeworks'][args.homework] group_list = homework['groups'] group_list.rename(columns={"Student #1 EPFL email": "email1", "Student #2 EPFL email": "email2", "Homeworks repository": "repository" }, inplace=True) group_list = group_list.applymap( lambda x: x.strip() if isinstance(x, str) else x) target = args.target username = args.username password = None if username: print('login:', username) password = getpass.getpass() # ############################################################### # generate group keys def make_group_keys(group): emails = [group.email1, group.email2] emails.sort() if len(emails) < 2: raise RuntimeError( 'invalid group: ' + str(emails)) print(emails) key = '_'.join(emails) return key group_keys = group_list.apply(make_group_keys, axis=1) group_list['group_key'] = group_keys group_list.set_index('group_key', inplace=True) # ############################################################### def run_command(cmd, cwd=None): if cwd is None: cwd = os.getcwd() process = subprocess.Popen(cmd, shell=True, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) output = process.communicate() output = [i.decode().strip() for i in output if i.decode().strip() != ''] output = '\n'.join(output) ret = process.returncode return ret, output # ############################################################### # search for failing to clone groups try: os.mkdir(os.path.join(config['git_root'], 'homeworks')) except Exception: pass def clone_group_repo(group): dirname = os.path.join( config['git_root'], 'homeworks', args.homework+".repositories", group.name) if os.path.isdir(dirname): ret = 0 else: clone_cmd = group.repository + ' ' + dirname ret, out = run_command(clone_cmd) if ret == 0: print('Cloning_Success: group ' + group.name) else: print('Cloning_Failure: group ' + group.name) return ret == 0, None, None cmd = 'git rev-parse --abbrev-ref origin/HEAD' ret, out = run_command(cmd, cwd=dirname) if not ret == 0: print('No HEAD yet: group ' + group.name) return ret == 0, None, None remote, master = out.split('/')[:2] cmd = ('git rev-list -n 1 --first-parent' f' --before="{homework["deadline"]}" {master}') ret, rev = run_command(cmd, cwd=dirname) cmd = f'git checkout {rev}' ret, out = run_command(cmd, cwd=dirname) if ret != 0: print(f"Failed to get revision: \n\n{out}") return ret == 0, master, rev if args.clone: res = group_list.apply( clone_group_repo, axis=1, result_type='expand') group_list['clone_success'] = res[0] group_list['master'] = res[1] group_list['revision'] = res[2] # ############################################################### # construct invalid groups def construct_valid_groups(group): # check if in student list try: count1 = students_list.loc[group.email1].size count2 = students_list.loc[group.email2].size is_valid = (count1 > 0 and count2 > 0) except KeyError: is_valid = False if is_valid: students_list.loc[group.email1, 'in_group'] = True students_list.loc[group.email2, 'in_group'] = True return is_valid valid_group = group_list.apply(construct_valid_groups, axis=1) group_list['valid_group'] = valid_group ################################################################ # generate org report file def append_group_in_report(f, group): f.write(f"* {group.name}\n") f.write(f"** {group.revision}\n\n") if 'report_template' in homework: f.write(homework['report_template']) elif 'report_template' in config: f.write(config['report_template']) f.write("\n") if args.report: fname = os.path.join(config['git_root'], 'homeworks', args.homework+'.report.org') if os.path.exists(fname): print( f"Warning: will not overwrite the report file: delete it manually ({fname})") else: f = open(fname, 'w') group_list.apply(lambda g: append_group_in_report(f, g), axis=1) f.close() ################################################################ # make random groups unregistered_list = [ e.name for k, e in students_list.query('in_group==False').iterrows()] random.shuffle(unregistered_list) if len(unregistered_list): if len(unregistered_list) % 2 == 0: random_groups = [e for e in zip( unregistered_list[::2], unregistered_list[1::2])] else: random_groups = [e for e in zip( unregistered_list[:-1:2], unregistered_list[1:-1:2])] random_groups[-1] = random_groups[-1][0], random_groups[-1][1], unregistered_list[-1] random_groups = [" - {0}".format(", ".join(b)) for b in random_groups] random_groups = '\n\n'.join(random_groups) ################################################################ # output info print('\ntotal_students:', len(students_list)) print('registered_students:', len(students_list.query("in_group==True"))) print('unregistered_students:', len(students_list.query("in_group==False"))) print('invalid_groups:', len(group_list.query("valid_group==False"))) ################################################################ # sending emails # send emails o failed clones def send_failed_clone_email(group): mailer.mail( username=username, password=password, sender_email=target, subject='SP4E homeworks: error in cloning your project', copy_emails=config['teachers']+config['assistants'], message=f""" Dear Students, Apparently we cannot clone your repository: {group.repository} Please fix the permissions. Best regards, The teaching team. """, target_emails=[group.email1, group.email2], markdown=True ) def send_invalid_groups(group): mailer.mail( username=username, password=password, sender_email=target, subject=f'{config["acronym"]} homeworks: invalid group', message=""" Dear Students, Your group is composed with at least a student not officially registered for the class. Therefore I have to ask you to change the composition of the group. You have to understand that the grading of so many projects is a lot of work. Therefore we will do it only for the registered students. With my best regards, The teaching team. """, copy_emails=config['teachers']+config['assistants'], target_emails=[group.email1, group.email2], markdown=True ) # send email to unregistered people def send_unregistered(): if 'group_form' in homework: group_form = homework["group_form"] else: group_form = config["group_form"] if 'unregistered_student_message' in config: msg = config['unregistered_student_message'] else: msg = """ - Dear Students, +Dear Students, - Apparently you did not register yet to any group. - Several reasons might explain this. +Apparently you did not register yet to any group. +Several reasons might explain this. - If it happens that you need to find a pair, please find below the - automatically created groups. +If it happens that you need to find a pair, please find below the +automatically created groups. {random_groups} - If the situation does not suit you, please inform us as quickly as - possible. +If the situation does not suit you, please inform us as quickly as +possible. - You still have to create a repository to store your homework. - Please inform us of your repository URI, for instance by filling the - [form]({group_form}) +You still have to create a repository to store your homework. +Please inform us of your repository URI, for instance by filling the +[form]({group_form}) - With my best regards, +With my best regards, - The teaching team. +The teaching team. """ mailer.mail( username=username, password=password, sender_email=target, subject=f'{config["acronym"]} homeworks: not registered in a group', message=msg.format(random_groups=random_groups, unregistered_list=unregistered_list, group_form=group_form), copy_emails=config['teachers']+config['assistants'], target_emails=unregistered_list, markdown=True ) if username: if args.clone: group_list.query("clone_success==False").apply( send_failed_clone_email, axis=1) group_list.query("valid_group==False").apply( send_invalid_groups, axis=1) if len(unregistered_list) > 0: send_unregistered() ################################################################ if __name__ == "__main__": main() diff --git a/pyproject.toml b/pyproject.toml index 1c42a6f..6d8840a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,57 +1,57 @@ [tool.poetry] name = "slides" version = "0.0.1" description = "Small tool utility to manage classes and presentations" authors = ["Guillaume Anciaux "] license = "GPL" readme = "README.md" packages = [{include = "Slides"}] homepage = "https://gitlab.com/ganciaux/slides" repository = "https://gitlab.com/ganciaux/slides" [tool.poetry.dependencies] python = ">=3.8" pandas = "^2.0.0" gitpython = "^3.1.31" pyyaml = "^6.0" jupyter = "^1.0.0" -notebook = ">6.4.12" +notebook = ">6.4.12, <7" sympy = "^1.11.1" pypdf2 = "^3.0.1" matplotlib = ">=3.6.0" wand = "^0.6.11" dill = "^0.3.6" jupyter-contrib-nbextensions = "^0.7.0" pyparsing = "^3.0.9" markdown = "^3.4.3" pep8 = "^1.7.1" python-pptx = "^0.6.21" bibtexparser = "^1.4.0" osmpythontools = "^0.3.5" ipyleaflet = "^0.17.3" googlemaps = "^4.10.0" [tool.poetry.group.dev.dependencies] pytest = "^7.2.1" [build-system] requires = ["poetry-core", "poetry-dynamic-versioning"] build-backend = "poetry_dynamic_versioning.backend" [tool.poetry-dynamic-versioning] enable = true metadata = false dirty = true vcs = "git" style = "semver" latest-tag = true [tool.poetry.scripts] slides = { callable = "Slides:scripts.slides.main"} organize_image = { callable = "Slides:scripts.organize_image.main"} pLatex = { callable = "Slides:scripts.pLatex.main"} email2class = { callable = "Slides:scripts.email2class.main"} validate_groups = { callable = "Slides:scripts.validate_groups.main"} send_feedbacks = { callable = "Slides:scripts.send_feedbacks.main"} randomly-pick-student = { callable = "Slides:scripts.randomly_pick_student.main"} pptx2slides = { callable = "Slides:scripts.pptx2slides.main"}