diff --git a/prep_assignment.py b/prep_assignment.py
index bb359ca942cecdef31d6efa8d9058c5d47b578f7..dbf0fa3e1e0208adbcc5b9432d7a0fa271e296fb 100644
--- a/prep_assignment.py
+++ b/prep_assignment.py
@@ -1,6 +1,6 @@
 import random
 import subprocess
-# from math import ceil, log10
+from math import ceil, log10
 from typing import Tuple
 
 from api.canvas_classes import *
@@ -24,6 +24,7 @@ def get_students() -> Set[CompositeUser]:
 
 
 # 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)),
@@ -35,12 +36,31 @@ def create_pairs(students: Set[CompositeUser], groupset_name: str = 'Unknown Ass
     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]
-    # noinspection PyUnusedLocal
-    confirmation: str
     if preassigned_students:
         print('First we shall confirm pre-assigned partners.')
     else:
@@ -52,7 +72,7 @@ def create_pairs(students: Set[CompositeUser], groupset_name: str = 'Unknown Ass
         potential_pair = []
         pair_number += 1
         print(f'Preparing pair {pair_number}...')
-        student: CompositeUser = preassigned_students.pop()
+        student = preassigned_students.pop()
         potential_pair.append(pair_number)
         potential_pair.append(student)
         potential_partners: Set[str] = student.graylist[groupset_name]
@@ -120,9 +140,10 @@ def create_pairs(students: Set[CompositeUser], groupset_name: str = 'Unknown Ass
         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[2] is None and \
+                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 \
@@ -134,6 +155,7 @@ def create_pairs(students: Set[CompositeUser], groupset_name: str = 'Unknown Ass
                       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:]:
@@ -142,83 +164,81 @@ def create_pairs(students: Set[CompositeUser], groupset_name: str = 'Unknown Ass
     return student_pairs
 
 
-def save_student_roster(students: Set[CompositeUser]):
+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(assignment_number, student_pairs):
-    filename = f'{assignment_number}-pairs.md'
+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 {assignment_number}\n\n')
+        pair_file.write(f'# PARTNERS FOR ASSIGNMENT {groupset_name}\n\n')
         for pair in student_pairs:
-            pair_file.write(f'-   {assignment_number}pair {pair[0]}\n')
+            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(assignment_number, student_pairs, verbose):
-    if verbose:
-        print('Creating file for clone script.')
-    filename = f'{assignment_number}-clone.sh'
+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:
-            if verbose:
-                print(f'Creating repo for pair number {pair[0]}')
+            print(f'\tCreating repo for pair number {str(pair[0]).zfill(zero_padding)}')
             project = GitlabProject.create_project_in_group(Course.gitlab_namespace,
-                                                            f'{assignment_number}pair{pair[0]}')
-            if verbose:
-                print(f'\tAdding {pair[1]}')
+                                                            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())
-            if verbose:
-                print(f'\tAdding {pair[2]}')
+            print(f'\t\tAdding {pair[2]}')
             project.add_user_as_maintainer(pair[2].get_gitlab_user())
             if pair[3] is not None:
-                if verbose:
-                    print(f'\tAdding {pair[3]}')
+                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(assignment_number, student_pairs):
+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(f'{assignment_number}pairs')
+    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'{assignment_number}pair {pair[0]}')
+        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)
-    # zero_padding = ceil(log10(len(partners)))
-    # for partner in partners:
-    #     print(f'{groupset} {str(partner[0]).zfill(zero_padding)}: {partner[1]}\t{partner[2]}\t{partner[3]}')
-    """
-    assignment = '28'
-    pairs = create_pairs('2019-08.csv')
-    save_pairs(assignment, pairs)
-    print('Pairs created')
-
-    create_repositories(assignment, pairs, True)
-    print('Repositories created')
-    create_groups(assignment, pairs)
-    print('Canvas groups created')
-
+    create_contact_list(groupset, partners)
+    print()
+    create_repositories(groupset, partners)
+    print()
+    create_groups(groupset, partners)
+    print()
     print('TODO:\tAdd issues')
     print('\tCommit starter code')
-    print('\tUpdate graylists (also, please update the code to update the graylists)')
-    """