import random import subprocess from math import ceil, log10 from typing import Tuple from api.canvas_classes import * from api.gitlab_classes import * from api.composite_user import CompositeUser from course import Course def get_students() -> Set[CompositeUser]: filename: str students: Set[CompositeUser] file_not_found = True while file_not_found: try: filename = input('Please provide the name of the existing student roster file: ') students = CompositeUser.read_student_csv(filename) file_not_found = False except FileNotFoundError: print(f'File "{filename}" not found.') return students # TODO: assign_partners for arbitrarily-sized teams # TODO: break this up into bite-sized chunks def create_pairs(students: Set[CompositeUser], groupset_name: str = 'Unknown Assignment') -> \ List[Tuple[int, CompositeUser, CompositeUser, Optional[CompositeUser]]]: students_with_blacklist: Set[CompositeUser] = sorted(list(filter(lambda s: s.has_blacklist(), students)), key=lambda t: len(t.blacklist), reverse=True) preassigned_students: Set[CompositeUser] = set() random_student = students.pop() # TODO: remove duplication fields = random_student.graylist.keys() students.add(random_student) if groupset_name in fields: preassigned_students = set(filter(lambda s: len(s.graylist[groupset_name]) > 0, students)) unassigned_students: Set[CompositeUser] = set(students) # handle student excusals excused_students = set(filter(lambda s: s.graylist[groupset_name] == set('X'), preassigned_students)) # noinspection PyUnusedLocal confirmation: str # noinspection PyUnusedLocal student: CompositeUser for student in excused_students: confirmation = input(f'\tExclude {student.readable_name} from student pairing ([Yes]/No/Abort)? ') if confirmation == "" or confirmation.upper()[0] == 'Y': print(f'Removing {student.readable_name} from pre-assigned students and from unassigned students.') preassigned_students.remove(student) unassigned_students.remove(student) student.graylist[groupset_name] = set() elif confirmation.upper()[0] == 'N': print(f'Removing {student.readable_name} from pre-assigned students but leaving in unassigned students.') preassigned_students.remove(student) student.graylist[groupset_name] = set() else: print('Aborting.') exit(0) # handle truly-preassigned students pair_number: int = 0 student_pairs: List[Tuple[int, CompositeUser, CompositeUser, Optional[CompositeUser]]] = [] # noinspection PyUnusedLocal potential_pair: List[CompositeUser] if preassigned_students: print('First we shall confirm pre-assigned partners.') else: confirmation = input(f'There are no pre-assigned students for {groupset_name}. Proceed ([Yes]/Abort)? ') if not (confirmation == "" or confirmation.upper()[0] == 'Y'): print('Aborting.') exit(0) while preassigned_students: potential_pair = [] pair_number += 1 print(f'Preparing pair {pair_number}...') student = preassigned_students.pop() potential_pair.append(pair_number) potential_pair.append(student) potential_partners: Set[str] = student.graylist[groupset_name] print(f'\t{student.readable_name} ({student.canvas_username}) pre-assigned to {potential_partners}') while potential_partners: student = CompositeUser.get_user(potential_partners.pop()) potential_pair.append(student) partner_potential_partners: Set[str] = student.graylist[groupset_name] print(f'\t{student.readable_name} ({student.canvas_username}) pre-assigned to {partner_potential_partners}') confirmation = input('Confirm partners [Yes]/No/Abort? ') if confirmation == "" or confirmation.upper()[0] == 'Y': # noinspection PyUnusedLocal pair: Tuple[int, CompositeUser, CompositeUser, Optional[CompositeUser]] if len(potential_pair) == 3: pair = (potential_pair[0], potential_pair[1], potential_pair[2], None) else: pair = (potential_pair[0], potential_pair[1], potential_pair[2], potential_pair[3]) student_pairs.append(tuple(pair)) for student in potential_pair[1:]: unassigned_students.remove(student) if student in preassigned_students: preassigned_students.remove(student) elif confirmation.upper()[0] == 'N': print('We\'re not accepting NO for an answer yet. Goodbye.') # TODO: accept NO for an answer exit(1) else: print('Aborting.') exit(0) if students_with_blacklist: print('Next we shall assign partners to students with blacklists.') else: print('There are no students with blacklists.') for student in students_with_blacklist: pair_number += 1 unassigned_students.remove(student) potential_partner: CompositeUser = random.choice(tuple(unassigned_students)) while not (student.is_blacklist_compatible(potential_partner) and student.is_graylist_compatible(potential_partner)): # has the potential to run infinitely potential_partner = random.choice(tuple(unassigned_students)) unassigned_students.remove(potential_partner) student_pairs.append((pair_number, student, potential_partner, None)) print(f'\t{student.readable_name} partnered with {potential_partner.readable_name}') print('Finally we shall assign partners to the remaining students.') odd_student: Optional[CompositeUser] = \ random.choice(tuple(unassigned_students)) if len(unassigned_students) % 2 == 1 else None if odd_student is not None: unassigned_students.remove(odd_student) while unassigned_students: pair_number += 1 student = random.choice(tuple(unassigned_students)) unassigned_students.remove(student) attempts = 1 potential_partner = random.choice(tuple(unassigned_students)) while not (student.is_graylist_compatible(potential_partner) and attempts <= len(unassigned_students)): attempts += 1 potential_partner = random.choice(tuple(unassigned_students)) if attempts > len(unassigned_students): print(f'NO MATCH POSSIBLE FOR {student}! YOU REALLY SHOULD WRITE CODE TO SWAP PARTNERS.') exit(1) # TODO: write code to swap partners unassigned_students.remove(potential_partner) student_pairs.append((pair_number, student, potential_partner, None)) print(f'\t{student.readable_name} partnered with {potential_partner.readable_name}') if odd_student is not None: print('Very finally, we shall now assign the odd student to an existing partnership.') match_found = False while not match_found: pair_number -= 1 potential_partners: Tuple[int, CompositeUser, CompositeUser, Optional[CompositeUser]] = \ student_pairs[pair_number] if potential_partners[3] is None and \ odd_student.is_graylist_compatible(potential_partners[1]) and \ odd_student.is_graylist_compatible(potential_partners[2]) and \ odd_student.is_blacklist_compatible(potential_partners[1]) and \ odd_student.is_blacklist_compatible(potential_partners[2]): match_found = True student_pairs[pair_number] = ( potential_partners[0], potential_partners[1], potential_partners[2], odd_student) print(f'\t{odd_student.readable_name} partnered with ' f'{potential_partners[1].readable_name} and {potential_partners[2].readable_name}') else: print('There is no odd student to add to an existing partnership.') # update graylists for pair in student_pairs: usernames = set(map(lambda s: s.canvas_username if isinstance(s, CompositeUser) else None, pair)) - {None} for student in pair[1:]: if student is not None: student.assign_partners(groupset_name, usernames - {student.canvas_username}) return student_pairs def save_student_roster(students: Set[CompositeUser]) -> None: filename = input('Please provide the name of the new student roster file: ') print(f'Writing {filename}.') CompositeUser.write_student_csv(students, filename) def create_contact_list(groupset_name: str, student_pairs: List[Tuple[int, CompositeUser, CompositeUser, Optional[CompositeUser]]]) -> None: filename = f'{groupset_name}-partners.md' print(f'Writing {filename}.') zero_padding: int = ceil(log10(len(partners))) with open(filename, mode='w') as pair_file: pair_file.write(f'# PARTNERS FOR ASSIGNMENT {groupset_name}\n\n') for pair in student_pairs: pair_file.write(f'- {groupset_name} {str(pair[0]).zfill(zero_padding)}\n') pair_file.write(f' - {pair[1]}\n') pair_file.write(f' - {pair[2]}\n') if pair[3] is not None: pair_file.write(f' - {pair[3]}\n') def create_repositories(groupset_name: str, student_pairs: List[Tuple[int, CompositeUser, CompositeUser, Optional[CompositeUser]]]) -> None: filename = f'{groupset_name}-clone.sh' print(f'Creating file for clone script: {filename}. Creating repositories on Gitlab.') zero_padding: int = ceil(log10(len(partners))) with open(filename, mode='w') as clone_file: clone_file.write('#!/bin/bash\n\n') clone_file.write('# Auto-generated clone script.\n') for pair in student_pairs: print(f'\tCreating repo for pair number {str(pair[0]).zfill(zero_padding)}') project = GitlabProject.create_project_in_group(Course.gitlab_namespace, f'{groupset_name}{str(pair[0]).zfill(zero_padding)}') print(f'\t\tAdding {pair[1]}') project.add_user_as_maintainer(pair[1].get_gitlab_user()) print(f'\t\tAdding {pair[2]}') project.add_user_as_maintainer(pair[2].get_gitlab_user()) if pair[3] is not None: print(f'\t\tAdding {pair[3]}') project.add_user_as_maintainer(pair[3].get_gitlab_user()) repo_url = project.get_cloning_url() clone_file.write(f'git clone {repo_url}\n') subprocess.call(['chmod', '+x', filename]) print('Repositories created') def create_groups(groupset_name: str, student_pairs: List[Tuple[int, CompositeUser, CompositeUser, Optional[CompositeUser]]]) -> None: print(f'Creating groupset {groupset_name} and student groups on Canvas.') course = CanvasCourse(Course.canvas_course_id) group_set = course.create_user_groupset(groupset_name) zero_padding: int = ceil(log10(len(partners))) for pair in student_pairs: group = group_set.create_group(f'\t{groupset_name} {str(pair[0]).zfill(zero_padding)}') group.add_student(pair[1].get_canvas_user()) group.add_student(pair[2].get_canvas_user()) if pair[3] is not None: group.add_student(pair[3].get_canvas_user()) print('Canvas groups created') if __name__ == '__main__': groupset: str = input('Please provide the name of the student groupset: ') student_set: Set[CompositeUser] = get_students() # TODO: check for changes to course roster # TODO: check for changes to gitlab usernames partners: List[Tuple[int, CompositeUser, CompositeUser, Optional[CompositeUser]]] = create_pairs(student_set, groupset) print() save_student_roster(student_set) create_contact_list(groupset, partners) print() create_repositories(groupset, partners) print() create_groups(groupset, partners) print() print('TODO:\tAdd issues') print('\tCommit starter code')