diff --git a/api/gitlab_classes.py b/api/gitlab_classes.py index 4e8a059252cb6eb7c7bea344a27236850cdb9405..4ca6b635c0480751823758717a33742521b4287b 100644 --- a/api/gitlab_classes.py +++ b/api/gitlab_classes.py @@ -215,6 +215,12 @@ class GitlabCommit: def is_merge(self) -> bool: return len(self.gitlab_commit.parent_ids) > 1 + def get_id(self) -> str: + return self.gitlab_commit.id + + def get_short_id(self) -> str: + return self.gitlab_commit.short_id + # noinspection PyShadowingNames def get_diffs(self) -> List[Dict[str, Union[str, int]]]: diffs: List[Dict[str, Union[str, int]]] = [] @@ -234,6 +240,34 @@ class GitlabCommit: deletions += diff['-'] return max(insertions, deletions) + # noinspection PyShadowingNames + def get_diff_size_by_filetype(self) -> Dict[str, int]: + binary_like_text_files = {"css", "csv", "fxml", "html"} # TODO soft-code this + # noinspection SpellCheckingInspection + binary_files = {"docx", "gif", "jpg", "jpeg", "pdf", "png", "pptx", "svg", "xlsx"} # TODO this, too + return_diff: Dict[str, int] = {} + insert_diff: Dict[str, int] = {} + delete_diff: Dict[str, int] = {} + if not self.is_merge(): + for diff in self.get_diffs(): + file = diff['file'].split('.') + if len(file) == 1: + filetype = 'no type' + else: + filetype = file[-1] + if filetype not in return_diff.keys(): + return_diff[filetype] = 0 + insert_diff[filetype] = 0 + delete_diff[filetype] = 0 + if filetype in binary_files.union(binary_like_text_files): + insert_diff[filetype] += 1 + else: + insert_diff[filetype] += diff['+'] + delete_diff[filetype] += diff['-'] + for key in return_diff.keys(): + return_diff[key] = max(insert_diff[key], delete_diff[key]) + return return_diff + @staticmethod def _number_of_lines_too_long(lines, subject_line_length, message_line_length): # noinspection PyUnusedLocal diff --git a/grade_team_contribution.py b/grade_team_contribution.py index 618369479afc9a7af577674afb65061f9caedf7a..29a13591c312b75f9ccedbe5559bbfb5eecff6c9 100644 --- a/grade_team_contribution.py +++ b/grade_team_contribution.py @@ -1,7 +1,6 @@ -import math import textwrap from datetime import date -from math import ceil, log10 +from math import log10, floor from typing import Tuple from gitlab import GitlabError @@ -94,8 +93,24 @@ def get_project_prefix(canvas_groups): return prefix +def _combine_typed_contributions(typed_sizes: Tuple[Dict[str, int]]) -> Dict[str, int]: + combined_typed_size: Dict[str, int] = {} + keys: Set[str] = set() + for typed_size in typed_sizes: + keys = keys.union(typed_size.keys()) + for key in keys: + combined_typed_size[key] = 0 + for typed_size in typed_sizes: + if key in typed_size.keys(): + combined_typed_size[key] += typed_size[key] + return combined_typed_size + + def display_git_contributions(project: GitlabProject): # TODO: recognize that this only works for projects in namespace; will need to ask whether to retrieve project. + binary_like_text_files = {"css", "csv", "fxml", "html"} # TODO soft-code this + # noinspection SpellCheckingInspection + binary_files = {"docx", "gif", "jpg", "jpeg", "pdf", "png", "pptx", "svg", "xlsx"} # TODO this, too # noinspection PyUnusedLocal project_commits: List[GitlabCommit] if assignment_start_date == '': @@ -103,6 +118,7 @@ def display_git_contributions(project: GitlabProject): else: project_commits = project.get_commits(after_date=assignment_start_date) contributions: Dict[str, int] = {} # TODO: also broaden to multiple branches? + typed_contributions: Dict[str, Dict[str, int]] = {} timestamps: Dict[str, List[datetime]] = {} contributors: Set[Tuple[str, str]] = set() # noinspection PyShadowingNames @@ -111,24 +127,33 @@ def display_git_contributions(project: GitlabProject): author = commit.get_author() contributors.add((author['name'], author['email'])) email = author['email'] # TODO: manage aliases - size = commit.get_diff_size() # TODO: distinguish between file types + size = commit.get_diff_size() + typed_size = commit.get_diff_size_by_filetype() if email != 'bohn@unl.edu': # TODO: un-hard-code this -- may not be necessary with a starting date if email not in contributions: contributions[email] = 0 + typed_contributions[email] = {} timestamps[email] = [] contributions[email] += size + typed_sizes: Tuple[Dict[str, int]] = (typed_contributions[email], typed_size) + typed_contributions[email] = _combine_typed_contributions(typed_sizes) timestamps[email].append(commit.get_timestamp()) print(f'Contributions by each partner to {project} :') for contribution in contributions: contributor = list(filter(lambda c: c[1] == contribution, contributors))[0] email = contributor[1] + typed_contribution = typed_contributions[email] timestamps[email].sort() number_of_commits = len(timestamps[email]) print(f'\t{contributor}') print( - f'\t{str(contributions[contribution]).rjust(5)} line changes in {str(number_of_commits).rjust(3)} commits') + f'\t{str(contributions[contribution]).rjust(5)} total line changes in ' + f'{str(number_of_commits).rjust(3)} commits') + for filetype in sorted(typed_contribution.keys()): + change_type = 'file' if filetype in binary_files.union(binary_like_text_files) else 'line' + print(f'\t\t\t{filetype} {str(typed_contribution[filetype]).rjust(10-len(filetype))} {change_type} changes') print(f'\t\tFirst commit: {timestamps[email][0]}') - print(f'\t\tMedian commit: {timestamps[email][math.floor(number_of_commits/2)]}') + print(f'\t\tMedian commit: {timestamps[email][floor(number_of_commits/2)]}') print(f'\t\tLast commit: {timestamps[email][-1]}') @@ -171,8 +196,7 @@ if __name__ == '__main__': if option is options[1]: print('Which group?') student_groups = [select_from_list(student_groups, 'student group')] - zero_padding: int = ceil(log10(len(projects))) # remove this after 24pair is graded - # zero_padding: int = floor(log10(len(projects))) + 1 # leaving this here until 24pair is graded + zero_padding: int = floor(log10(len(projects))) + 1 assignment_start_date = get_assignment_start() # TODO: only need this if grading git histories for student_group in student_groups: # TODO: Skip past graded groups input(f'\n\nPress Enter to grade {student_group}')