From c6c64c0420dbf4f50c95a5db85384f8f60c227b0 Mon Sep 17 00:00:00 2001
From: Christopher Bohn <bohn@unl.edu>
Date: Fri, 20 Sep 2019 10:00:42 -0500
Subject: [PATCH] Wrote code to prepare an assignment.

This first iteration is limited to working with an even number of
students, does not yet add Issues to the Gitlab Issue Tracker, and
does not yet update the graylists in the student CSV.
---
 api/canvas_experiments.py | 10 ++++
 api/course.py             |  3 +-
 canvas_classes.py         | 20 ++++----
 composite_user.py         | 27 +++++++----
 gitlab_classes.py         | 20 ++++----
 prep_assignment.py        | 97 +++++++++++++++++++++++++++++++++++++++
 6 files changed, 147 insertions(+), 30 deletions(-)
 create mode 100644 prep_assignment.py

diff --git a/api/canvas_experiments.py b/api/canvas_experiments.py
index 4719dbc..d4c0fa5 100644
--- a/api/canvas_experiments.py
+++ b/api/canvas_experiments.py
@@ -42,6 +42,7 @@ if __name__ == '__main__':
     recipients = canvas.search_recipients()
     print(recipients)
     """
+    """
     course = canvas.get_course(Course.canvas_course_id)
     users = course.get_users()
     for user in users:
@@ -63,3 +64,12 @@ if __name__ == '__main__':
     users = foo2.get_users()
     for user in users:
         print(user.name)
+    """
+    user = canvas.get_user(30266045, 'sis_user_id')     # can retrieve my own user
+    print(user)
+    # user = canvas.get_user(76390201, 'sis_user_id')     # cannot retrieve arbitrary user
+    # print(user)
+    users = canvas.get_course(Course.canvas_course_id).get_users()
+    # user = list(filter(lambda s: s.sis_user_id == 76390201, users))[0]
+    user = list(filter(lambda s: s.login_id == 'mkluck2', users))[0]
+    print(user)
\ No newline at end of file
diff --git a/api/course.py b/api/course.py
index 5a907ff..b2ae0d9 100644
--- a/api/course.py
+++ b/api/course.py
@@ -1,6 +1,7 @@
 class Course:
     # GitLab course information
-    gitlab_namespace = 'csce_361/sandbox'
+    # gitlab_namespace = 'csce_361/sandbox'
+    gitlab_namespace = 'csce_361/fall2019'
 
     # Canvas course information
     # canvas_course_id = '73696'  # Software Engineering Sandbox
diff --git a/canvas_classes.py b/canvas_classes.py
index 03f52a9..3b639de 100644
--- a/canvas_classes.py
+++ b/canvas_classes.py
@@ -40,7 +40,7 @@ class CanvasUser:
 
     def __repr__(self):
         username = self.get_username()
-        return f'@{username}'
+        return f'{username}'
 
     def __eq__(self, other):
         if isinstance(other, CanvasUser):
@@ -102,7 +102,7 @@ class CanvasUser:
 """
 
 
-class GroupSet:     # aka, group_category
+class CanvasGroupSet:     # aka, group_category
     def __init__(self, group_category):
         super().__init__()
         self.canvas_group_category = group_category
@@ -114,19 +114,19 @@ class GroupSet:     # aka, group_category
         canvas_groups = self.canvas_group_category.get_groups()
         groups = []
         for group in canvas_groups:
-            groups.append(Group(group))
+            groups.append(CanvasGroup(group))
         return groups
 
     def create_group(self, group_name):
         canvas_group = self.canvas_group_category.create_group(name=group_name)
-        return Group(canvas_group)
+        return CanvasGroup(canvas_group)
 
     def create_groups(self, number_of_groups):
         base_name = self.get_name()
         groups = []
         for group_number in range(1, number_of_groups+1):
             canvas_group = self.create_group(f'{base_name} {group_number}')
-            groups.append(Group(canvas_group))
+            groups.append(CanvasGroup(canvas_group))
         return groups
 
     def __repr__(self):
@@ -178,7 +178,7 @@ class GroupSet:     # aka, group_category
 """
 
 
-class Group:
+class CanvasGroup:
     def __init__(self, group):
         super().__init__()
         self.canvas_group = group
@@ -261,7 +261,7 @@ class Group:
 """
 
 
-class Course:
+class CanvasCourse:
     def __init__(self, course_id):
         self.canvas_course = CanvasSession.get_session().get_course(course_id)
 
@@ -290,19 +290,19 @@ class Course:
         canvas_groups = self.canvas_course.get_groups()
         groups = []
         for group in canvas_groups:
-            groups.append(Group(group))
+            groups.append(CanvasGroup(group))
         return groups
 
     def get_group_sets(self):
         canvas_group_categories = self.canvas_course.get_group_categories()
         group_sets = []
         for group_category in canvas_group_categories:
-            group_sets.append(GroupSet(group_category))
+            group_sets.append(CanvasGroupSet(group_category))
         return group_sets
 
     def create_groupset(self, groupset_name):
         group_category = self.canvas_course.create_group_category(groupset_name)
-        return GroupSet(group_category)
+        return CanvasGroupSet(group_category)
 
     def __repr__(self):
         return f'{self.canvas_course.course_code}: {self.canvas_course.name}'
diff --git a/composite_user.py b/composite_user.py
index bf2d485..802127e 100644
--- a/composite_user.py
+++ b/composite_user.py
@@ -1,5 +1,7 @@
 from canvas_classes import CanvasUser
+from canvas_classes import CanvasCourse
 from gitlab_classes import GitlabUser
+from course import Course
 import csv
 
 NO_PARTNERING_LIST_MAXIMUM = 10
@@ -13,7 +15,7 @@ class CompositeUser:
         self.gitlab_user = None
         self.sortable_name = student_dictionary['SortableName']
         self.readable_name = student_dictionary['ReadableName']
-        self.NUID = student_dictionary['NUID']
+        self.NUID = int(student_dictionary['NUID'])
         self.canvas_username = student_dictionary['CanvasUsername']
         self.gitlab_username = student_dictionary['GitlabUsername']
         self.canvas_email = student_dictionary['CanvasEmail']
@@ -28,12 +30,12 @@ class CompositeUser:
 
     def get_canvas_user(self):
         if self.canvas_user is None:
-            self.canvas_user = CanvasUser(self.NUID)
+            self.canvas_user = CanvasUser(self.NUID)    # n.b., can retrieve own user but not arbitrary user
         return self.canvas_user
 
     def get_gitlab_user(self):
         if self.gitlab_user is None:
-            self.gitlab_user = GitlabUser(self.NUID)
+            self.gitlab_user = GitlabUser(self.gitlab_username)
         return self.gitlab_user
 
     def tentatively_pair_with(self, username):
@@ -52,10 +54,10 @@ class CompositeUser:
         return len(self.blacklist) > 0
 
     def is_blacklist_compatible(self, other):
-        return other not in self.blacklist
+        return other not in self.blacklist and self not in other.blacklist
 
     def is_graylist_compatible(self, other):
-        return other not in self.graylist
+        return other not in self.graylist and self not in other.graylist
 
     def __repr__(self):
         if self.canvas_email == self.gitlab_email:
@@ -72,6 +74,9 @@ class CompositeUser:
     def __ne__(self, other):
         return not self.__eq__(other)
 
+    def __hash__(self) -> int:
+        return hash(self.canvas_username)
+
     @staticmethod
     def get_user(username_or_email):
         return CompositeUser.instances[username_or_email]
@@ -81,18 +86,22 @@ class CompositeUser:
         students = set()
         with open(filename, mode='r') as csv_file:
             csv_reader = csv.DictReader(csv_file)
-            for student in csv_reader:
+            for csv_student in csv_reader:
                 graylist = set()
                 blacklist = set()
                 for count in range(NO_PARTNERING_LIST_MAXIMUM):
-                    former_partner = student[f'Graylist{count}']
-                    undesired_partner = student[f'Blacklist{count}']
+                    former_partner = csv_student[f'Graylist{count}']
+                    undesired_partner = csv_student[f'Blacklist{count}']
                     if former_partner != "":
                         graylist.add(former_partner)
                     if undesired_partner != "":
                         blacklist.add(undesired_partner)
-                student = CompositeUser(student, graylist, blacklist)
+                student = CompositeUser(csv_student, graylist, blacklist)
                 students.add(student)
+        canvas_students = CanvasCourse(Course.canvas_course_id).get_students()
+        for canvas_student in canvas_students:
+            composite_student = list(filter(lambda s: s.canvas_username == canvas_student.get_username(), students))[0]
+            composite_student.canvas_user = canvas_student
         return students
 
     @staticmethod
diff --git a/gitlab_classes.py b/gitlab_classes.py
index cc3d2cf..18ee72c 100644
--- a/gitlab_classes.py
+++ b/gitlab_classes.py
@@ -55,7 +55,7 @@ class GitlabUser:
         return not self.__eq__(other)
 
 
-class Issue:
+class GitlabIssue:
     def __init__(self, issue):
         """
         Creates an Issue object, populating the backing git_issue instance with the appropriate gitlab.Issue object
@@ -168,7 +168,7 @@ class Issue:
     # subscribed
 
 
-class Project:
+class GitlabProject:
     def __init__(self, project):
         """
         Creates a Project object, populating the backing git_project instance with the appropriate gitlab.Project object
@@ -196,7 +196,7 @@ class Project:
             gitlab_projects = GitlabSession.get_session().groups.get(group).projects.list(all=True)
         projects = []
         for project in gitlab_projects:
-            projects.append(Project(project))
+            projects.append(GitlabProject(project))
         return projects
 
     @staticmethod
@@ -204,17 +204,17 @@ class Project:
         gitlab_projects = GitlabSession.get_session().projects.list(search=search_term, all=True)
         projects = []
         for project in gitlab_projects:
-            projects.append(Project(project))
+            projects.append(GitlabProject(project))
         return projects
 
     @staticmethod
     def create_project(project_name):
-        return GitlabSession.get_session().projects.create({'name': project_name})
+        return GitlabProject(GitlabSession.get_session().projects.create({'name': project_name}))
 
     @staticmethod
     def create_project_in_group(group_name, project_name):
         group_id = GitlabSession.get_session().groups.get(group_name).id
-        return GitlabSession.get_session().projects.create({'name': project_name, 'namespace_id': group_id})
+        return GitlabProject(GitlabSession.get_session().projects.create({'name': project_name, 'namespace_id': group_id}))
 
     def get_project_id(self):
         return self.git_project.id
@@ -312,12 +312,12 @@ class Project:
         gitlab_issues = self.git_project.issues.list(order_by='created_at', sort='asc', all=True)
         issues = []
         for issue in gitlab_issues:
-            issues.append(Issue(issue))
+            issues.append(GitlabIssue(issue))
         return issues
 
     def create_issue(self, title, description):
         gitlab_issue = self.git_project.issues.create({'title': title, 'description': description})
-        return Issue(gitlab_issue)
+        return GitlabIssue(gitlab_issue)
 
     def __repr__(self):
         return self.get_name_with_namespace()
@@ -373,7 +373,7 @@ class Project:
 
 if __name__ == '__main__':
     namespace = 'csce_361/sandbox'
-    test_projects = Project.get_projects_by_group(namespace)
+    test_projects = GitlabProject.get_projects_by_group(namespace)
     print('All projects in sandbox:')
     for test_project in test_projects:
         print(test_project)
@@ -391,7 +391,7 @@ if __name__ == '__main__':
     for test_issue in test_issues:
         creation = test_issue.get_created_at()
         print(f'{test_issue}\tcreated at {creation}.')
-    test_projects = Project.get_projects_by_keyword('csce361-homework')
+    test_projects = GitlabProject.get_projects_by_keyword('csce361-homework')
     number_of_projects = len(test_projects)
     print(f'retrieved {number_of_projects} projects matching \'csce361-homework\'')
     start_date = datetime(2019, 8, 1, tzinfo=timezone.utc)
diff --git a/prep_assignment.py b/prep_assignment.py
new file mode 100644
index 0000000..edf24f6
--- /dev/null
+++ b/prep_assignment.py
@@ -0,0 +1,97 @@
+import random
+import subprocess
+from composite_user import CompositeUser
+from canvas_classes import *
+from gitlab_classes import *
+from course import Course
+
+
+def create_pairs(filename):
+    # only works when there are an even number of students
+    students = CompositeUser.read_student_csv(filename)
+    students_with_blacklist = sorted(list(filter(lambda s: s.has_blacklist(), students)),
+                                     key=lambda t: len(t.blacklist), reverse=True)
+    unassigned_students = set(students)
+    pair_number = 0
+    pairs = []
+    for student in students_with_blacklist:
+        pair_number += 1
+        unassigned_students.remove(student)
+        potential_partner = 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)
+        pairs.append((pair_number, student, potential_partner))
+    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))
+        unassigned_students.remove(potential_partner)
+        pairs.append((pair_number, student, potential_partner))
+    return pairs
+
+
+def save_pairs(assignment_number, student_pairs):
+    filename = f'{assignment_number}-pairs.md'
+    with open(filename, mode='w') as pair_file:
+        pair_file.write(f'# PARTNERS FOR ASSIGNMENT {assignment_number}\n\n')
+        for pair in student_pairs:
+            pair_file.write(f'-   {assignment_number}pair {pair[0]}\n')
+            pair_file.write(f'    -   {pair[1]}\n')
+            pair_file.write(f'    -   {pair[2]}\n')
+
+
+def create_repositories(assignment_number, student_pairs):
+    filename = f'{assignment_number}-clone.sh'
+    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:
+            project = GitlabProject.create_project_in_group(Course.gitlab_namespace, f'{assignment_number}pair{pair[0]}')
+            project.add_user_as_maintainer(pair[1].get_gitlab_user())
+            project.add_user_as_maintainer(pair[2].get_gitlab_user())
+            repo_url = project.get_cloning_url()
+            clone_file.write(f'git clone {repo_url}\n')
+    subprocess.call(['chmod', '+x', filename])
+
+
+def create_groups(assignment_number, student_pairs):
+    course = CanvasCourse(Course.canvas_course_id)
+    group_set = course.create_groupset(f'{assignment_number}pairs')
+    for pair in student_pairs:
+        group = group_set.create_group(f'{assignment_number}pair {pair[0]}')
+        group.add_student(pair[1].get_canvas_user())
+        group.add_student(pair[2].get_canvas_user())
+
+
+if __name__ == '__main__':
+    assignment = 10
+    pairs = create_pairs('2019-08.csv')
+    save_pairs(assignment, pairs)
+    print('Pairs created')
+
+    # pair = list(pairs)[0]
+    # print(f'{pair[1].get_gitlab_user()}, {pair[2].get_gitlab_user()}')
+    # project = GitlabProject.create_project_in_group(Course.gitlab_namespace, f'{assignment_number}pairx{pair[0]}')
+    # project.add_user_as_maintainer(pair[1].get_gitlab_user())
+    # project.add_user_as_maintainer(pair[2].get_gitlab_user())
+
+    create_repositories(assignment, pairs)
+    print('Repositories created')
+    create_groups(assignment, pairs)
+    print('Canvas groups created')
+
+    print('TODO:\tAdd issues')
+    print('\tCommit starter code')
+
+    """
+    BALLS! I forgot to update the graylists
+    I also forgot newlines in the cloning script - fixed code, need to fix script
+    """
-- 
GitLab