diff --git a/denis/final.py b/denis/final.py index cbee80df..3728d311 100755 --- a/denis/final.py +++ b/denis/final.py @@ -11,4 +11,6 @@ print(f'final subs for {assignment} released') -utilities.update_tags(assignment, 'final') +tags = utilities.update_tags(assignment, 'final') + +utilities.run_automated_checks(tags) diff --git a/denis/initial.py b/denis/initial.py index 5faa098c..71cf2d05 100755 --- a/denis/initial.py +++ b/denis/initial.py @@ -58,4 +58,6 @@ print(f'initial subs for {assignment} released') -utilities.update_tags(assignment, 'initial') +tags = utilities.update_tags(assignment, 'initial') + +utilities.run_automated_checks(tags) diff --git a/denis/utilities.py b/denis/utilities.py index 0120a7e0..09e34f74 100644 --- a/denis/utilities.py +++ b/denis/utilities.py @@ -38,6 +38,12 @@ def release_subs(sub_ids): input=journal_data, check=True) +def configure_repo(repo): + with repo.config_writer() as config: + config.set_value('user', 'name', 'denis') + config.set_value('user', 'email', 'denis@denis') + + def update_tags(assignment, component): grd_tbl = mailman.db.Gradeable subs = (grd_tbl.select() @@ -47,15 +53,15 @@ def update_tags(assignment, component): with tempfile.TemporaryDirectory() as repo_path: repo = git.Repo.clone_from(PULL_URL, repo_path) repo.create_remote(REMOTE_NAME, PUSH_URL) - repo.config_writer().set_value('user', 'name', 'denis').release() - (repo.config_writer().set_value('user', 'email', 'denis@denis') - .release()) + configure_repo(repo) if 'EMPTY' not in repo.tags: repo.git.commit('--allow-empty', '-m', 'No gradeable submission.') repo.create_tag('EMPTY') + updated_tags = [] for user in orbit.db.User.select(): new_tag_name = f'{assignment}_{component}_{user.username}' + updated_tags.append(new_tag_name) if new_tag_name in repo.tags: print('Potential issue? Attempted to create duplicate tag ' f'{new_tag_name}') @@ -70,3 +76,20 @@ def update_tags(assignment, component): repo.create_tag(new_tag_name, ref=to_promote.commit, message=msg) repo.git.push(REMOTE_NAME, tags=True) + + return updated_tags + + +def run_automated_checks(tags): + with tempfile.TemporaryDirectory() as repo_path: + repo = git.Repo.clone_from(PULL_URL, repo_path) + + remote = repo.create_remote(REMOTE_NAME, PUSH_URL) + remote.fetch('refs/notes/*:refs/notes/*') + configure_repo(repo) + + for tag in tags: + msg = 'Automated tests by denis' + repo.git.execute(['git', 'notes', '--ref=denis', 'add', tag, '-m', msg]) + + remote.push('refs/notes/*:refs/notes/*') diff --git a/mailman/patchset.py b/mailman/patchset.py index 7738f05d..44a3854f 100644 --- a/mailman/patchset.py +++ b/mailman/patchset.py @@ -19,10 +19,9 @@ def try_or_false(do, exc): return False -def tag_and_push(repo_path, tag_name): +def tag_and_push(repo, tag_name, msg=None): try: - repo = git.Repo(repo_path) - repo.create_tag(tag_name) + repo.create_tag(tag_name, message=msg) repo.create_remote('grading', REMOTE_PUSH_URL) repo.git.push('grading', tags=True) return True @@ -31,14 +30,11 @@ def tag_and_push(repo_path, tag_name): return False -author_args = ['-c', 'user.name=mailman', '-c', - 'user.email=mailman@mailman'] git_am_args = ['git', '-c', 'advice.mergeConflict=false', - *author_args, 'am', '--keep'] + 'am', '--keep'] -def do_check(repo_path, cover_letter, patches): - repo = git.Repo.init(repo_path) +def do_check(repo, cover_letter, patches): whitespace_errors = [] def am_cover_letter(keep_empty=True): @@ -51,7 +47,7 @@ def am_cover_letter(keep_empty=True): git.GitCommandError): return "missing cover letter!" - repo.git.execute(["git", *author_args, "am", "--abort"]) + repo.git.execute(["git", "am", "--abort"]) if not try_or_false(lambda: am_cover_letter(keep_empty=True), git.GitCommandError): return ("missing cover letter and " @@ -69,7 +65,7 @@ def do_git_am(extra_args=[]): git.GitCommandError): continue - repo.git.execute(["git", *author_args, "am", "--abort"]) + repo.git.execute(["git", "am", "--abort"]) # Try again, if we succeed, count this patch as a whitespace error if try_or_false(lambda: do_git_am(), git.GitCommandError): @@ -80,7 +76,7 @@ def do_git_am(extra_args=[]): return f'patch {i+1} failed to apply!' if whitespace_errors: - return ('whitespace error patch(es) ' + return (f'whitespace error patch{"es" if len(whitespace_errors) > 1 else ""} ' f'{",".join(whitespace_errors)}?') else: return 'patchset applies.' @@ -88,13 +84,16 @@ def do_git_am(extra_args=[]): def check(cover_letter, patches, submission_id): with tempfile.TemporaryDirectory() as repo_path: - status = do_check(repo_path, cover_letter, patches) + repo = git.Repo.init(repo_path) + with repo.config_writer() as config: + config.set_value('user', 'name', 'mailman') + config.set_value('user', 'email', 'mailman@mailman') + status = do_check(repo, cover_letter, patches) if status[-1] == '!': - repo = git.Repo(repo_path) for patch in patches: patch_abspath = str(maildir / patch.msg_id) - repo.git.execute(['git', *author_args, 'commit', '--allow-empty', '-F', patch_abspath]) - tag_and_push(repo_path, submission_id) + repo.git.execute(['git', 'commit', '--allow-empty', '-F', patch_abspath]) + tag_and_push(repo, submission_id, msg=status) return status @@ -110,8 +109,11 @@ def apply_peer_review(email, submission_id, review_id): multi_options=[f'--branch={review_id}', '--single-branch', '--no-tags']) + with repo.config_writer() as config: + config.set_value('user', 'name', 'mailman') + config.set_value('user', 'email', 'mailman@mailman') repo.git.execute([*args, patch_abspath]) - tag_and_push(repo_path, submission_id) + tag_and_push(repo, submission_id) except git.GitCommandError as e: print(e, file=sys.stderr) status = 'failed to apply peer review' diff --git a/orbit/Containerfile b/orbit/Containerfile index f083a0d7..9927359d 100644 --- a/orbit/Containerfile +++ b/orbit/Containerfile @@ -18,6 +18,7 @@ FROM alpine:3.20 AS orbit RUN apk update && apk upgrade && apk add \ py3-bcrypt \ + py3-gitpython \ py3-peewee \ py3-markdown \ uwsgi-python3 \ diff --git a/orbit/radius.py b/orbit/radius.py index ab26f8f0..6ca6a6fb 100644 --- a/orbit/radius.py +++ b/orbit/radius.py @@ -4,6 +4,7 @@ import base64 import bcrypt +import git import html import markdown import os @@ -425,7 +426,7 @@ class OopsStatus: class AsmtTable: def __init__(self, assignment, oopsieness, peer1, peer2, init, - review1, review2, final): + review1, review1_grade, review2, review2_grade, final, final_grade): self.assignment = assignment self.name = assignment.name self.oopsieness = oopsieness @@ -433,8 +434,11 @@ def __init__(self, assignment, oopsieness, peer1, peer2, init, self.peer2 = peer2 self.init = init self.review1 = review1 + self.review1_grade = review1_grade self.review2 = review2 + self.review2_grade = review2_grade self.final = final + self.final_grade = final_grade def oops_button_hover(self): match self.oopsieness: @@ -448,6 +452,48 @@ def oops_button_hover(self): case OopsStatus.UNAVAILABLE: return "You have already used your oopsie" + def get_automated_feedback(self, attr): + if attr not in ['init', 'final'] or (gbl := getattr(self, attr)) is None: + return 'No submission' + + match attr: + case 'init': + due_date = int(self.assignment.initial_due_date) + case 'final': + due_date = int(self.assignment.final_due_date) + + if due_date < int(datetime.now().timestamp()): + return gbl.status + + match gbl.status[-1]: + case '.': + return 'Submission accepted' + case '?': + return 'Issues detected in patchset' + case '!': + return 'Submission rejected' + case _: + return '---' + + def get_total_score(self): + weighted_sum = 0 + sum_of_weights = 0 + try: + weighted_sum += 0.8 * int(self.final_grade) + sum_of_weights += 0.8 + if self.peer1 is not None: + weighted_sum += 0.1 * int(self.review1_grade) + sum_of_weights += 0.1 + if self.peer2 is not None: + weighted_sum += 0.1 * int(self.review2_grade) + sum_of_weights += 0.1 + # if any of the grades are None, attempting to cast to int throws a TypeError + except TypeError: + return '-' + except ValueError: + return '???' + return f'{weighted_sum/sum_of_weights:.1f}' + def gradeable_row(self, item_name, gradeable, rightmost_col): return f""" @@ -480,8 +526,8 @@ def body(self): return f""" {self.gradeable_row('Final Submission', self.final, self.oopsie_button())} - Comments - - + Automated Feedback + {self.get_automated_feedback('final')} """ if (not self.init or @@ -491,14 +537,14 @@ def body(self): {self.gradeable_row('Initial Submission', self.init, self.oopsie_button())} Automated Feedback - - + {self.get_automated_feedback('init')} """ return f""" {self.gradeable_row('Initial Submission', self.init, self.oopsie_button())} Automated Feedback - - + {self.get_automated_feedback('init')} @@ -506,12 +552,12 @@ def body(self): Submission ID Score - {self.gradeable_row(self.peer1 + ' Peer Review', self.review1, '-') if self.peer1 else ''} - {self.gradeable_row(self.peer2 + ' Peer Review', self.review2, '-') if self.peer2 else ''} - {self.gradeable_row('Final Submission', self.final, '-')} + {self.gradeable_row(self.peer1 + ' Peer Review', self.review1, self.review1_grade if self.review1_grade else '-') if self.peer1 else ''} + {self.gradeable_row(self.peer2 + ' Peer Review', self.review2, self.review2_grade if self.review2_grade else '-') if self.peer2 else ''} + {self.gradeable_row('Final Submission', self.final, self.final_grade if self.final_grade else '-')} - Comments - - + Automated Feedback + {self.get_automated_feedback('final')} """ @@ -520,7 +566,7 @@ def __str__(self): - + @@ -593,8 +639,18 @@ def handle_dashboard(rocket): rev1 = asn_gradeables.where(grd_tbl.component == 'review1').first() rev2 = asn_gradeables.where(grd_tbl.component == 'review2').first() final = asn_gradeables.where(grd_tbl.component == 'final').first() - ret += str(AsmtTable(assignment, oopsieness, peer1, peer2, init, rev1, - rev2, final)) + + repo = git.Repo('/var/lib/git/grading.git') + grades = {} + for component in ['review1', 'review2', 'final']: + tag = f'{assignment.name}_{component}_{rocket.session.username}' + try: + grades[component] = repo.git.execute(['git', 'notes', '--ref=grade', 'show', tag]) + except git.GitCommandError: + grades[component] = None + + ret += str(AsmtTable(assignment, oopsieness, peer1, peer2, init, + rev1, grades['review1'], rev2, grades['review2'], final, grades['final'])) return rocket.respond(ret + '', 'Dashboard')

{self.name}

Total Score: -Total Score: {self.get_total_score()} Timestamp Submission ID Request an 'Oopsie'