Skip to content
Snippets Groups Projects
Select Git revision
  • 91d3b525e85349c73acdbfa3312d07e5ebbf6c4c
  • 3.9 default
  • develop
  • 6.0
  • 5.0
  • 4.0
  • scrutinizer-patch-4
  • scrutinizer-patch-3
  • scrutinizer-patch-2
  • scrutinizer-patch-1
  • 3.7
  • 3.8
  • 3.6
  • 3.9_backported
  • 3.8_backported
  • 3.7_backported
  • 3.5
  • 3.6_backported
  • 3.5_backported
  • 3.4
  • 3.3_backported
  • 6.0.4
  • 6.0.3
  • 5.0.7
  • 6.0.2
  • 6.0.1
  • 5.0.6
  • 6.0.0
  • 5.0.5
  • 6.0.0-rc
  • 5.0.4
  • 6.0.0-beta
  • 5.0.3
  • 4.0.6
  • 5.0.2
  • 5.0.1
  • 4.0.5
  • 5.0.0
  • 4.0.4
  • 5.0.0-rc2
  • 5.0.0-rc1
41 results

propal.class.php

Blame
  • grade_team_contribution.py 10.55 KiB
    import textwrap
    from datetime import date
    from math import log10, floor
    from typing import Tuple
    
    from gitlab import GitlabError
    
    from api.canvas_classes import *
    from api.gitlab_classes import *
    from common_functions import select_from_list, strip_html
    from course import Course
    
    assignment_start_date: str = ''
    
    
    def get_assignment_start():
        input_is_invalid: bool = True
        user_input: str = ''
        while input_is_invalid:
            user_input = input(
                'Enter the assignment start date (yyyy-mm-dd or yyyymmdd) or hit Enter to consider all commits: ')
            # validate input
            if len(user_input) == 8:
                user_input = f'{user_input[:4]}-{user_input[4:6]}-{user_input[-2:]}'
            if len(user_input) == 10:
                if user_input[:4].isnumeric() and user_input[4] == '-' and user_input[5:7].isnumeric() and \
                        user_input[-3] == '-' and user_input[-2:].isnumeric():
                    year = int(user_input[:4])
                    month = int(user_input[5:7])
                    day = int(user_input[-2:])
                    if year < 1946:
                        print('\tThe year must be within modern the era of computing.')
                    elif year < date.today().year:
                        if 1 <= month <= 12:
                            if 1 <= day <= 31:  # not worth my time to validate 28/29/30-day months
                                input_is_invalid = False
                            else:
                                print('\tThe day must be between 01 and 31, inclusive.')
                        else:
                            print('\tThe month must be between 01 and 12, inclusive.')
                    elif year == date.today().year:
                        if month < 1:
                            print('\tThe month must be between 01 and 12, inclusive.')
                        elif month < date.today().month:
                            input_is_invalid = False
                        elif month == date.today().month:
                            if day < 1:
                                print('\tThe day must be between 01 and 31, inclusive.')
                            elif day <= date.today().day:
                                input_is_invalid = False
                            else:
                                print('\tThe date cannot be later than today.')
                        else:
                            print('\tThe date cannot be later than today.')
                    else:
                        print('\tThe date cannot be later than today.')
                else:
                    print('\tValidation failed due to non-numeric input.')
            elif len(user_input) == 0:
                input_is_invalid = False
            else:
                print('\tValidation failed due to incorrect number of characters.')
        return user_input
    
    
    def structure_text(text: str) -> List[str]:
        return textwrap.wrap(strip_html(text))
    
    
    def display_peer_reviews(assignment, students):
        for student in students:
            text = []
            print(f'\n\n\t{student.get_name()}'.expandtabs(4))
            if assignment.is_quiz():
                response = assignment.get_quiz_response(student)
                if response is not None:
                    for entry in response:
                        text = text + structure_text(entry['question']) + structure_text(entry['answer'])
            else:
                response = assignment.get_submission_text(student)
                if response is not None:
                    text = structure_text(response)
            for line in text:
                print(f'\t\t{line}'.expandtabs(4))
    
    
    def get_project_prefix(canvas_groups):
        name_segments = canvas_groups[0].get_name().split()
        prefix = input(f'What is the prefix of the gitlab project names (enter * if no common prefix exists)? '
                       f'[{name_segments[0]}] ')
        if prefix == '':
            prefix = name_segments[0]
        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 == '':
            project_commits = project.get_commits()
        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
        for commit in project_commits:
            if not commit.is_merge():
                author = commit.get_author()
                contributors.add((author['name'], author['email']))
                email = author['email']  # TODO: manage aliases
                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)} 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][floor(number_of_commits/2)]}')
            print(f'\t\tLast commit:   {timestamps[email][-1]}')
    
    
    # noinspection PyUnusedLocal
    def grade(assignment1, assignment2, students):
        print('Enter grades through Canvas gradebook')
    
    
    if __name__ == '__main__':
        print('Establishing connections to Canvas and Gitlab...')
        course = CanvasCourse(Course.canvas_course_id)
        projects = GitlabProject.get_projects_by_group(Course.gitlab_namespace)
        print('Connections established.')
        print('First, select the "peer review" assignment to review and grade (select 0 if not grading peer reviews).\n')
        assignment_groups = course.get_assignment_groups()
        assignment_group = select_from_list(assignment_groups, 'assignment group', True)
        grading_peer_reviews = assignment_group is not None
        print()
        peer_review_assignment = None
        if grading_peer_reviews:
            assignments = assignment_group.get_assignments()
            peer_review_assignment = select_from_list(assignments, 'assignment')
            print(f'\nSelected {peer_review_assignment}.')
        # TODO: select second assignment (git history)
        print('Are you grading git histories?')
        options = ['Yes', 'No']
        option = select_from_list(options, 'option')
        grading_git_histories = option == options[0]
        print()
        print('Now select the student groupset with the teams.\n')
        student_groupsets = course.get_user_groupsets()
        student_groupset = select_from_list(student_groupsets, 'groupset')
        print(f'\nSelected {student_groupset}.\n')
        student_groups = student_groupset.get_groups()  # TODO: only need this if grading git histories
        project_prefix = get_project_prefix(student_groups)
        no_common_prefix: bool = True if project_prefix == '*' else False
        print('Are you grading all groups, or are you revisiting a specific group?')
        options = ['All groups', 'Specific group']
        option = select_from_list(options, 'option')
        if option is options[1]:
            print('Which group?')
            student_groups = [select_from_list(student_groups, 'student group')]
        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}')
            print()
            if grading_git_histories:
                if no_common_prefix:
                    custom_project: str = None
                    repository: GitlabProject = None
                    while custom_project is None:
                        custom_project = input(
                            f'Enter path to {student_group.get_name()}\'s repository (leave blank to skip): ')
                        try:
                            if len(custom_project) > 0:
                                repository = GitlabProject(custom_project)
                        except GitlabError:
                            print(f'Could not location repository {custom_project}; please confirm path.')
                            custom_project = None
                    if len(custom_project) > 0:
                        display_git_contributions(repository)
                else:
                    project_name = f'{project_prefix}{student_group.get_name().split()[1]}'.zfill(zero_padding)
                    display_git_contributions(list(filter(lambda p: p.get_name() == project_name, projects))[0])
            if grading_peer_reviews:
                display_peer_reviews(peer_review_assignment, student_group.get_students())
            # TODO: Ask if you want to grade (keep track of groups that you don't grade)
            if True:
                grade(peer_review_assignment, None, student_group.get_students())