Skip to content

Commit

Permalink
Merge branch 'development' into frontend/enhancement/error_pages
Browse files Browse the repository at this point in the history
  • Loading branch information
Matisse-Sulzer committed Apr 14, 2024
2 parents 8baecd8 + 710526e commit df75fd5
Show file tree
Hide file tree
Showing 10 changed files with 91 additions and 16 deletions.
2 changes: 2 additions & 0 deletions backend/project/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from flask import Flask
from flask_cors import CORS
from sqlalchemy_utils import register_composites
from .executor import executor
from .db_in import db
from .endpoints.index.index import index_bp
from .endpoints.users import users_bp
Expand All @@ -22,6 +23,7 @@ def create_app():
"""

app = Flask(__name__)
executor.init_app(app)
app.register_blueprint(index_bp)
app.register_blueprint(users_bp)
app.register_blueprint(courses_bp)
Expand Down
2 changes: 2 additions & 0 deletions backend/project/endpoints/projects/endpoint_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
location="form"
)

parser.add_argument("runner", type=str, help='Projects runner', location="form")


def parse_project_params():
"""
Expand Down
10 changes: 7 additions & 3 deletions backend/project/endpoints/projects/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@

from project.db_in import db

from project.models.project import Project
from project.models.project import Project, Runner
from project.utils.query_agent import query_selected_from_model, create_model_instance
from project.utils.authentication import authorize_teacher

from project.endpoints.projects.endpoint_parser import parse_project_params

API_URL = os.getenv('API_HOST')
UPLOAD_FOLDER = os.getenv('UPLOAD_URL')
UPLOAD_FOLDER = os.getenv("UPLOAD_FOLDER")


class ProjectsEndpoint(Resource):
Expand All @@ -43,7 +43,6 @@ def get(self, teacher_id=None):
filters=request.args
)

@authorize_teacher
def post(self, teacher_id=None):
"""
Post functionality for project
Expand Down Expand Up @@ -84,6 +83,11 @@ def post(self, teacher_id=None):
file.save(file_path)
with zipfile.ZipFile(file_path) as upload_zip:
upload_zip.extractall(project_upload_directory)

if not new_project.runner and \
os.path.exists(os.path.join(project_upload_directory, "Dockerfile")):

new_project.runner = Runner.CUSTOM
except zipfile.BadZipfile:
os.remove(os.path.join(project_upload_directory, filename))
db.session.rollback()
Expand Down
36 changes: 29 additions & 7 deletions backend/project/endpoints/submissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
from urllib.parse import urljoin
from datetime import datetime
from os import getenv, path, makedirs
from zoneinfo import ZoneInfo
from shutil import rmtree
from dotenv import load_dotenv
from flask import Blueprint, request
from flask_restful import Resource
from sqlalchemy import exc
from project.executor import executor
from project.db_in import db
from project.models.submission import Submission, SubmissionStatus
from project.models.project import Project
Expand All @@ -20,11 +22,15 @@
authorize_submissions_request, authorize_grader, \
authorize_student_submission, authorize_submission_author

from project.utils.submissions.evaluator import run_evaluator

load_dotenv()
API_HOST = getenv("API_HOST")
UPLOAD_FOLDER = getenv("UPLOAD_FOLDER")
BASE_URL = urljoin(f"{API_HOST}/", "/submissions")

TIMEZONE = getenv("TIMEZONE", "GMT")

submissions_bp = Blueprint("submissions", __name__)

class SubmissionsEndpoint(Resource):
Expand Down Expand Up @@ -105,10 +111,7 @@ def post(self) -> dict[str, any]:
submission.project_id = int(project_id)

# Submission time
submission.submission_time = datetime.now()

# Submission status
submission.submission_status = SubmissionStatus.RUNNING
submission.submission_time = datetime.now(ZoneInfo(TIMEZONE))

# Submission files
submission.submission_path = "" # Must be set on creation
Expand All @@ -123,6 +126,12 @@ def post(self) -> dict[str, any]:
f"(required files={','.join(project.regex_expressions)})"
return data, 400

deadlines = project.deadlines
is_late = True
for deadline in deadlines:
if submission.submission_time < deadline.deadline:
is_late = False

# Submission_id needed for the file location
session.add(submission)
session.commit()
Expand All @@ -132,13 +141,26 @@ def post(self) -> dict[str, any]:
"submissions", str(submission.submission_id))
try:
makedirs(submission.submission_path, exist_ok=True)
input_folder = path.join(submission.submission_path, "submission")
makedirs(input_folder, exist_ok=True)
for file in files:
file.save(path.join(submission.submission_path, file.filename))
session.commit()
file.save(path.join(input_folder, file.filename))
except OSError:
rmtree(submission.submission_path)
session.rollback()

if project.runner:
submission.submission_status = SubmissionStatus.RUNNING
executor.submit(
run_evaluator,
submission,
path.join(UPLOAD_FOLDER, str(project.project_id)),
project.runner.value,
False)
else:
submission.submission_status = SubmissionStatus.LATE if is_late \
else SubmissionStatus.SUCCESS

data["message"] = "Successfully fetched the submissions"
data["url"] = urljoin(f"{API_HOST}/", f"/submissions/{submission.submission_id}")
data["data"] = {
Expand All @@ -149,7 +171,7 @@ def post(self) -> dict[str, any]:
"submission_time": submission.submission_time,
"submission_status": submission.submission_status
}
return data, 201
return data, 202

except exc.SQLAlchemyError:
session.rollback()
Expand Down
Empty file.
9 changes: 9 additions & 0 deletions backend/project/executor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""
This file is used to create an instance of the Executor class from the flask_executor package.
This instance is used to create a background task that will run the evaluator.
This is done to prevent the server from being blocked while the model is being trained.
"""

from flask_executor import Executor

executor = Executor()
41 changes: 38 additions & 3 deletions backend/project/utils/submissions/evaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,18 @@
exit code is returned. The output of the evaluator is written to a log file
in the submission output folder.
"""
from os import path
from os import path, makedirs
import docker
from sqlalchemy.exc import SQLAlchemyError
from project.db_in import db
from project.models.submission import Submission

DOCKER_IMAGE_MAPPER = {
"python": path.join(path.dirname(__file__), "evaluators", "python"),
"PYTHON": path.join(path.dirname(__file__), "evaluators", "python"),
}


def evaluate(submission: Submission, project_path: str, evaluator: str) -> int:
def evaluate(submission: Submission, project_path: str, evaluator: str, is_late: bool) -> int:
"""
Evaluate a submission using the evaluator.
Expand Down Expand Up @@ -51,6 +53,7 @@ def evaluate(submission: Submission, project_path: str, evaluator: str) -> int:
submission_solution_path)

submission_output_path = path.join(submission_path, "output")
makedirs(submission_output_path, exist_ok=True)
test_output_path = path.join(submission_output_path, "test_output.log")

exit_code = container.wait()
Expand All @@ -62,6 +65,38 @@ def evaluate(submission: Submission, project_path: str, evaluator: str) -> int:

return exit_code['StatusCode']

def run_evaluator(submission: Submission, project_path: str, evaluator: str, is_late: bool) -> int:
"""
Run the evaluator for the submission.
Args:
submission (Submission): The submission to evaluate.
project_path (str): The path to the project.
evaluator (str): The evaluator to use.
is_late (bool): Whether the submission is late.
Returns:
int: The exit code of the evaluator.
"""
status_code = evaluate(submission, project_path, evaluator, is_late)

if not is_late:
if status_code == 0:
submission.submission_status = 'SUCCESS'
else:
submission.submission_status = 'FAIL'
else:
submission.submission_status = 'LATE'

try:
db.session.merge(submission)
db.session.commit()
except SQLAlchemyError:
pass

return status_code


def create_and_run_evaluator(docker_image: str,
submission_id: int,
project_path: str,
Expand Down
1 change: 1 addition & 0 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ SQLAlchemy~=2.0.27
requests>=2.31.0
waitress
flask_swagger_ui
flask_executor
6 changes: 3 additions & 3 deletions backend/tests/utils/submission_evaluators/python_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def evaluate_python(submission_root, project_path_succes):
project_id = 1
submission = Submission(submission_id=1, project_id=project_id)
submission.submission_path = create_submission_folder(submission_root, project_id)
return evaluate(submission, project_path_succes, "python"), submission.submission_path
return evaluate(submission, project_path_succes, "PYTHON", False), submission.submission_path

def prep_submission_and_clear_after_py(tc_folder: str) -> tuple[Submission, Project]:
"""
Expand Down Expand Up @@ -64,13 +64,13 @@ def test_logs_output(evaluate_python):
def test_with_dependency():
"""Test whether the evaluator works with a dependency."""
submission, project = prep_submission_and_clear_after_py("tc_2")
exit_code = evaluate(submission, project, "python")
exit_code = evaluate(submission, project, "PYTHON", False)
cleanup_after_test(submission.submission_path)
assert exit_code == 0

def test_dependency_manifest():
"""Test whether the evaluator works with a dependency manifest."""
submission, project = prep_submission_and_clear_after_py("tc_3")
exit_code = evaluate(submission, project, "python")
exit_code = evaluate(submission, project, "PYTHON", False)
cleanup_after_test(submission.submission_path)
assert exit_code != 0
Binary file not shown.

0 comments on commit df75fd5

Please sign in to comment.