From 92242cd12155fa4e1612c8694aed1bf1e515e28e Mon Sep 17 00:00:00 2001 From: Siebe Vlietinck <71773032+Vucis@users.noreply.github.com> Date: Thu, 23 May 2024 14:53:42 +0200 Subject: [PATCH 1/7] Fixed join code copy to clipboard (#404) * fixed join code copy * removed : --- frontend/src/components/Courses/CourseDetailTeacher.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/Courses/CourseDetailTeacher.tsx b/frontend/src/components/Courses/CourseDetailTeacher.tsx index 6ba52b63..80142e00 100644 --- a/frontend/src/components/Courses/CourseDetailTeacher.tsx +++ b/frontend/src/components/Courses/CourseDetailTeacher.tsx @@ -461,7 +461,7 @@ function JoinCodeMenu({ const handleCopyToClipboard = (join_code: string) => { const host = window.location.host; navigator.clipboard.writeText( - `${host}/${i18next.resolvedLanguage}/courses/join?code=${join_code}` + `${window.location.protocol}//${host}/${i18next.resolvedLanguage}/courses/join?code=${join_code}` ); }; From 1fbd30c647cae7cae414663208a04679b92692ad Mon Sep 17 00:00:00 2001 From: Cedric Mekeirle <143823820+JibrilExe@users.noreply.github.com> Date: Thu, 23 May 2024 14:55:45 +0200 Subject: [PATCH 2/7] Only return projects where u are student if visible for students is true (#403) * projects visible for students * lint --- backend/project/endpoints/projects/projects.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/backend/project/endpoints/projects/projects.py b/backend/project/endpoints/projects/projects.py index 27b603a3..94813bbd 100644 --- a/backend/project/endpoints/projects/projects.py +++ b/backend/project/endpoints/projects/projects.py @@ -43,13 +43,13 @@ def get(self, uid=None): } try: # Get all the courses a user is part of - courses = CourseStudent.query.filter_by(uid=uid).\ + courses_student = CourseStudent.query.filter_by(uid=uid).\ with_entities(CourseStudent.course_id).all() - courses += CourseAdmin.query.filter_by(uid=uid).\ + courses = CourseAdmin.query.filter_by(uid=uid).\ with_entities(CourseAdmin.course_id).all() courses += Course.query.filter_by(teacher=uid).with_entities(Course.course_id).all() courses = [c[0] for c in courses] # Remove the tuple wrapping the course_id - + courses_student = [c[0] for c in courses_student] # Filter the projects based on the query parameters filters = dict(request.args) conditions = [] @@ -62,6 +62,9 @@ def get(self, uid=None): projects = projects.filter(and_(*conditions)) if conditions else projects projects = projects.all() projects = [p for p in projects if get_course_of_project(p.project_id) in courses] + projects_student = Project.query.filter(Project.course_id.in_(courses_student)).all() + projects_student = [p for p in projects_student if p.visible_for_students] + projects += projects_student # Return the projects data["message"] = "Successfully fetched the projects" From 8509c1010e7c83b987c711cf366d7527c7d1a82a Mon Sep 17 00:00:00 2001 From: Gerwoud Van den Eynden <62761483+Gerwoud@users.noreply.github.com> Date: Thu, 23 May 2024 14:56:17 +0200 Subject: [PATCH 3/7] added conditional rendering for edit funtionality (#402) --- .../pages/project/projectView/ProjectView.tsx | 43 +++++++++---------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/frontend/src/pages/project/projectView/ProjectView.tsx b/frontend/src/pages/project/projectView/ProjectView.tsx index 8b1f8dd8..f5f5df09 100644 --- a/frontend/src/pages/project/projectView/ProjectView.tsx +++ b/frontend/src/pages/project/projectView/ProjectView.tsx @@ -195,10 +195,9 @@ export default function ProjectView() { { - !edit && <>{projectData.description} - } - { - edit && <> setDescription(event.target.value)}/> + !edit + ? <>{projectData.description} + : edit && <> setDescription(event.target.value)}/> } @@ -212,25 +211,23 @@ export default function ProjectView() { alignItems="flex-end" justifyContent="end" > - { - edit && ( - <> - - - - - + { me && me.role === "TEACHER" && ( + edit + ? ( + <> + + + + + + + ) + : ( + setEdit(true)}> + - - ) - } - { - !edit && ( - setEdit(true)}> - - - ) - } + ) + )} @@ -242,7 +239,7 @@ export default function ProjectView() { projectId={projectId} /> - {me && me.role == "TEACHER" && ( + {me && me.role === "TEACHER" && ( Date: Thu, 23 May 2024 14:57:53 +0200 Subject: [PATCH 4/7] added translation for homepage of user guide (#400) * added translation * fix --- .../current/intro.md | 42 +------------------ 1 file changed, 1 insertion(+), 41 deletions(-) diff --git a/documentation/i18n/nl/docusaurus-plugin-content-docs/current/intro.md b/documentation/i18n/nl/docusaurus-plugin-content-docs/current/intro.md index 7c253496..02c86b75 100644 --- a/documentation/i18n/nl/docusaurus-plugin-content-docs/current/intro.md +++ b/documentation/i18n/nl/docusaurus-plugin-content-docs/current/intro.md @@ -4,44 +4,4 @@ sidebar_position: 1 # Project user guide -If you need help using the you can read the user guide below. - -## Getting Started - -Get started by **creating a new site**. - -Or **try Docusaurus immediately** with **[docusaurus.new](https://docusaurus.new)**. - -### What you'll need - -- [Node.js](https://nodejs.org/en/download/) version 18.0 or above: - - When installing Node.js, you are recommended to check all checkboxes related to dependencies. - -## Generate a new site - -Generate a new Docusaurus site using the **classic template**. - -The classic template will automatically be added to your project after you run the command: - -```bash -npm init docusaurus@latest my-website classic -``` - -You can type this command into Command Prompt, Powershell, Terminal, or any other integrated terminal of your code editor. - -The command also installs all necessary dependencies you need to run Docusaurus. - -## Start your site - -Run the development server: - -```bash -cd my-website -npm run start -``` - -The `cd` command changes the directory you're working with. In order to work with your newly created Docusaurus site, you'll need to navigate the terminal there. - -The `npm run start` command builds your website locally and serves it through a development server, ready for you to view at http://localhost:3000/. - -Open `docs/intro.md` (this page) and edit some lines: the site **reloads automatically** and displays your changes. +Indien je hulp nodig hebt met het gebruik van projecct Péristeronas kan je de onderstaande gebruikershandleiding raadplegen. From d79bfd9b68d239945f704aad0e934a06f5907f1d Mon Sep 17 00:00:00 2001 From: Cedric Mekeirle <143823820+JibrilExe@users.noreply.github.com> Date: Thu, 23 May 2024 14:59:57 +0200 Subject: [PATCH 5/7] Groups backend (#338) * temp bad * db_constr, model and first attempt at endpoint for group * group prim key (group_id,project_id) added delete endpoint to leave groups, next up is test * allow students in max 1 group * model tests * lint * group menu frontend * hm * working endpoint for create and delete group * translations * begone front * front removal * lintr * fixed changes, untested tho * groups locked var should not mess up all older code * only student or teacher can get groups ; unlock groups * linter mad * Very mad lintr * vscode linter errors should be more obvi * removed some teacher_id = None * removed unused import * bad prints --- backend/db_construct.sql | 20 ++- .../projects/groups/group_student.py | 128 ++++++++++++++++++ .../endpoints/projects/groups/groups.py | 127 +++++++++++++++++ .../endpoints/projects/project_endpoint.py | 7 +- backend/project/models/group.py | 17 +++ backend/project/models/group_student.py | 13 ++ backend/project/models/project.py | 1 + backend/project/utils/authentication.py | 22 +++ backend/tests/conftest.py | 9 +- backend/tests/models/group_test.py | 52 +++++++ 10 files changed, 392 insertions(+), 4 deletions(-) create mode 100644 backend/project/endpoints/projects/groups/group_student.py create mode 100644 backend/project/endpoints/projects/groups/groups.py create mode 100644 backend/project/models/group.py create mode 100644 backend/project/models/group_student.py create mode 100644 backend/tests/models/group_test.py diff --git a/backend/db_construct.sql b/backend/db_construct.sql index b4614151..5b209431 100644 --- a/backend/db_construct.sql +++ b/backend/db_construct.sql @@ -20,8 +20,8 @@ CREATE TABLE courses ( ); CREATE TABLE course_join_codes ( - join_code UUID DEFAULT gen_random_uuid() NOT NULL, - course_id INT NOT NULL, + join_code UUID DEFAULT gen_random_uuid() NOT NULL, + course_id INT NOT NULL, expiry_time DATE, for_admins BOOLEAN NOT NULL, CONSTRAINT fk_course_join_link FOREIGN KEY(course_id) REFERENCES courses(course_id) ON DELETE CASCADE, @@ -53,12 +53,28 @@ CREATE TABLE projects ( course_id INT NOT NULL, visible_for_students BOOLEAN NOT NULL, archived BOOLEAN NOT NULL, + groups_locked BOOLEAN DEFAULT FALSE, regex_expressions VARCHAR(50)[], runner runner, PRIMARY KEY(project_id), CONSTRAINT fk_course FOREIGN KEY(course_id) REFERENCES courses(course_id) ON DELETE CASCADE ); +CREATE TABLE groups ( + group_id INT GENERATED ALWAYS AS IDENTITY, + project_id INT NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE, + group_size INT NOT NULL, + PRIMARY KEY(project_id, group_id) +); + +CREATE TABLE group_students ( + uid VARCHAR(255) NOT NULL REFERENCES users(uid) ON DELETE CASCADE, + group_id INT NOT NULL, + project_id INT NOT NULL, + PRIMARY KEY(uid, group_id, project_id), + CONSTRAINT fk_group_reference FOREIGN KEY (group_id, project_id) REFERENCES groups(group_id, project_id) ON DELETE CASCADE +); + CREATE TABLE submissions ( submission_id INT GENERATED ALWAYS AS IDENTITY, uid VARCHAR(255) NOT NULL, diff --git a/backend/project/endpoints/projects/groups/group_student.py b/backend/project/endpoints/projects/groups/group_student.py new file mode 100644 index 00000000..6a148a1f --- /dev/null +++ b/backend/project/endpoints/projects/groups/group_student.py @@ -0,0 +1,128 @@ +"""Endpoint for joining and leaving groups in a project""" + + +from os import getenv +from urllib.parse import urljoin +from dotenv import load_dotenv +from flask import request +from flask_restful import Resource +from sqlalchemy.exc import SQLAlchemyError + +from project.utils.query_agent import insert_into_model +from project.models.group import Group +from project.models.project import Project +from project.utils.authentication import authorize_student_submission + +from project import db + +load_dotenv() +API_URL = getenv("API_HOST") +RESPONSE_URL = urljoin(f"{API_URL}/", "groups") + + +class GroupStudent(Resource): + """Api endpoint to allow students to join and leave project groups""" + @authorize_student_submission + def post(self, project_id, group_id, uid=None): + """ + This function will allow students to join project groups if not full + """ + try: + project = db.session.query(Project).filter_by( + project_id=project_id).first() + if project.groups_locked: + return { + "message": "Groups are locked for this project", + "url": RESPONSE_URL + }, 400 + + group = db.session.query(Group).filter_by( + project_id=project_id, group_id=group_id).first() + if group is None: + return { + "message": "Group does not exist", + "url": RESPONSE_URL + }, 404 + + joined_groups = db.session.query(GroupStudent).filter_by( + uid=uid, project_id=project_id).all() + if len(joined_groups) > 0: + return { + "message": "Student is already in a group", + "url": RESPONSE_URL + }, 400 + + joined_students = db.session.query(GroupStudent).filter_by( + group_id=group_id, project_id=project_id).all() + if len(joined_students) >= group.group_size: + return { + "message": "Group is full", + "url": RESPONSE_URL + }, 400 + + req = request.json + req["project_id"] = project_id + req["group_id"] = group_id + req["uid"] = uid + return insert_into_model( + GroupStudent, + req, + RESPONSE_URL, + "group_id", + required_fields=["project_id", "group_id", "uid"] + ) + except SQLAlchemyError: + data = { + "url": urljoin(f"{API_URL}/", "projects") + } + data["message"] = "An error occurred while fetching the projects" + return data, 500 + + + @authorize_student_submission + def delete(self, project_id, group_id, uid=None): + """ + This function will allow students to leave project groups + """ + data = { + "url": urljoin(f"{API_URL}/", "projects") + } + try: + project = db.session.query(Project).filter_by( + project_id=project_id).first() + if project.groups_locked: + return { + "message": "Groups are locked for this project", + "url": RESPONSE_URL + }, 400 + + group = db.session.query(Group).filter_by( + project_id=project_id, group_id=group_id).first() + if group is None: + return { + "message": "Group does not exist", + "url": RESPONSE_URL + }, 404 + + if uid is None: + return { + "message": "Failed to verify uid of user", + "url": RESPONSE_URL + }, 400 + + student_group = db.session.query(GroupStudent).filter_by( + group_id=group_id, project_id=project_id, uid=uid).first() + if student_group is None: + return { + "message": "Student is not in the group", + "url": RESPONSE_URL + }, 404 + + db.session.delete(student_group) + db.session.commit() + data["message"] = "Student has succesfully left the group" + return data, 200 + + except SQLAlchemyError: + data["message"] = "An error occurred while fetching the projects" + return data, 500 diff --git a/backend/project/endpoints/projects/groups/groups.py b/backend/project/endpoints/projects/groups/groups.py new file mode 100644 index 00000000..a6b070e0 --- /dev/null +++ b/backend/project/endpoints/projects/groups/groups.py @@ -0,0 +1,127 @@ +"""Endpoint for creating/deleting groups in a project""" +from os import getenv +from urllib.parse import urljoin +from dotenv import load_dotenv +from flask import request +from flask_restful import Resource +from sqlalchemy.exc import SQLAlchemyError + +from project.models.project import Project +from project.models.group import Group +from project.utils.query_agent import query_selected_from_model, insert_into_model +from project.utils.authentication import ( + authorize_teacher_or_student_of_project, + authorize_teacher_of_project +) +from project import db + +load_dotenv() +API_URL = getenv("API_HOST") +RESPONSE_URL = urljoin(f"{API_URL}/", "groups") + + +class Groups(Resource): + """Api endpoint for the /project/project_id/groups link""" + + @authorize_teacher_of_project + def patch(self, project_id): + """ + This function will set locked state of project groups, + need to pass locked field in the body + """ + req = request.json + locked = req.get("locked") + if locked is None: + return { + "message": "Bad request: locked field is required", + "url": RESPONSE_URL + }, 400 + + try: + project = db.session.query(Project).filter_by( + project_id=project_id).first() + if project is None: + return { + "message": "Project does not exist", + "url": RESPONSE_URL + }, 404 + project.groups_locked = locked + db.session.commit() + + return { + "message": "Groups are locked", + "url": RESPONSE_URL + }, 200 + except SQLAlchemyError: + return { + "message": "Database error", + "url": RESPONSE_URL + }, 500 + + @authorize_teacher_or_student_of_project + def get(self, project_id): + """ + Get function for /project/project_id/groups this will be the main endpoint + to get all groups for a project + """ + return query_selected_from_model( + Group, + RESPONSE_URL, + url_mapper={"group_id": RESPONSE_URL}, + filters={"project_id": project_id} + ) + + @authorize_teacher_of_project + def post(self, project_id): + """ + This function will create a new group for a project + if the body of the post contains a group_size and project_id exists + """ + + req = request.json + req["project_id"] = project_id + return insert_into_model( + Group, + req, + RESPONSE_URL, + "group_id", + required_fields=["project_id", "group_size"] + ) + + @authorize_teacher_of_project + def delete(self, project_id): + """ + This function will delete a group + if group_id is provided and request is from teacher + """ + + req = request.json + group_id = req.get("group_id") + if group_id is None: + return { + "message": "Bad request: group_id is required", + "url": RESPONSE_URL + }, 400 + + try: + project = db.session.query(Project).filter_by( + project_id=project_id).first() + if project is None: + return { + "message": "Project associated with group does not exist", + "url": RESPONSE_URL + }, 404 + + group = db.session.query(Group).filter_by( + project_id=project_id, group_id=group_id).first() + db.session.delete(group) + db.session.commit() + return { + "message": "Group deleted", + "url": RESPONSE_URL + }, 204 + except SQLAlchemyError: + return { + "message": "Database error", + "url": RESPONSE_URL + }, 500 diff --git a/backend/project/endpoints/projects/project_endpoint.py b/backend/project/endpoints/projects/project_endpoint.py index 6fa7510a..2c9a09b9 100644 --- a/backend/project/endpoints/projects/project_endpoint.py +++ b/backend/project/endpoints/projects/project_endpoint.py @@ -10,7 +10,7 @@ from project.endpoints.projects.project_assignment_file import ProjectAssignmentFiles from project.endpoints.projects.project_submissions_download import SubmissionDownload from project.endpoints.projects.project_last_submission import SubmissionPerUser - +from project.endpoints.projects.groups.groups import Groups project_bp = Blueprint('project_endpoint', __name__) @@ -38,3 +38,8 @@ '/projects//latest-per-user', view_func=SubmissionPerUser.as_view('latest_per_user') ) + +project_bp.add_url_rule( + '/projects//groups', + view_func=Groups.as_view('groups') +) diff --git a/backend/project/models/group.py b/backend/project/models/group.py new file mode 100644 index 00000000..fca8060f --- /dev/null +++ b/backend/project/models/group.py @@ -0,0 +1,17 @@ +"""Group model""" +from dataclasses import dataclass +from sqlalchemy import Integer, Column, ForeignKey +from project import db + + +@dataclass +class Group(db.Model): + """ + This class will contain the model for the groups + """ + __tablename__ = "groups" + + group_id: int = Column(Integer, autoincrement=True, primary_key=True) + project_id: int = Column(Integer, ForeignKey( + "projects.project_id"), autoincrement=False, primary_key=True) + group_size: int = Column(Integer, nullable=False) diff --git a/backend/project/models/group_student.py b/backend/project/models/group_student.py new file mode 100644 index 00000000..57a337a2 --- /dev/null +++ b/backend/project/models/group_student.py @@ -0,0 +1,13 @@ +"""Model for relation between groups and students""" +from dataclasses import dataclass +from sqlalchemy import Integer, Column, ForeignKey, String +from project.db_in import db + +@dataclass +class GroupStudent(db.Model): + """Model for relation between groups and students""" + __tablename__ = "group_students" + + uid: str = Column(String(255), ForeignKey("users.uid"), primary_key=True) + group_id: int = Column(Integer, ForeignKey("groups.group_id"), primary_key=True) + project_id: int = Column(Integer, ForeignKey("groups.project_id"), primary_key=True) diff --git a/backend/project/models/project.py b/backend/project/models/project.py index 75e425e6..788864b0 100644 --- a/backend/project/models/project.py +++ b/backend/project/models/project.py @@ -55,6 +55,7 @@ class Project(db.Model): # pylint: disable=too-many-instance-attributes 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) + groups_locked: bool = Column(Boolean) runner: Runner = Column( EnumField(Runner, name="runner"), nullable=False) diff --git a/backend/project/utils/authentication.py b/backend/project/utils/authentication.py index 30a79d68..75bbe08f 100644 --- a/backend/project/utils/authentication.py +++ b/backend/project/utils/authentication.py @@ -50,6 +50,7 @@ def wrap(*args, **kwargs): return f(*args, **kwargs) return wrap + def login_required_return_uid(f): """ This function will check if the person sending a request to the API is logged in @@ -62,6 +63,7 @@ def wrap(*args, **kwargs): return f(*args, **kwargs) return wrap + def authorize_admin(f): """ This function will check if the person sending a request to the API is logged in and an admin. @@ -169,6 +171,26 @@ def wrap(*args, **kwargs): return wrap +def authorize_teacher_or_student_of_project(f): + """ + This function will check if the person sending a request to the API is logged in, + and the teacher or student of the course which the project in the request belongs to. + Returns 403: Not Authorized if either condition is false + """ + @wraps(f) + def wrap(*args, **kwargs): + auth_user_id = return_authenticated_user_id() + project_id = kwargs["project_id"] + course_id = get_course_of_project(project_id) + + if (is_teacher_of_course(auth_user_id, course_id) or + is_student_of_course(auth_user_id, course_id)): + return f(*args, **kwargs) + + abort(make_response(({"message": """You are not authorized to perfom this action, + you are not the teacher OR student of this project"""}, 403))) + return wrap + def authorize_teacher_or_project_admin(f): """ This function will check if the person sending a request to the API is logged in, diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 8404d35f..8a3f8ff0 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -17,7 +17,7 @@ from project.models.project import Project from project.models.course_relation import CourseStudent,CourseAdmin from project.models.submission import Submission, SubmissionStatus - +from project.models.group import Group ### CLIENT & SESSION ### @@ -53,6 +53,8 @@ def session() -> Generator[Session, any, None]: session.commit() session.add_all(submissions(session)) session.commit() + session.add(group(session)) + session.commit() yield session finally: @@ -199,3 +201,8 @@ def submissions(session): submission_status= SubmissionStatus.SUCCESS ) ] + +def group(session): + """Return a group to populate the database""" + project_id = session.query(Project).filter_by(title="B+ Trees").first().project_id + return Group(project_id=project_id, group_size=4) diff --git a/backend/tests/models/group_test.py b/backend/tests/models/group_test.py new file mode 100644 index 00000000..1844fe9e --- /dev/null +++ b/backend/tests/models/group_test.py @@ -0,0 +1,52 @@ +"""Tests for the Group and GroupStudent model""" +from sqlalchemy.orm import Session + +from project.models.project import Project +from project.models.group import Group +from project.models.group_student import GroupStudent +from project.models.user import User + + +class TestGroupModel: + """Test class for Group and GroupStudent tests""" + + def test_group_model(self, session: Session): + "Group create test" + project = session.query(Project).first() + group = Group(project_id=project.project_id, group_size=4) + session.add(group) + session.commit() + assert session.query(Group).filter_by( + group_id=group.group_id, project_id=project.project_id) is not None + assert session.query(Group).first().group_size == 4 + + def test_group_join(self, session: Session): + """Group join test""" + project = session.query(Project).filter_by(title="B+ Trees").first() + group = session.query(Group).filter_by( + project_id=project.project_id).first() + student = session.query(User).first() + + student_group = GroupStudent( + group_id=group.group_id, uid=student.uid, project_id=project.project_id) + session.add(student_group) + session.commit() + assert session.query(GroupStudent).first().uid == student.uid + + def test_group_leave(self, session: Session): + """Group leave test""" + project = session.query(Project).filter_by(title="B+ Trees").first() + group = session.query(Group).filter_by( + project_id=project.project_id).first() + student = session.query(User).first() + + student_group = GroupStudent( + group_id=group.group_id, uid=student.uid, project_id=project.project_id) + session.add(student_group) + session.commit() + + session.delete(student_group) + session.commit() + + assert session.query(GroupStudent).filter_by( + uid=student.uid, group_id=group.group_id, project_id=project.project_id).first() is None From 253ab7b6458cc8e34a7bf498480f05b788639fc7 Mon Sep 17 00:00:00 2001 From: Aron Buzogany <108480125+AronBuzogany@users.noreply.github.com> Date: Thu, 23 May 2024 15:13:02 +0200 Subject: [PATCH 6/7] unzipping submissions on submission (#406) * unzipping submissions * run_test -> run_tests * run_test -> run_tests --- .../endpoints/submissions/submissions.py | 11 +++++++++-- .../project/utils/submissions/evaluator.py | 19 +++++++++++-------- .../evaluators/general/entry_point.sh | 2 +- .../evaluators/python/entry_point.sh | 2 +- .../assignment/{run_test.sh => run_tests.sh} | 0 .../assignment/{run_test.sh => run_tests.sh} | 0 .../assignment/{run_test.sh => run_tests.sh} | 0 7 files changed, 22 insertions(+), 12 deletions(-) rename backend/tests/utils/submission_evaluators/resources/python/tc_1/assignment/{run_test.sh => run_tests.sh} (100%) rename backend/tests/utils/submission_evaluators/resources/python/tc_2/assignment/{run_test.sh => run_tests.sh} (100%) rename backend/tests/utils/submission_evaluators/resources/python/tc_3/assignment/{run_test.sh => run_tests.sh} (100%) diff --git a/backend/project/endpoints/submissions/submissions.py b/backend/project/endpoints/submissions/submissions.py index a47ca63b..b03490fe 100644 --- a/backend/project/endpoints/submissions/submissions.py +++ b/backend/project/endpoints/submissions/submissions.py @@ -7,6 +7,7 @@ from datetime import datetime from zoneinfo import ZoneInfo from shutil import rmtree +import zipfile from flask import request from flask_restful import Resource from sqlalchemy import exc, and_ @@ -104,7 +105,7 @@ def get(self, uid=None) -> dict[str, any]: return data, 500 @authorize_student_submission - def post(self, uid=None) -> dict[str, any]: + def post(self, uid=None) -> dict[str, any]: # pylint: disable=too-many-locals, too-many-branches, too-many-statements """Post a new submission to a project Returns: @@ -174,7 +175,13 @@ def post(self, uid=None) -> dict[str, any]: input_folder = path.join(submission.submission_path, "submission") makedirs(input_folder, exist_ok=True) for file in files: - file.save(path.join(input_folder, file.filename)) + file_path = path.join(input_folder, file.filename) + file.save(file_path) + if file.filename.endswith(".zip"): + with zipfile.ZipFile(file_path) as upload_zip: + upload_zip.extractall(input_folder) + + except OSError: rmtree(submission.submission_path) session.rollback() diff --git a/backend/project/utils/submissions/evaluator.py b/backend/project/utils/submissions/evaluator.py index 4a772cdd..50e75dc0 100644 --- a/backend/project/utils/submissions/evaluator.py +++ b/backend/project/utils/submissions/evaluator.py @@ -82,15 +82,18 @@ def run_evaluator(submission: Submission, project_path: str, evaluator: str, is_ 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' + try: + 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 = 'FAIL' - else: - submission.submission_status = 'LATE' + submission.submission_status = 'LATE' + except: # pylint: disable=bare-except + submission.submission_status = 'FAIL' + try: db.session.merge(submission) diff --git a/backend/project/utils/submissions/evaluators/general/entry_point.sh b/backend/project/utils/submissions/evaluators/general/entry_point.sh index 9cdc7a66..51758446 100644 --- a/backend/project/utils/submissions/evaluators/general/entry_point.sh +++ b/backend/project/utils/submissions/evaluators/general/entry_point.sh @@ -1,3 +1,3 @@ #!/bin/bash -bash /tests/run_test.sh +bash /tests/run_tests.sh diff --git a/backend/project/utils/submissions/evaluators/python/entry_point.sh b/backend/project/utils/submissions/evaluators/python/entry_point.sh index e24a883a..4518c789 100644 --- a/backend/project/utils/submissions/evaluators/python/entry_point.sh +++ b/backend/project/utils/submissions/evaluators/python/entry_point.sh @@ -37,4 +37,4 @@ fi echo "Running tests..." ls /submission -bash /tests/run_test.sh \ No newline at end of file +bash /tests/run_tests.sh \ No newline at end of file diff --git a/backend/tests/utils/submission_evaluators/resources/python/tc_1/assignment/run_test.sh b/backend/tests/utils/submission_evaluators/resources/python/tc_1/assignment/run_tests.sh similarity index 100% rename from backend/tests/utils/submission_evaluators/resources/python/tc_1/assignment/run_test.sh rename to backend/tests/utils/submission_evaluators/resources/python/tc_1/assignment/run_tests.sh diff --git a/backend/tests/utils/submission_evaluators/resources/python/tc_2/assignment/run_test.sh b/backend/tests/utils/submission_evaluators/resources/python/tc_2/assignment/run_tests.sh similarity index 100% rename from backend/tests/utils/submission_evaluators/resources/python/tc_2/assignment/run_test.sh rename to backend/tests/utils/submission_evaluators/resources/python/tc_2/assignment/run_tests.sh diff --git a/backend/tests/utils/submission_evaluators/resources/python/tc_3/assignment/run_test.sh b/backend/tests/utils/submission_evaluators/resources/python/tc_3/assignment/run_tests.sh similarity index 100% rename from backend/tests/utils/submission_evaluators/resources/python/tc_3/assignment/run_test.sh rename to backend/tests/utils/submission_evaluators/resources/python/tc_3/assignment/run_tests.sh From df9587f426b6b07deed23c6542015357f1a55546 Mon Sep 17 00:00:00 2001 From: Siebe Vlietinck <71773032+Vucis@users.noreply.github.com> Date: Thu, 23 May 2024 15:35:11 +0200 Subject: [PATCH 7/7] Fixed deadline time left (#407) * fixed deadline time left * > to >= --- frontend/src/components/Courses/CourseUtilComponents.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/Courses/CourseUtilComponents.tsx b/frontend/src/components/Courses/CourseUtilComponents.tsx index 2c33db07..70889c63 100644 --- a/frontend/src/components/Courses/CourseUtilComponents.tsx +++ b/frontend/src/components/Courses/CourseUtilComponents.tsx @@ -286,10 +286,10 @@ function EmptyOrNotProjects({ const deadlineDate = deadline.date; const diffTime = Math.abs(deadlineDate.getTime() - now.getTime()); const diffHours = Math.ceil(diffTime / (1000 * 60 * 60)); - const diffDays = Math.ceil(diffHours * 24); + const diffDays = Math.floor(diffHours / 24); timeLeft = - diffDays > 1 ? `${diffDays} days` : `${diffHours} hours`; + diffDays >= 1 ? `${diffDays} days` : `${diffHours} hours`; } } return (