From 02c5b870bc6ed9368ce813d2585a5612b4467a3a Mon Sep 17 00:00:00 2001
From: Christopher Bohn <bohn@unl.edu>
Date: Thu, 30 Jul 2020 14:25:28 -0500
Subject: [PATCH] Finished milestone issue enumerator / burndown generator

---
 api/gitlab_classes.py      |  9 ++++
 grade_team_contribution.py |  1 -
 milestone_sniffer.py       | 99 +++++++++++++++++++++++++++++++++++---
 3 files changed, 101 insertions(+), 8 deletions(-)

diff --git a/api/gitlab_classes.py b/api/gitlab_classes.py
index b8f634c..efdc493 100644
--- a/api/gitlab_classes.py
+++ b/api/gitlab_classes.py
@@ -202,6 +202,9 @@ class GitlabIssue:
     def __ne__(self, other: "GitlabIssue") -> bool:
         return not self.__eq__(other)
 
+    def __hash__(self) -> int:
+        return self.get_universal_issue_id()
+
     # other git_issue fields:
     # project_id
     # title
@@ -353,6 +356,9 @@ class GitlabCommit:
     def __ne__(self, other: "GitlabCommit") -> bool:
         return not self.__eq__(other)
 
+    def __hash__(self) -> int:
+        return hash(self.get_id())
+
     # git_commit fields:
     # comments
     # discussions
@@ -450,6 +456,9 @@ class GitlabMilestone:
     def __ne__(self, other: "GitlabMilestone") -> bool:
         return not self.__eq__(other)
 
+    def __hash__(self) -> int:
+        return self.git_milestone.id
+
     # other git_milestone fields:
     # id
     # iid
diff --git a/grade_team_contribution.py b/grade_team_contribution.py
index 29a1359..4b29ba6 100644
--- a/grade_team_contribution.py
+++ b/grade_team_contribution.py
@@ -1,5 +1,4 @@
 import textwrap
-from datetime import date
 from math import log10, floor
 from typing import Tuple
 
diff --git a/milestone_sniffer.py b/milestone_sniffer.py
index d7130c3..4748433 100644
--- a/milestone_sniffer.py
+++ b/milestone_sniffer.py
@@ -1,19 +1,104 @@
+import csv
+from datetime import timedelta, time
+
+from dateutil import tz
+
 from api.gitlab_classes import *
 from course import Course
 
+
+# noinspection PyShadowingNames
+def write_issue_enumeration(milestone: GitlabMilestone, filename: str) -> None:
+    fieldnames = ['Issue', 'Created', 'Closed', 'Assignee', 'Participants']
+    with open(filename, mode='w') as csv_file:
+        writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
+        writer.writeheader()
+        for issue in sorted(milestone.get_issues(), key=lambda i: i.get_project_issue_id()):
+            writer.writerow({
+                fieldnames[0]: issue.get_project_issue_id(),
+                fieldnames[1]: issue.get_created_at(),
+                fieldnames[2]: issue.get_closed_at() if issue.get_closed_at() is not None else '(open)',
+                fieldnames[3]: issue.get_assignee(),
+                fieldnames[4]: issue.get_participants()
+            })
+
+
+def write_issue_burndown(milestone: GitlabMilestone, filename: str) -> None:
+    timezone = tz.gettz('America/Chicago')
+    start: datetime = datetime.combine(date=(milestone.get_start_date()), time=time.min, tzinfo=timezone)
+    due: datetime = datetime.combine(date=(milestone.get_due_date()), time=time.max, tzinfo=timezone)
+    end: datetime = datetime.combine(date=date.today(), time=time.max, tzinfo=timezone)
+    step: timedelta = timedelta(hours=1)
+    window: datetime = start
+    issues: List[GitlabIssue] = milestone.get_issues()
+    timeline = []
+    while window <= due and window <= end:
+        existing_open_issues = set()
+        new_issues = set()
+        closed_issues = set()
+        for issue in issues:
+            if window <= issue.get_created_at() < window + step:
+                new_issues.add(issue)
+            elif issue.is_closed() and issue.get_closed_at() < window + step:
+                closed_issues.add(issue)
+            elif issue.get_created_at() < window:
+                existing_open_issues.add(issue)
+            else:
+                assert issue.get_created_at() >= window + step,\
+                    'Window starts at {Window}. Issue {issue} was not open before the window, was not created during ' \
+                    'the window, was not closed before the end of the window, and was not created after the window.'
+        timeline.append({
+            'Window': window,
+            'Open Issues': len(existing_open_issues),
+            'New Issues': len(new_issues),
+            'Closed Issues': len(closed_issues)
+        })
+        window += step
+    if len(timeline) == 0:
+        timeline.append({
+            'Window': start,
+            'Open Issues': 0,
+            'New Issues': 0,
+            'Closed Issues': 0
+        })
+    while window <= due:
+        timeline.append({
+            'Window': window,
+            'Open Issues': timeline[-1]['Open Issues'] + timeline[-1]['New Issues'],
+            'New Issues': 0,
+            'Closed Issues': timeline[-1]['Closed Issues']
+        })
+        window += step
+    if window != due:
+        timeline.append({
+            'Window': due,
+            'Open Issues': timeline[-1]['Open Issues'] + timeline[-1]['New Issues'],
+            'New Issues': 0,
+            'Closed Issues': timeline[-1]['Closed Issues']
+        })
+    fieldnames = ['Window', 'Open Issues', 'New Issues', 'Closed Issues']
+    with open(filename, mode='w') as csv_file:
+        writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
+        writer.writeheader()
+        for moment in timeline:
+            writer.writerow(moment)
+
+
 if __name__ == '__main__':
-    projects = sorted(list(filter(lambda p: p.get_name().startswith('36team'),
+    projects = sorted(list(filter(lambda p: p.get_name().startswith('36team'),  ## TODO soft-code prefix
                                   GitlabProject.get_projects_by_group(Course.gitlab_namespace))),
                       key=lambda p: p.get_name())
     for project in projects:
         milestones = sorted(project.get_milestones(), key=lambda m: m.get_title())
-        print('\n\n')
         if len(milestones) == 0:
             print(f'Project: {project} has no milestones')
         else:
             for milestone in milestones:
-                print(f'Project: {project}\tMilestone: {milestone}')
-                for issue in sorted(milestone.get_issues(), key=lambda i: i.get_project_issue_id()):
-                    print(f'\tIssue {issue}')
-                    print(f'\t\tCreated at {issue.get_created_at()}, assigned to {issue.get_assignee()}, updated at {issue.get_updated_at()}, closed at {issue.get_closed_at()}.')
-                    print(f'\t\tParticipants: {issue.get_participants()}')
+                write_issue_enumeration(milestone=milestone,
+                                        filename=f'{project.get_name()}-'
+                                                 f'{milestone.get_title().replace(" ", "_")}-'
+                                                 f'issues.csv')
+                write_issue_burndown(milestone=milestone,
+                                     filename=f'{project.get_name()}-'
+                                              f'{milestone.get_title().replace(" ", "_")}-'
+                                              f'burndown.csv')
-- 
GitLab