Skip to content
This repository has been archived by the owner on Jan 22, 2025. It is now read-only.

Commit

Permalink
Merge branch 'development' into frontend/feature/student-project-view
Browse files Browse the repository at this point in the history
  • Loading branch information
AronBuzogany committed Apr 9, 2024
2 parents 0ce2cc3 + bdde5d7 commit 1b0c7c3
Show file tree
Hide file tree
Showing 39 changed files with 764 additions and 594 deletions.
13 changes: 12 additions & 1 deletion backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
FROM docker:dind as builder

FROM python:3.9

COPY --from=builder /usr/local/bin/docker /usr/local/bin/docker

RUN mkdir /app
WORKDIR /app

ADD ./project /app/

COPY requirements.txt /app/requirements.txt

RUN pip3 install -r requirements.txt

COPY . /app

ENTRYPOINT ["python"]
CMD ["__main__.py"]

CMD ["__main__.py"]
26 changes: 19 additions & 7 deletions backend/Dockerfile.test
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
FROM python:3.9-slim
FROM docker:dind

# Set the working directory
WORKDIR /app
RUN apk add --no-cache \
python3 \
py3-pip \
tzdata

ENV TZ=UTC
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone



RUN python3 -m venv /venv
ENV PATH="/venv/bin:$PATH"

# Copy the application code into the container
COPY . /app

# Install dependencies
RUN apt-get update
RUN apt-get install -y --no-install-recommends python3-pip
WORKDIR /app

RUN pip3 install --no-cache-dir -r requirements.txt -r dev-requirements.txt

RUN chmod +x /app/entry-point.sh

ENTRYPOINT ["/app/entry-point.sh"]
10 changes: 6 additions & 4 deletions backend/db_construct.sql
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,19 @@ CREATE TABLE course_students (
PRIMARY KEY(course_id, uid)
);

CREATE TYPE deadline AS(
description TEXT,
deadline TIMESTAMP WITH TIME ZONE
);

CREATE TABLE projects (
project_id INT GENERATED ALWAYS AS IDENTITY,
title VARCHAR(50) NOT NULL,
description TEXT NOT NULL,
assignment_file VARCHAR(50),
deadline TIMESTAMP WITH TIME ZONE,
deadlines deadline[],
course_id INT NOT NULL,
visible_for_students BOOLEAN NOT NULL,
archived BOOLEAN NOT NULL,
test_path VARCHAR(50),
script_name VARCHAR(50),
regex_expressions VARCHAR(50)[],
PRIMARY KEY(project_id),
CONSTRAINT fk_course FOREIGN KEY(course_id) REFERENCES courses(course_id) ON DELETE CASCADE
Expand Down
12 changes: 12 additions & 0 deletions backend/entry-point.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/bin/sh
# Start the Docker daemon in the background
dockerd-entrypoint.sh &

# Wait for the Docker daemon to start
until docker info; do
echo "Waiting for Docker daemon to start..."
sleep 1
done

# Execute the command passed to the docker run command
exec "$@"
20 changes: 15 additions & 5 deletions backend/project/endpoints/projects/endpoint_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Parser for the argument when posting or patching a project
"""

import json
from flask_restful import reqparse
from werkzeug.datastructures import FileStorage

Expand All @@ -14,7 +15,7 @@
help='Projects assignment file',
location="form"
)
parser.add_argument("deadline", type=str, help='Projects deadline', location="form")
parser.add_argument('deadlines', type=str, help='Projects deadlines', location="form")
parser.add_argument("course_id", type=str, help='Projects course_id', location="form")
parser.add_argument(
"visible_for_students",
Expand All @@ -23,8 +24,6 @@
location="form"
)
parser.add_argument("archived", type=bool, help='Projects', location="form")
parser.add_argument("test_path", type=str, help='Projects test path', location="form")
parser.add_argument("script_name", type=str, help='Projects test script path', location="form")
parser.add_argument(
"regex_expressions",
type=str,
Expand All @@ -39,9 +38,20 @@ def parse_project_params():
"""
args = parser.parse_args()
result_dict = {}

for key, value in args.items():
if value is not None:
result_dict[key] = value
if "deadlines" == key:
deadlines_parsed = json.loads(value)
new_deadlines = []
for deadline in deadlines_parsed:
new_deadlines.append(
(
deadline["description"],
deadline["deadline"]
)
)
result_dict[key] = new_deadlines
else:
result_dict[key] = value

return result_dict
2 changes: 1 addition & 1 deletion backend/project/endpoints/projects/project_detail.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def patch(self, project_id):
zip_location = os.path.join(project_upload_directory, filename)
with zipfile.ZipFile(zip_location) as upload_zip:
upload_zip.extractall(project_upload_directory)
project_json["assignment_file"] = filename

except zipfile.BadZipfile:
db.session.rollback()
return ({
Expand Down
9 changes: 4 additions & 5 deletions backend/project/endpoints/projects/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def get(self, teacher_id=None):
return query_selected_from_model(
Project,
response_url,
select_values=["project_id", "title", "description", "deadline"],
select_values=["project_id", "title", "description", "deadlines"],
url_mapper={"project_id": response_url},
filters=request.args
)
Expand All @@ -55,7 +55,6 @@ def post(self, teacher_id=None):
if "assignment_file" in request.files:
file = request.files["assignment_file"]
filename = os.path.basename(file.filename)
project_json["assignment_file"] = filename

# save the file that is given with the request
try:
Expand All @@ -81,9 +80,9 @@ def post(self, teacher_id=None):
os.makedirs(project_upload_directory, exist_ok=True)
if filename is not None:
try:
file.save(os.path.join(project_upload_directory, filename))
zip_location = os.path.join(project_upload_directory, filename)
with zipfile.ZipFile(zip_location) as upload_zip:
file_path = os.path.join(project_upload_directory, filename)
file.save(file_path)
with zipfile.ZipFile(file_path) as upload_zip:
upload_zip.extractall(project_upload_directory)
except zipfile.BadZipfile:
os.remove(os.path.join(project_upload_directory, filename))
Expand Down
16 changes: 12 additions & 4 deletions backend/project/models/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from dataclasses import dataclass
from sqlalchemy import ARRAY, Boolean, Column, DateTime, ForeignKey, Integer, String, Text
from sqlalchemy_utils import CompositeType
from project.db_in import db

@dataclass
Expand All @@ -23,11 +24,18 @@ class Project(db.Model): # pylint: disable=too-many-instance-attributes
project_id: int = Column(Integer, primary_key=True)
title: str = Column(String(50), nullable=False, unique=False)
description: str = Column(Text, nullable=False)
assignment_file: str = Column(String(50))
deadline: str = Column(DateTime(timezone=True))
deadlines: list = Column(ARRAY(
CompositeType(
"deadline",
[
Column("description", Text),
Column("deadline", DateTime(timezone=True))
]
),
dimensions=1
)
)
course_id: int = Column(Integer, ForeignKey("courses.course_id"), nullable=False)
visible_for_students: bool = Column(Boolean, nullable=False)
archived: bool = Column(Boolean, nullable=False)
test_path: str = Column(String(50))
script_name: str = Column(String(50))
regex_expressions: list[str] = Column(ARRAY(String(50)))
Empty file.
Empty file.
97 changes: 97 additions & 0 deletions backend/project/utils/submissions/evaluator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
"""
This module is responsible for evaluating the submission.
It uses docker to run the evaluator in a container.
The image used for the container is determined by the evaluator argument.
If the evaluator is not found in the
DOCKER_IMAGE_MAPPER, the project test path is used as the image.
The evaluator is run in the container and the
exit code is returned. The output of the evaluator is written to a log file
in the submission output folder.
"""
from os import path
import docker
from project.models.submission import Submission

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


def evaluate(submission: Submission, project_path: str, evaluator: str) -> int:
"""
Evaluate a submission using the evaluator.
Args:
submission (Submissions): The submission to evaluate.
project_path (str): The path to the project.
evaluator (str): The evaluator to use.
Returns:
int: The exit code of the evaluator.
Raises:
ValueError: If the evaluator is not found in the DOCKER_IMAGE_MAPPER
and the project test path does not exist.
"""

docker_image = DOCKER_IMAGE_MAPPER.get(evaluator, None)
if docker_image is None:
docker_image = project_path
if not path.exists(docker_image):
raise ValueError(f"Test path: {docker_image},\
not found and the provided evaluator:\
{evaluator} is not associated with any image.")

submission_path = submission.submission_path
submission_solution_path = path.join(submission_path, "submission")

container = create_and_run_evaluator(docker_image,
submission.submission_id,
project_path,
submission_solution_path)

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

exit_code = container.wait()

with open(path.join(test_output_path), "w", encoding='utf-8') as output_file:
output_file.write(container.logs().decode('utf-8'))

container.remove()

return exit_code['StatusCode']

def create_and_run_evaluator(docker_image: str,
submission_id: int,
project_path: str,
submission_solution_path: str):
"""
Create and run the evaluator container.
Args:
docker_image (str): The path to the docker image.
submission_id (int): The id of the submission.
project_path (str): The path to the project.
submission_solution_path (str): The path to the submission solution.
Returns:
docker.models.containers.Container: The container that is running the evaluator.
"""
client = docker.from_env()
image, _ = client.images.build(path=docker_image, tag=f"submission_{submission_id}")


container = client.containers.run(
image.id,
detach=True,
command="bash entry_point.sh",
volumes={
path.abspath(project_path): {'bind': "/tests", 'mode': 'rw'},
path.abspath(submission_solution_path): {'bind': "/submission", 'mode': 'rw'}
},
stderr=True,
stdout=True,
pids_limit=256
)
return container
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
FROM python:3.9-slim

COPY . .
40 changes: 40 additions & 0 deletions backend/project/utils/submissions/evaluators/python/entry_point.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/bin/bash


tests_manifest_file="/tests/req-manifest.txt"

if [ -f "$tests_manifest_file" ]; then
echo "Tests manifest file found. Installing tests requirements..."
pip3 install -r $tests_manifest_file &> /dev/null
else
echo "No tests manifest file found."
submission_requirements_file="/submission/requirements.txt"
if [ -f "$submission_requirements_file" ]; then
echo "Requirements file found. Installing requirements..."
pip3 install -r $submission_requirements_file &> /dev/null
else
echo "No requirements file found."
fi

submission_dev_requirements_file="/submission/dev-requirements.txt"

if [ -f "$submission_dev_requirements_file" ]; then
echo "Dev requirements file found. Installing dev requirements..."
pip3 install -r $submission_dev_requirements_file &> /dev/null
else
echo "No dev requirements file found."
fi

tests_requirements_file="/tests/requirements.txt"

if [ -f "$tests_requirements_file" ]; then
echo "Tests requirements file found. Installing tests requirements..."
pip3 install -r $tests_requirements_file &> /dev/null
else
echo "No tests requirements file found."
fi
fi

echo "Running tests..."
ls /submission
bash /tests/run_test.sh
Loading

0 comments on commit 1b0c7c3

Please sign in to comment.