Source code for grader_labextension.handlers.version_control

# 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 json
import os
import sys
from urllib.parse import unquote, quote

from jsonschema.exceptions import ValidationError
from tornado.web import HTTPError

from grader_convert.converters.base import GraderConvertException
from grader_convert.converters.generate_assignment import GenerateAssignment
from .base_handler import ExtensionBaseHandler
from ..api.models.submission import Submission
from ..registry import register_handler
from ..services.git import GitError, GitService
from tornado.httpclient import HTTPClientError, HTTPResponse


[docs]@register_handler( path=r"\/lectures\/(?P<lecture_id>\d*)\/assignments\/(?P<assignment_id>\d*)\/generate\/?" ) class GenerateHandler(ExtensionBaseHandler): """ Tornado Handler class for http requests to /lectures/{lecture_id}/assignments/{assignment_id}/generate. """
[docs] async def put(self, lecture_id: int, assignment_id: int): """Generates the release files from the source files of a assignment :param lecture_id: id of the lecture :type lecture_id: int :param assignment_id: id of the assignment :type assignment_id: int """ try: lecture = await self.request_service.request( "GET", f"{self.service_base_url}/lectures/{lecture_id}", header=self.grader_authentication_header, ) assignment = await self.request_service.request( "GET", f"{self.service_base_url}/lectures/{lecture_id}/assignments/{assignment_id}", header=self.grader_authentication_header, ) except HTTPClientError as e: self.set_status(e.code) self.write_error(e.code) return code = lecture["code"] a_id = assignment["id"] generator = GenerateAssignment( input_dir=f"{self.root_dir}/source/{code}/{a_id}", output_dir=f"{self.root_dir}/release/{code}/{a_id}", file_pattern="*.ipynb", ) generator.force = True self.log.info("Starting GenerateAssignment converter") try: generator.start() except: e = sys.exc_info()[0] self.log.error(e) self.set_status(400) self.write_error(400) return try: gradebook_path = os.path.join(generator._output_directory, "gradebook.json") os.remove(gradebook_path) self.log.info(f"Successfully deleted {gradebook_path}") except OSError as e: self.log.error(f"Could delete {gradebook_path}! Error: {e.strerror}") self.log.info("GenerateAssignment conversion done") self.write("OK")
[docs]@register_handler( path=r"\/lectures\/(?P<lecture_id>\d*)\/assignments\/(?P<assignment_id>\d*)\/remote-status\/(?P<repo>\w*)\/?" ) class GitRemoteStatusHandler(ExtensionBaseHandler): """ Tornado Handler class for http requests to /lectures/{lecture_id}/assignments/{assignment_id}/remote_status/{repo}. """
[docs] async def get(self, lecture_id: int, assignment_id: int, repo: str): if repo not in {"assignment", "source", "release"}: self.write_error(404) lecture = await self.get_lecture(lecture_id) assignment = await self.get_assignment(lecture_id, assignment_id) git_service = GitService( server_root_dir=self.root_dir, lecture_code=lecture["code"], assignment_id=assignment["id"], repo_type=repo, config=self.config, force_user_repo=True if repo == "release" else False, ) try: if not git_service.is_git(): git_service.init() git_service.set_author() git_service.set_remote(f"grader_{repo}") git_service.fetch_all() status = git_service.check_remote_status(f"grader_{repo}", "main") except GitError: self.set_status(400) self.write_error(400) return self.write(status.name)
[docs]@register_handler( path=r"\/lectures\/(?P<lecture_id>\d*)\/assignments\/(?P<assignment_id>\d*)\/log\/(?P<repo>\w*)\/?" ) class GitLogHandler(ExtensionBaseHandler): """ Tornado Handler class for http requests to /lectures/{lecture_id}/assignments/{assignment_id}/log/{repo}. """
[docs] async def get(self, lecture_id: int, assignment_id: int, repo: str): """ Sends a GET request to the grader service to get the logs of a given repo. :param lecture_id: id of the lecture :param assignment_id: id of the assignment :param repo: repo name :return: logs of git repo """ if repo not in {"assignment", "source", "release"}: self.write_error(404) n_history = int(self.get_argument("n", "10")) try: lecture = await self.request_service.request( "GET", f"{self.service_base_url}/lectures/{lecture_id}", header=self.grader_authentication_header, ) assignment = await self.request_service.request( "GET", f"{self.service_base_url}/lectures/{lecture_id}/assignments/{assignment_id}", header=self.grader_authentication_header, ) except HTTPClientError as e: self.set_status(e.code) self.write_error(e.code) return git_service = GitService( server_root_dir=self.root_dir, lecture_code=lecture["code"], assignment_id=assignment["id"], repo_type=repo, config=self.config, force_user_repo=True if repo == "release" else False, ) try: if not git_service.is_git(): git_service.init() git_service.set_author() git_service.set_remote(f"grader_{repo}") git_service.fetch_all() if git_service.local_branch_exists("main"): # at least main should exist logs = git_service.get_log(n_history) else: logs = [] except GitError: self.set_status(400) self.write_error(400) return self.write(json.dumps(logs))
[docs]@register_handler( path=r"\/lectures\/(?P<lecture_id>\d*)\/assignments\/(?P<assignment_id>\d*)\/pull\/(?P<repo>\w*)\/?" ) class PullHandler(ExtensionBaseHandler): """ Tornado Handler class for http requests to /lectures/{lecture_id}/assignments/{assignment_id}/pull/{repo}. """
[docs] async def get(self, lecture_id: int, assignment_id: int, repo: str): """Creates a local repository and pulls the specified repo type :param lecture_id: id of the lecture :type lecture_id: int :param assignment_id: id of the assignment :type assignment_id: int :param repo: type of the repository :type repo: str """ if repo not in {"assignment", "source", "release"}: self.write_error(404) try: lecture = await self.request_service.request( "GET", f"{self.service_base_url}/lectures/{lecture_id}", header=self.grader_authentication_header, ) assignment = await self.request_service.request( "GET", f"{self.service_base_url}/lectures/{lecture_id}/assignments/{assignment_id}", header=self.grader_authentication_header, ) except HTTPClientError as e: self.set_status(e.code) self.write_error(e.code) return git_service = GitService( server_root_dir=self.root_dir, lecture_code=lecture["code"], assignment_id=assignment["id"], repo_type=repo, config=self.config, force_user_repo=repo == "release", ) try: if not git_service.is_git(): git_service.init() git_service.set_author() git_service.set_remote(f"grader_{repo}") git_service.pull(f"grader_{repo}", force=True) self.write("OK") except GitError as e: self.log.error("GitError:\n" + e.error) self.write_error(400)
[docs]@register_handler( path=r"\/lectures\/(?P<lecture_id>\d*)\/assignments\/(?P<assignment_id>\d*)\/push\/(?P<repo>\w*)\/?" ) class PushHandler(ExtensionBaseHandler): """ Tornado Handler class for http requests to /lectures/{lecture_id}/assignments/{assignment_id}/push/{repo}. """
[docs] async def put(self, lecture_id: int, assignment_id: int, repo: str): """Pushes from the local repositories to remote If the repo type is release, it also generate the release files and updates the assignment properties in the grader service :param lecture_id: id of the lecture :type lecture_id: int :param assignment_id: id of the assignment :type assignment_id: int :param repo: type of the repository :type repo: str """ if repo not in {"assignment", "source", "release"}: self.write_error(404) commit_message = self.get_argument("commit-message", None) submit = self.get_argument("submit", "false") == "true" if repo == "source" and (commit_message is None or commit_message == ""): self.write_error(400) try: lecture = await self.request_service.request( "GET", f"{self.service_base_url}/lectures/{lecture_id}", header=self.grader_authentication_header, ) assignment = await self.request_service.request( "GET", f"{self.service_base_url}/lectures/{lecture_id}/assignments/{assignment_id}", header=self.grader_authentication_header, ) except HTTPClientError as e: self.set_status(e.code) self.write_error(e.code) return git_service = GitService( server_root_dir=self.root_dir, lecture_code=lecture["code"], assignment_id=assignment["id"], repo_type=repo, config=self.config, ) if repo == "release": git_service.delete_repo_contents(include_git=True) src_path = GitService( self.root_dir, lecture["code"], assignment["id"], repo_type="source", config=self.config, ).path git_service.copy_repo_contents(src=src_path) # call nbconvert before pushing generator = GenerateAssignment( input_dir=src_path, output_dir=git_service.path, file_pattern="*.ipynb" ) generator.force = True self.log.info("Starting GenerateAssignment converter") try: generator.start() self.log.info("GenerateAssignment conversion done") except GraderConvertException as e: self.log.error("Converting failed: Error converting notebook!", exc_info=True) try: msg = e.args[0] assert isinstance(msg, str) except (KeyError, AssertionError): msg = "Converting release version failed!" raise HTTPError(400, message=msg) try: gradebook_path = os.path.join(git_service.path, "gradebook.json") self.log.info(f"Reading gradebook file: {gradebook_path}") with open(gradebook_path, "r") as f: gradebook_json: dict = json.load(f) except FileNotFoundError: self.log.error(f"Cannot find gradebook file: {gradebook_path}") return self.log.info(f"Setting properties of assignment from {gradebook_path}") response: HTTPResponse = await self.request_service.request( "PUT", f"{self.service_base_url}/lectures/{lecture_id}/assignments/{assignment_id}/properties", header=self.grader_authentication_header, body=gradebook_json, decode_response=False, ) if response.code == 200: self.log.info("Properties set for assignment") else: self.log.error( f"Could not set assignment properties! Error code {response.code}" ) try: os.remove(gradebook_path) self.log.info(f"Successfully deleted {gradebook_path}") except OSError as e: self.log.error( f"Cannot delete {gradebook_path}! Error: {e.strerror}\nAborting push!" ) return self.log.info(f"File contents of {repo}: {git_service.path}") self.log.info(",".join(os.listdir(git_service.path))) try: if not git_service.is_git(): git_service.init() git_service.set_author() # TODO: create .gitignore file git_service.set_remote(f"grader_{repo}") except GitError as e: self.log.error("GitError:\n" + e.error) self.write_error(400) return try: git_service.commit(m=commit_message) except GitError as e: self.log.error("GitError:\n" + e.error) try: git_service.push(f"grader_{repo}", force=True) except GitError as e: self.log.error("GitError:\n" + e.error) self.write_error(400) return if submit and repo == "assignment": self.log.info(f"Submitting assignment {assignment_id}!") try: latest_commit_hash = git_service.get_log(history_count=1)[0]["commit"] submission = Submission(commit_hash=latest_commit_hash) await self.request_service.request( "POST", f"{self.service_base_url}/lectures/{lecture_id}/assignments/{assignment_id}/submissions", body=submission.to_dict(), header=self.grader_authentication_header, ) except (KeyError, IndexError): self.set_status(500) self.write_error(500) return except HTTPClientError as e: self.set_status(e.code) self.write_error(e.code) return self.write("OK")
[docs]@register_handler( path=r"\/lectures\/(?P<lecture_id>\d*)\/assignments\/(?P<assignment_id>\d*)\/reset\/?" ) class ResetHandler(ExtensionBaseHandler): """ Tornado Handler class for http requests to /lectures/{lecture_id}/assignments/{assignment_id}/reset. """
[docs] async def get(self, lecture_id: int, assignment_id: int): """ Sends a GET request to the grader service that resets the user repo. :param lecture_id: id of the lecture :param assignment_id: id of the assignment :return: void """ try: await self.request_service.request( "GET", f"{self.service_base_url}/lectures/{lecture_id}/assignments/{assignment_id}/reset", header=self.grader_authentication_header, ) except HTTPClientError as e: self.set_status(e.code) self.write_error(e.code) return self.write("OK")
[docs]@register_handler( path=r"\/(?P<lecture_id>\d*)\/(?P<assignment_id>\d*)\/(?P<notebook_name>.*)" ) class NotebookAccessHandler(ExtensionBaseHandler): """ Tornado Handler class for http requests to /lectures/{lecture_id}/assignments/{assignment_id}/{notebook_name}. """
[docs] async def get(self, lecture_id: int, assignment_id: int, notebook_name: str): """ Sends a GET request to the grader service to access notebook and redirect to it. :param lecture_id: id of the lecture :param assignment_id: id of the assignment :param notebook_name: notebook name :return: void """ notebook_name = unquote(notebook_name) try: lecture = await self.request_service.request( "GET", f"{self.service_base_url}/lectures/{lecture_id}", header=self.grader_authentication_header, ) assignment = await self.request_service.request( "GET", f"{self.service_base_url}/lectures/{lecture_id}/assignments/{assignment_id}", header=self.grader_authentication_header, ) except HTTPClientError as e: self.set_status(e.code) self.write_error(e.code) return git_service = GitService( server_root_dir=self.root_dir, lecture_code=lecture["code"], assignment_id=assignment["id"], repo_type="release", config=self.config, force_user_repo=True, ) if not git_service.is_git(): try: git_service.init() git_service.set_author() git_service.set_remote(f"grader_release") git_service.pull(f"grader_release", force=True) self.write("OK") except GitError as e: self.log.error("GitError:\n" + e.error) self.write_error(400) # http://128.130.202.214:8080/user/ubuntu/lab/tree/20wle2/Assignment%201/6%20-%20Truth%20Tables.ipynb try: username = self.get_current_user()["name"] except TypeError as e: self.log.error(e) self.write_error(403) return url = f'/user/{username}/lab/tree/{lecture["code"]}/{assignment["id"]}/{quote(notebook_name)}' self.log.info(f"Redirecting to {url}") self.redirect(url)