diff --git a/api/gitlab_classes.py b/api/gitlab_classes.py index b8f634ca6a7d66167a5ebbaf9a22a6e2de36349e..efdc493dd3f0d09facfda28fd8f8766f5ecd71d3 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 29a13591c312b75f9ccedbe5559bbfb5eecff6c9..4b29ba6fc28179bf3cd478d9725c1e4ee2256b6f 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 d7130c396abf5396fca8d3e4145d5f3190a13e2d..474843325c2985eecedf4401db6d79cc78df44b3 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')