Skip to content
Snippets Groups Projects
Select Git revision
  • 218d9b2f8485f9f66100f79316c78d8dcb3d0af1
  • master default protected
2 results

prep_assignment.py

Blame
  • prep_assignment.py 12.41 KiB
    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
    # TODO: manage multiple sections
    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(student_pairs)))
        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(student_pairs)))
        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(student_pairs)))
        for pair in student_pairs:
            print(f'\tCreating group for pair number {str(pair[0]).zfill(zero_padding)}')
            group = group_set.create_group(f'\t{groupset_name} {str(pair[0]).zfill(zero_padding)}')
            group.add_student(pair[1].get_canvas_user())
            print(f'\t\tAdding {pair[1]}')
            group.add_student(pair[2].get_canvas_user())
            print(f'\t\tAdding {pair[2]}')
            if pair[3] is not None:
                group.add_student(pair[3].get_canvas_user())
                print(f'\t\tAdding {pair[3]}')
        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')