Source code for grader_service.handlers.submissions

# Copyright (c) 2022, TU Wien
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

import datetime
import json
import os.path
import subprocess

from grader_service.autograding.grader_executor import GraderExecutor

from grader_service.autograding.local_feedback import GenerateFeedbackExecutor
from grader_service.handlers.handler_utils import parse_ids
from grader_service.orm import Lecture
from grader_service.orm.user import User
import tornado
from grader_service.api.models.submission import Submission as SubmissionModel
from grader_service.orm.assignment import Assignment, AutoGradingBehaviour
from grader_service.orm.base import DeleteState
from grader_service.orm.submission import Submission
from grader_service.orm.takepart import Role, Scope
from grader_service.registry import VersionSpecifier, register_handler
from sqlalchemy.sql.expression import func
from tornado.web import HTTPError
from grader_convert.gradebook.models import GradeBookModel

from grader_service.handlers.base_handler import GraderBaseHandler, authorize, RequestHandlerConfig


[docs]def tuple_to_submission(t): """ Transforms tuple with values into a submission entity. :param t: tuple with values :type t: tuple :return: submission entity """ s = Submission() ( s.id, s.auto_status, s.manual_status, s.score, s.username, s.assignid, s.commit_hash, s.feedback_available, s.logs, s.date, ) = t return s
[docs]@register_handler( path=r"\/lectures\/(?P<lecture_id>\d*)\/assignments\/(?P<assignment_id>\d*)\/submissions\/?", version_specifier=VersionSpecifier.ALL, ) class SubmissionHandler(GraderBaseHandler): """ Tornado Handler class for http requests to /lectures/{lecture_id}/assignments/{assignment_id}/submissions. """
[docs] def on_finish(self): # we do not close the session we just commit because we might run # LocalAutogradeExecutor or GenerateFeedbackExecutor in POST which still need it pass
[docs] @authorize([Scope.student, Scope.tutor, Scope.instructor]) async def get(self, lecture_id: int, assignment_id: int): """ Return the submissions of an assignment. Two query parameter: latest, instructor-version. latest: only get the latest submissions of users. instructor-version: if true, get the submissions of all users in lecture if false, get own submissions. :param lecture_id: id of the lecture :type lecture_id: int :param assignment_id: id of the assignment :type assignment_id: int :raises HTTPError: throws err if user is not authorized or the assignment was not found """ lecture_id, assignment_id = parse_ids(lecture_id, assignment_id) self.validate_parameters("filter", "instructor-version", "format") submission_filter = self.get_argument("filter", "none") if submission_filter not in ["none", "latest", "best"]: raise HTTPError(400, reason="Filter parameter has to be either 'none', 'latest' or 'best'") instructor_version = self.get_argument("instructor-version", None) == "true" response_format = self.get_argument("format", "json") if response_format not in ["json", "csv"]: raise HTTPError(400, reason="Response format can either be 'json' or 'csv'") role: Role = self.get_role(lecture_id) if instructor_version and role.role < Scope.tutor: raise HTTPError(403) assignment = self.get_assignment(lecture_id, assignment_id) if instructor_version: if submission_filter == 'latest': submissions = ( self.session.query( Submission.id, Submission.auto_status, Submission.manual_status, Submission.score, Submission.username, Submission.assignid, Submission.commit_hash, Submission.feedback_available, Submission.logs, func.max(Submission.date), ) .filter(Submission.assignid == assignment_id) .group_by(Submission.username) .all() ) submissions = [tuple_to_submission(t) for t in submissions] elif submission_filter == 'best': submissions = ( self.session.query( Submission.id, Submission.auto_status, Submission.manual_status, func.max(Submission.score), Submission.username, Submission.assignid, Submission.commit_hash, Submission.feedback_available, Submission.logs, Submission.date, ) .filter(Submission.assignid == assignment_id) .group_by(Submission.username) .all() ) submissions = [tuple_to_submission(t) for t in submissions] else: submissions = assignment.submissions else: if submission_filter == 'latest': submissions = ( self.session.query( Submission.id, Submission.auto_status, Submission.manual_status, Submission.score, Submission.username, Submission.assignid, Submission.commit_hash, Submission.feedback_available, Submission.logs, func.max(Submission.date), ) .filter( Submission.assignid == assignment_id, Submission.username == role.username, ) .group_by(Submission.username) .all() ) submissions = [tuple_to_submission(t) for t in submissions] elif submission_filter == 'best': submissions = ( self.session.query( Submission.id, Submission.auto_status, Submission.manual_status, func.max(Submission.score), Submission.username, Submission.assignid, Submission.commit_hash, Submission.feedback_available, Submission.logs, Submission.date, ) .filter( Submission.assignid == assignment_id, Submission.username == role.username, ) .group_by(Submission.username) .all() ) submissions = [tuple_to_submission(t) for t in submissions] else: submissions = ( self.session.query( Submission ) .filter( Submission.assignid == assignment_id, Submission.username == role.username, ) .all() ) if response_format == "csv": # csv format does not include logs self.set_header("Content-Type", "text/csv") for i, s in enumerate(submissions): d = s.model.to_dict() if i == 0: self.write(",".join((k for k in d.keys() if k != "logs"))+"\n") self.write(",".join((str(v) for k, v in d.items() if k != "logs"))+"\n") else: self.write_json(submissions) self.session.close() # manually close here because on_finish overwrite
[docs] @authorize([Scope.student, Scope.tutor, Scope.instructor]) async def post(self, lecture_id: int, assignment_id: int): """ Create submission based on commit hash. :param lecture_id: id of the lecture :type lecture_id: int :param assignment_id: id of the assignment :type assignment_id: int :raises HTTPError: throws err if user is not authorized or the assignment was not found """ lecture_id, assignment_id = parse_ids(lecture_id, assignment_id) self.validate_parameters() body = tornado.escape.json_decode(self.request.body) try: commit_hash = body["commit_hash"] except KeyError: raise HTTPError(400) assignment = self.get_assignment(lecture_id, assignment_id) submission_ts = datetime.datetime.utcnow() if assignment.duedate is not None and submission_ts > assignment.duedate: raise HTTPError(400, reason="Submission after due date of assignment!") submission = Submission() submission.assignid = assignment.id submission.date = submission_ts submission.username = self.user.name submission.feedback_available = False if assignment.duedate is not None and submission.date > assignment.duedate: self.write({"message": "Cannot submit assignment: Past due date!"}) self.write_error(400) git_repo_path = self.construct_git_dir(repo_type=assignment.type, lecture=assignment.lecture, assignment=assignment) if git_repo_path is None or not os.path.exists(git_repo_path): raise HTTPError(404) try: subprocess.run(["git", "branch", "main", "--contains", commit_hash], cwd=git_repo_path, capture_output=True) except subprocess.CalledProcessError: raise HTTPError(404) submission.commit_hash = commit_hash submission.auto_status = "not_graded" submission.manual_status = "not_graded" self.session.add(submission) self.session.commit() self.set_status(201) self.write_json(submission) # If the assignment has automatic grading or fully automatic grading perform necessary operations if assignment.automatic_grading in [AutoGradingBehaviour.auto, AutoGradingBehaviour.full_auto]: self.set_status(202) executor = RequestHandlerConfig.instance().autograde_executor_class( self.application.grader_service_dir, submission, close_session=False, config=self.application.config ) if assignment.automatic_grading == AutoGradingBehaviour.full_auto: feedback_executor = GenerateFeedbackExecutor( self.application.grader_service_dir, submission, config=self.application.config ) GraderExecutor.instance().submit( executor.start, on_finish=lambda: GraderExecutor.instance().submit(feedback_executor.start) ) else: GraderExecutor.instance().submit( executor.start, lambda: self.log.info(f"Autograding task of submission {submission.id} exited!") ) if assignment.automatic_grading == AutoGradingBehaviour.unassisted: self.session.close()
[docs]@register_handler( path=r"\/lectures\/(?P<lecture_id>\d*)\/assignments\/(?P<assignment_id>\d*)\/submissions\/(?P<submission_id>\d*)\/?", version_specifier=VersionSpecifier.ALL, ) class SubmissionObjectHandler(GraderBaseHandler): """ Tornado Handler class for http requests to /lectures/{lecture_id}/assignments/{assignment_id}/submissions/{submission_id}. """
[docs] @authorize([Scope.tutor, Scope.instructor]) async def get(self, lecture_id: int, assignment_id: int, submission_id: int): """ Returns a specific submission. :param lecture_id: id of the lecture :type lecture_id: int :param assignment_id: id of the assignment :type assignment_id: int :param submission_id: id of the submission :type submission_id: int """ lecture_id, assignment_id, submission_id = parse_ids( lecture_id, assignment_id, submission_id ) submission = self.get_submission(lecture_id, assignment_id, submission_id) self.write_json(submission)
[docs] @authorize([Scope.tutor, Scope.instructor]) async def put(self, lecture_id: int, assignment_id: int, submission_id: int): """ Updates a specific submission and returns the updated entity. :param lecture_id: id of the lecture :type lecture_id: int :param assignment_id: id of the assignment :type assignment_id: int :param submission_id: id of the submission :type submission_id: int """ lecture_id, assignment_id, submission_id = parse_ids( lecture_id, assignment_id, submission_id ) body = tornado.escape.json_decode(self.request.body) sub_model = SubmissionModel.from_dict(body) sub = self.get_submission(lecture_id, assignment_id, submission_id) # sub.date = sub_model.submitted_at # sub.assignid = assignment_id # sub.username = self.user.name sub.auto_status = sub_model.auto_status sub.manual_status = sub_model.manual_status sub.feedback_available = sub_model.feedback_available or False self.session.commit() self.write_json(sub)
[docs]@register_handler( path=r"\/lectures\/(?P<lecture_id>\d*)\/assignments\/(?P<assignment_id>\d*)\/submissions\/(?P<submission_id>\d*)\/properties\/?", version_specifier=VersionSpecifier.ALL, ) class SubmissionPropertiesHandler(GraderBaseHandler): """ Tornado Handler class for http requests to /lectures/{lecture_id}/assignments/{assignment_id}/submissions/ {submission_id}/properties. """
[docs] @authorize([Scope.tutor, Scope.instructor]) async def get(self, lecture_id: int, assignment_id: int, submission_id: int): """ Returns the properties of a submission, :param lecture_id: id of the lecture :type lecture_id: int :param assignment_id: id of the assignment :type assignment_id: int :param submission_id: id of the submission :type submission_id: int :raises HTTPError: throws err if the submission or their properties are not found """ lecture_id, assignment_id, submission_id = parse_ids( lecture_id, assignment_id, submission_id ) submission = self.get_submission(lecture_id, assignment_id, submission_id) if submission.properties is not None: self.write(submission.properties) else: raise HTTPError(404)
[docs] @authorize([Scope.tutor, Scope.instructor]) async def put(self, lecture_id: int, assignment_id: int, submission_id: int): """ Updates the properties of a submission. :param lecture_id: id of the lecture :type lecture_id: int :param assignment_id: id of the assignment :type assignment_id: int :param submission_id: id of the submission :type submission_id: int :raises HTTPError: throws err if the submission are not found """ lecture_id, assignment_id, submission_id = parse_ids( lecture_id, assignment_id, submission_id ) submission = self.get_submission(lecture_id, assignment_id, submission_id) properties_string: str = self.request.body.decode("utf-8") try: score = GradeBookModel.from_dict(json.loads(properties_string)).score except: raise HTTPError(400, reason="Cannot parse properties file!") submission.score = score submission.properties = properties_string self.session.commit() self.write_json(submission)