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 01/21] 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 02/21] 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 03/21] 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 04/21] 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 05/21] 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 06/21] 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 07/21] 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 ( From e7129cd44bc091ef137bf02fb1d20ef1b14d58eb Mon Sep 17 00:00:00 2001 From: Aron Buzogany <108480125+AronBuzogany@users.noreply.github.com> Date: Thu, 23 May 2024 16:27:37 +0200 Subject: [PATCH 08/21] Fix #408 (#409) Co-authored-by: Warre Provoost <133233646+warreprovoost@users.noreply.github.com> --- frontend/src/utils/fetches/FetchProjects.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/src/utils/fetches/FetchProjects.tsx b/frontend/src/utils/fetches/FetchProjects.tsx index 64bbbad0..969bf154 100644 --- a/frontend/src/utils/fetches/FetchProjects.tsx +++ b/frontend/src/utils/fetches/FetchProjects.tsx @@ -5,15 +5,16 @@ import { ProjectDeadline, ShortSubmission, } from "../../pages/project/projectDeadline/ProjectDeadline.tsx"; +import { Me } from "../../types/me.ts"; const API_URL = import.meta.env.VITE_APP_API_HOST; export const fetchProjectPage = async () => { - const projects = await fetchProjects(); const me = await fetchMe(); + const projects = await fetchProjects(me); return { projects, me }; }; -export const fetchProjects = async () => { +export const fetchProjects = async (me: Me) => { try { const response = await authenticatedFetch(`${API_URL}/projects`); const jsonData = await response.json(); @@ -24,7 +25,7 @@ export const fetchProjects = async () => { const project_id = url_split[url_split.length - 1]; const response_submissions = await ( await authenticatedFetch( - encodeURI(`${API_URL}/submissions?project_id=${project_id}`) + encodeURI(`${API_URL}/submissions?project_id=${project_id}&uid=${me.uid}`) ) ).json(); From 50dfaaf9482971d508cc0b2b1cfc29e2890ceaba Mon Sep 17 00:00:00 2001 From: Cedric Mekeirle <143823820+JibrilExe@users.noreply.github.com> Date: Thu, 23 May 2024 16:36:23 +0200 Subject: [PATCH 09/21] Filter on conditions the students projects (#411) * projects visible for students * lint * filter on conditions * lint * fixed bad merge --- backend/project/endpoints/projects/projects.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/project/endpoints/projects/projects.py b/backend/project/endpoints/projects/projects.py index 94813bbd..03d46112 100644 --- a/backend/project/endpoints/projects/projects.py +++ b/backend/project/endpoints/projects/projects.py @@ -62,7 +62,10 @@ 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 = Project.query.filter(Project.course_id.in_(courses_student)) + projects_student = projects_student.filter(and_(*conditions)) \ + if conditions else projects_student + projects_student = projects_student.all() projects_student = [p for p in projects_student if p.visible_for_students] projects += projects_student From c979408855789202e5b868b5ce4fdc746be7473b Mon Sep 17 00:00:00 2001 From: Warre Provoost <133233646+warreprovoost@users.noreply.github.com> Date: Thu, 23 May 2024 16:46:18 +0200 Subject: [PATCH 10/21] show red dots on home page (#412) --- frontend/src/pages/home/HomePage.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/home/HomePage.tsx b/frontend/src/pages/home/HomePage.tsx index 35775325..94956541 100644 --- a/frontend/src/pages/home/HomePage.tsx +++ b/frontend/src/pages/home/HomePage.tsx @@ -11,14 +11,13 @@ import { import { DateCalendar } from "@mui/x-date-pickers/DateCalendar"; import { DayCalendarSkeleton, LocalizationProvider } from "@mui/x-date-pickers"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import dayjs, { Dayjs } from "dayjs"; import { PickersDay, PickersDayProps } from "@mui/x-date-pickers/PickersDay"; import { ProjectDeadlineCard } from "../project/projectDeadline/ProjectDeadlineCard.tsx"; import { ProjectDeadline } from "../project/projectDeadline/ProjectDeadline.tsx"; import { useLoaderData } from "react-router-dom"; import { Me } from "../../types/me.ts"; - interface DeadlineInfoProps { selectedDay: Dayjs; deadlines: ProjectDeadline[]; @@ -144,6 +143,11 @@ export default function HomePage() { .sort((a, b) => dayjs(b.deadline).diff(dayjs(a.deadline))) .slice(0, 3); // only show the first 3 const noDeadlineProject = projects.filter((p) => p.deadline === undefined); + + useEffect(() => { + handleMonthChange(selectedDay, projects, setHighlightedDays); + }, [projects, selectedDay]); + return ( From 15f0c705b3d5920c49b44a7e361d9180f11d58d1 Mon Sep 17 00:00:00 2001 From: Jarne Clauw <67628242+JarneClauw@users.noreply.github.com> Date: Thu, 23 May 2024 16:47:24 +0200 Subject: [PATCH 11/21] Project issues (#410) * Fixing the legacy warning * Adding auth decorators to the project submission download enpoints * Fixing query parameter tests * remove bad param filter --- .../project/endpoints/projects/project_last_submission.py | 2 ++ .../endpoints/projects/project_submissions_download.py | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/backend/project/endpoints/projects/project_last_submission.py b/backend/project/endpoints/projects/project_last_submission.py index 5b998c25..6cd4e719 100644 --- a/backend/project/endpoints/projects/project_last_submission.py +++ b/backend/project/endpoints/projects/project_last_submission.py @@ -6,6 +6,7 @@ from urllib.parse import urljoin from flask_restful import Resource from project.endpoints.projects.project_submissions_download import get_last_submissions_per_user +from project.utils.authentication import authorize_teacher_or_project_admin API_HOST = getenv("API_HOST") UPLOAD_FOLDER = getenv("UPLOAD_FOLDER") @@ -16,6 +17,7 @@ class SubmissionPerUser(Resource): Recourse to get all the submissions for users """ + @authorize_teacher_or_project_admin def get(self, project_id: int): """ Download all submissions for a project as a zip file. diff --git a/backend/project/endpoints/projects/project_submissions_download.py b/backend/project/endpoints/projects/project_submissions_download.py index 6ba93e93..31d8e80a 100644 --- a/backend/project/endpoints/projects/project_submissions_download.py +++ b/backend/project/endpoints/projects/project_submissions_download.py @@ -14,6 +14,7 @@ from project.models.project import Project from project.models.submission import Submission from project.db_in import db +from project.utils.authentication import authorize_teacher_or_project_admin API_HOST = getenv("API_HOST") UPLOAD_FOLDER = getenv("UPLOAD_FOLDER") @@ -24,7 +25,7 @@ def get_last_submissions_per_user(project_id): Get the last submissions per user for a given project """ try: - project = Project.query.get(project_id) + project = db.session.get(Project, project_id) except SQLAlchemyError: return {"message": "Internal server error"}, 500 @@ -57,6 +58,8 @@ class SubmissionDownload(Resource): """ Resource to download all submissions for a project. """ + + @authorize_teacher_or_project_admin def get(self, project_id: int): """ Download all submissions for a project as a zip file. From 3d4fe01a88ba91210df9c620d0408f89fe0b4721 Mon Sep 17 00:00:00 2001 From: Jarne Clauw <67628242+JarneClauw@users.noreply.github.com> Date: Thu, 23 May 2024 17:50:27 +0200 Subject: [PATCH 12/21] Project tests (#416) * Auth tests * Fixing the legacy warning * Adding auth decorators to the project submission download enpoints * Fixing auth tests * Broken data field and query parameter tests * Fixing query parameter tests * More tests but first fixing some issues * working tests * remove bad param filter * stuff --- backend/tests/endpoints/conftest.py | 73 ++++++++-- backend/tests/endpoints/project_test.py | 168 +++++++++++++++++++++++- 2 files changed, 228 insertions(+), 13 deletions(-) diff --git a/backend/tests/endpoints/conftest.py b/backend/tests/endpoints/conftest.py index 3f7717d7..64e44f0d 100644 --- a/backend/tests/endpoints/conftest.py +++ b/backend/tests/endpoints/conftest.py @@ -4,6 +4,9 @@ from datetime import datetime from zoneinfo import ZoneInfo from typing import Any +from zipfile import ZipFile +import os + import pytest from pytest import fixture, FixtureRequest from flask.testing import FlaskClient @@ -125,12 +128,13 @@ def course(session: Session, student: User, teacher: User, admin: User) -> Cours return course + ### PROJECTS ### @fixture def project(session: Session, course: Course): """Return a project entry""" project = Project( - title="Test project", + title="project", description="Test project", deadlines=[{"deadline":"2024-05-23T21:59:59", "description":"Final deadline"}], course_id=course.course_id, @@ -143,6 +147,45 @@ def project(session: Session, course: Course): session.commit() return project +@fixture +def project_invisible(session: Session, course: Course): + """Return a project entry that is not visible for the student""" + project = Project( + title="invisible project", + description="Test project", + deadlines=[{"deadline":"2024-05-23T21:59:59", "description":"Final deadline"}], + course_id=course.course_id, + visible_for_students=False, + archived=False, + runner=Runner.GENERAL, + regex_expressions=[".*.pdf"] + ) + session.add(project) + session.commit() + return project + +@fixture +def project_archived(session: Session, course: Course): + """Return a project entry that is not visible for the student""" + project = Project( + title="archived project", + description="Test project", + deadlines=[{"deadline":"2024-05-23T21:59:59", "description":"Final deadline"}], + course_id=course.course_id, + visible_for_students=True, + archived=True, + runner=Runner.GENERAL, + regex_expressions=[".*.pdf"] + ) + session.add(project) + session.commit() + return project + +@fixture +def projects(project: Project, project_invisible: Project, project_archived: Project): + """Return a list of project entries""" + return [project, project_invisible, project_archived] + ### SUBMISSIONS ### @@ -161,13 +204,6 @@ def submission(session: Session, student: User, project: Project): return submission ### FILES ### -@fixture -def file_empty(): - """Return an empty file""" - descriptor, name = tempfile.mkstemp() - with open(descriptor, "rb") as temp: - yield temp, name - @fixture def file_no_name(): """Return a file with no name""" @@ -176,15 +212,34 @@ def file_no_name(): temp.write("This is a test file.") with open(name, "rb") as temp: yield temp, "" + os.remove(name) @fixture def files(): """Return a temporary file""" - name = "/tmp/test.pdf" + name = "test.pdf" with open(name, "w", encoding="UTF-8") as file: file.write("This is a test file.") with open(name, "rb") as file: yield [(file, name)] + os.remove(name) + +@fixture +def file_assignment(): + """Return an assignment file for a project""" + assignment_file = "assignment.md" + assignment_content = "# Assignment" + with open(assignment_file, "w", encoding="UTF-8") as file: + file.write(assignment_content) + + zip_file = "project.zip" + with ZipFile(zip_file, "w") as zipf: + zipf.write(assignment_file) + + yield (zipf, zip_file) + + os.remove(assignment_file) + os.remove(zip_file) diff --git a/backend/tests/endpoints/project_test.py b/backend/tests/endpoints/project_test.py index 0884145a..2c4f56ab 100644 --- a/backend/tests/endpoints/project_test.py +++ b/backend/tests/endpoints/project_test.py @@ -1,9 +1,173 @@ """Tests for project endpoints.""" +from typing import Any import json +from pytest import mark +from flask.testing import FlaskClient + +from project.models.project import Project from tests.utils.auth_login import get_csrf_from_login +from tests.endpoints.endpoint import ( + TestEndpoint, + authentication_tests, + authorization_tests, + query_parameter_tests +) + +class TestProjectsEndpoint(TestEndpoint): + """Class to test the projects API endpoint""" + + ### AUTHENTICATION ### + # Where is login required + authentication_tests = \ + authentication_tests("/projects", ["get", "post"]) + \ + authentication_tests("/projects/@project_id", ["get", "patch", "delete"]) + \ + authentication_tests("/projects/@project_id/assignment", ["get"]) + \ + authentication_tests("/projects/@project_id/submissions-download", ["get"]) + \ + authentication_tests("/projects/@project_id/latest-per-user", ["get"]) + + @mark.parametrize("auth_test", authentication_tests, indirect=True) + def test_authentication(self, auth_test: tuple[str, Any, str, bool]): + """Test the authentication""" + super().authentication(auth_test) + + + + ### AUTHORIZATION ### + # Who can access what + authorization_tests = \ + authorization_tests("/projects", "get", + ["student", "student_other", "teacher", "teacher_other", "admin", "admin_other"], + []) + \ + authorization_tests("/projects", "post", + ["teacher"], + ["student", "student_other", "teacher_other", "admin", "admin_other"]) + \ + authorization_tests("/projects/@project_id", "get", + ["student", "teacher", "admin"], + ["student_other", "teacher_other", "admin_other"]) + \ + authorization_tests("/projects/@project_id", "patch", + ["teacher", "admin"], + ["student", "student_other", "teacher_other", "admin_other"]) + \ + authorization_tests("/projects/@project_id", "delete", + ["teacher"], + ["student", "student_other", "teacher_other", "admin", "admin_other"]) + \ + authorization_tests("/projects/@project_id/assignment", "get", + ["student", "teacher", "admin"], + ["student_other", "teacher_other", "admin_other"]) + \ + authorization_tests("/projects/@project_id/submissions-download", "get", + ["teacher", "admin"], + ["student", "student_other", "teacher_other", "admin_other"]) + \ + authorization_tests("/projects/@project_id/latest-per-user", "get", + ["teacher", "admin"], + ["student_other", "teacher_other", "admin_other"]) + + @mark.parametrize("auth_test", authorization_tests, indirect=True) + def test_authorization(self, auth_test: tuple[str, Any, str, bool]): + """Test the authorization""" + super().authorization(auth_test) + + + + ### QUERY PARAMETER ### + # Test a query parameter, should return [] for wrong values + query_parameter_tests = \ + query_parameter_tests("/projects", "get", "student", ["project_id", "title", "course_id"]) + + @mark.parametrize("query_parameter_test", query_parameter_tests, indirect=True) + def test_query_parameters(self, query_parameter_test: tuple[str, Any, str, bool]): + """Test a query parameter""" + super().query_parameter(query_parameter_test) + + + + ### PROJECTS ### + def test_get_projects(self, client: FlaskClient, projects: list[Project]): + """Test getting all projects""" + response = client.get( + "/projects", + headers = {"X-CSRF-TOKEN":get_csrf_from_login(client, "student")} + ) + assert response.status_code == 200 + data = response.json["data"] + assert [project["title"] in ["project", "archived project"] for project in data] + + def test_get_projects_project_id( + self, client: FlaskClient, api_host: str, project: Project, projects: list[Project] + ): + """Test getting all projects for a given project_id""" + response = client.get( + f"/projects?project_id={project.project_id}", + headers = {"X-CSRF-TOKEN":get_csrf_from_login(client, "teacher")} + ) + assert response.status_code == 200 + data = response.json["data"] + assert len(data) == 1 + assert data[0]["project_id"] == f"{api_host}/projects/{project.project_id}" + + def test_get_projects_title( + self, client: FlaskClient, project: Project, projects: list[Project] + ): + """Test getting all projects for a given title""" + response = client.get( + f"/projects?title={project.title}", + headers = {"X-CSRF-TOKEN":get_csrf_from_login(client, "teacher")} + ) + assert response.status_code == 200 + data = response.json["data"] + assert len(data) == 1 + assert data[0]["title"] == project.title + def test_get_projects_course_id( + self, client: FlaskClient, project: Project, projects: list[Project] + ): + """Test getting all projects for a given course_id""" + response = client.get( + f"/projects?course_id={project.course_id}", + headers = {"X-CSRF-TOKEN":get_csrf_from_login(client, "teacher")} + ) + assert response.status_code == 200 + assert len(response.json["data"]) == len(projects) + + + + ### PROJECT ### + def test_patch_project(self, client: FlaskClient, project: Project): + """Test patching a project""" + csrf = get_csrf_from_login(client, "teacher") + response = client.patch( + f"/projects/{project.project_id}", + headers = {"X-CSRF-TOKEN":csrf}, + data = { + "title": "A new title" + } + ) + assert response.status_code == 200 + response = client.get( + f"/projects/{project.project_id}", + headers = {"X-CSRF-TOKEN":csrf} + ) + assert response.status_code == 200 + data = response.json["data"] + assert data["title"] == "A new title" + + def test_delete_project(self, client: FlaskClient, project: Project): + """Test deleting a project""" + csrf = get_csrf_from_login(client, "teacher") + response = client.delete( + f"/projects/{project.project_id}", + headers = {"X-CSRF-TOKEN":csrf} + ) + assert response.status_code == 200 + response = client.get( + f"/projects/{project.project_id}", + headers = {"X-CSRF-TOKEN":csrf} + ) + assert response.status_code == 404 + + + +### OLD TESTS ### def test_assignment_download(client, valid_project): """ Method for assignment download @@ -26,7 +190,6 @@ def test_assignment_download(client, valid_project): # 404 because the file is not found, no assignment.md in zip file assert response.status_code == 404 - def test_not_found_download(client): """ Test a not present project download @@ -37,14 +200,12 @@ def test_not_found_download(client): response = client.get("/projects/-1/assignments", headers = {"X-CSRF-TOKEN":csrf}) assert response.status_code == 404 - def test_projects_home(client): """Test home project endpoint.""" csrf = get_csrf_from_login(client, "teacher1") response = client.get("/projects", headers = {"X-CSRF-TOKEN":csrf}) assert response.status_code == 200 - def test_getting_all_projects(client): """Test getting all projects""" csrf = get_csrf_from_login(client, "teacher1") @@ -52,7 +213,6 @@ def test_getting_all_projects(client): assert response.status_code == 200 assert isinstance(response.json['data'], list) - def test_post_project(client, valid_project): """Test posting a project to the database and testing if it's present""" valid_project["deadlines"] = json.dumps(valid_project["deadlines"]) From dd84501ba13ca3d2d3d39c3b97ef24c469c75499 Mon Sep 17 00:00:00 2001 From: Gerwoud Van den Eynden <62761483+Gerwoud@users.noreply.github.com> Date: Thu, 23 May 2024 17:57:08 +0200 Subject: [PATCH 13/21] added 403 for overview page (#414) * added 403 for overview page * linter * added good response.ok placement and status code for 404 * linter --- frontend/src/loaders/submission-overview-loader.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/frontend/src/loaders/submission-overview-loader.ts b/frontend/src/loaders/submission-overview-loader.ts index 2ee5ab7c..26008260 100644 --- a/frontend/src/loaders/submission-overview-loader.ts +++ b/frontend/src/loaders/submission-overview-loader.ts @@ -27,10 +27,20 @@ export default async function loadSubmissionOverview({ }: { params: Params; }) { + const projectId = params.projectId; const projectResponse = await authenticatedFetch( `${APIURL}/projects/${projectId}` ); + + if (!projectResponse.ok) { + if (projectResponse.status == 403) { + throw new Response("Not authenticated", {status: 403}); + } else if (projectResponse.status == 404) { + throw new Response("Not found", {status: 404}); + } + } + const projectData = (await projectResponse.json())["data"]; const overviewResponse = await authenticatedFetch( From ff7dd73de9d424245e1a133a6e8b02c163c542bd Mon Sep 17 00:00:00 2001 From: Warre Provoost <133233646+warreprovoost@users.noreply.github.com> Date: Thu, 23 May 2024 19:11:41 +0200 Subject: [PATCH 14/21] Student course overview (#417) * student overview * linter * linter * linter * check if teacher --- frontend/src/App.tsx | 4 +- .../Courses/CourseDetailStudent.tsx | 94 +++++++++++++++++++ .../Courses/CourseDetailTeacher.tsx | 5 +- .../src/components/Courses/CourseUtils.tsx | 3 +- .../src/components/Courses/CoursesDetail.tsx | 24 +++++ 5 files changed, 125 insertions(+), 5 deletions(-) create mode 100644 frontend/src/components/Courses/CourseDetailStudent.tsx create mode 100644 frontend/src/components/Courses/CoursesDetail.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c3a967a0..53c4d5ec 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -6,7 +6,6 @@ import { } from "react-router-dom"; import Layout from "./components/Header/Layout"; import { AllCoursesTeacher } from "./components/Courses/AllCoursesTeacher"; -import { CourseDetailTeacher } from "./components/Courses/CourseDetailTeacher"; import { dataLoaderCourseDetail, dataLoaderCourses, @@ -23,6 +22,7 @@ import { synchronizeJoinCode } from "./loaders/join-code.ts"; import { fetchMe } from "./utils/fetches/FetchMe.ts"; import {fetchProjectForm} from "./components/ProjectForm/project-form.ts"; import loadSubmissionOverview from "./loaders/submission-overview-loader.ts"; +import CoursesDetail from "./components/Courses/CoursesDetail.tsx"; const router = createBrowserRouter( createRoutesFromElements( @@ -38,7 +38,7 @@ const router = createBrowserRouter( } loader={dataLoaderCourses}/> - } loader={dataLoaderCourseDetail} /> + } loader={dataLoaderCourseDetail} /> + + + + +
+ {t("projects")}: + +
+
+
+ + + + + {t("admins")}: + + {adminMes.map((admin: Me) => ( + + + + {admin.display_name} + + + + + ))} + + + + + +
+ + ); +} diff --git a/frontend/src/components/Courses/CourseDetailTeacher.tsx b/frontend/src/components/Courses/CourseDetailTeacher.tsx index 80142e00..4b512ccc 100644 --- a/frontend/src/components/Courses/CourseDetailTeacher.tsx +++ b/frontend/src/components/Courses/CourseDetailTeacher.tsx @@ -113,7 +113,7 @@ function handleDeleteCourse( * * @returns A jsx component representing the course detail page for a teacher */ -export function CourseDetailTeacher(): JSX.Element { +export default function CourseDetailTeacher() { const [selectedStudents, setSelectedStudents] = useState([]); const [anchorEl, setAnchorElStudent] = useState(null); const openCodes = Boolean(anchorEl); @@ -129,6 +129,7 @@ export function CourseDetailTeacher(): JSX.Element { projects: ProjectDetail[]; adminMes: Me[]; studentMes: Me[]; + me:Me; }; const { course, projects, adminMes, studentMes } = courseDetail; const { t } = useTranslation("translation", { @@ -276,7 +277,7 @@ export function CourseDetailTeacher(): JSX.Element { * @param projects - The array of projects. * @returns Either a place holder for no projects or a grid of cards describing the projects. */ -function EmptyOrNotProjects({ +export function EmptyOrNotProjects({ projects, }: { projects: ProjectDetail[]; diff --git a/frontend/src/components/Courses/CourseUtils.tsx b/frontend/src/components/Courses/CourseUtils.tsx index 118f8b88..8525f930 100644 --- a/frontend/src/components/Courses/CourseUtils.tsx +++ b/frontend/src/components/Courses/CourseUtils.tsx @@ -247,5 +247,6 @@ export const dataLoaderCourseDetail = async ({ const student_uids = students.map((student: {uid: string}) => getIdFromLink(student.uid)); const adminMes = await fetchMes([course.teacher, ...admin_uids]); const studentMes = await fetchMes(student_uids); - return { course, projects, adminMes, studentMes }; + const me = await fetchMe(); + return { course, projects, adminMes, studentMes, me}; }; diff --git a/frontend/src/components/Courses/CoursesDetail.tsx b/frontend/src/components/Courses/CoursesDetail.tsx new file mode 100644 index 00000000..a697f58b --- /dev/null +++ b/frontend/src/components/Courses/CoursesDetail.tsx @@ -0,0 +1,24 @@ +import {useLoaderData} from "react-router-dom"; +import {Me} from "../../types/me.ts"; +import {Course, ProjectDetail} from "./CourseUtils.tsx"; +import CourseDetailTeacher from "./CourseDetailTeacher.tsx"; +import CourseDetailStudent from "./CourseDetailStudent.tsx"; + +/** + * gives the right detail page + * @returns - detail page + */ +export default function CoursesDetail() :JSX.Element { + const loader = useLoaderData() as { + course: Course; + projects: ProjectDetail[]; + adminMes: Me[]; + studentMes: Me[]; + me:Me; + }; + if (loader.course.teacher === loader.me.uid) { + return ; + } else { + return ; + } +} \ No newline at end of file From aa7383f975db1f90a3d3582566e0a3654db1a105 Mon Sep 17 00:00:00 2001 From: Aron Buzogany <108480125+AronBuzogany@users.noreply.github.com> Date: Thu, 23 May 2024 20:07:08 +0200 Subject: [PATCH 15/21] fixed authorizations in projectview (#418) * fixed authorizations in projectview * linting * added locales --- frontend/public/locales/en/translation.json | 3 + frontend/public/locales/nl/translation.json | 3 + frontend/src/App.tsx | 31 +- frontend/src/loaders/project-view-loader.ts | 84 +++++ .../pages/project/projectView/ProjectView.tsx | 311 +++++++++--------- 5 files changed, 263 insertions(+), 169 deletions(-) create mode 100644 frontend/src/loaders/project-view-loader.ts diff --git a/frontend/public/locales/en/translation.json b/frontend/public/locales/en/translation.json index e6b5c6e5..317f62c8 100644 --- a/frontend/public/locales/en/translation.json +++ b/frontend/public/locales/en/translation.json @@ -65,6 +65,9 @@ "submit": "Submit", "previousSubmissions": "Previous Submissions", "noFileSelected": "No file selected", + "delete": "Delete", + "deleteProjectWarning": "Are you sure you want to delete this project?", + "imSure": "Yes, I'm sure", "submissionGrid": { "late": "Late", "fail": "Fail", diff --git a/frontend/public/locales/nl/translation.json b/frontend/public/locales/nl/translation.json index 7715aecd..c74cb996 100644 --- a/frontend/public/locales/nl/translation.json +++ b/frontend/public/locales/nl/translation.json @@ -102,6 +102,9 @@ "submit": "Indienen", "previousSubmissions": "Vorige indieningen", "noFileSelected": "Er is geen bestand geselecteerd", + "delete": "Verwijder", + "deleteProjectWarning": "Bent u zeker dat u dit project wilt verwijderen?", + "imSure": "Ja, ik ben zeker", "submissionGrid": { "late": "Te laat", "fail": "Gefaald", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 53c4d5ec..6d2cad70 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -20,9 +20,10 @@ import HomePages from "./pages/home/HomePages.tsx"; import ProjectOverView from "./pages/project/projectOverview.tsx"; import { synchronizeJoinCode } from "./loaders/join-code.ts"; import { fetchMe } from "./utils/fetches/FetchMe.ts"; -import {fetchProjectForm} from "./components/ProjectForm/project-form.ts"; +import { fetchProjectForm } from "./components/ProjectForm/project-form.ts"; import loadSubmissionOverview from "./loaders/submission-overview-loader.ts"; import CoursesDetail from "./components/Courses/CoursesDetail.tsx"; +import loadProjectViewData from "./loaders/project-view-loader.ts"; const router = createBrowserRouter( createRoutesFromElements( @@ -36,9 +37,17 @@ const router = createBrowserRouter( }> } loader={fetchProjectPage} /> - } loader={dataLoaderCourses}/> + } + loader={dataLoaderCourses} + /> - } loader={dataLoaderCourseDetail} /> + } + loader={dataLoaderCourseDetail} + /> } /> - }> - } loader={fetchProjectForm}/> + } + loader={loadProjectViewData} + > + } + loader={fetchProjectForm} + /> - , - ), + + ) ); /** diff --git a/frontend/src/loaders/project-view-loader.ts b/frontend/src/loaders/project-view-loader.ts new file mode 100644 index 00000000..0a9cf6f8 --- /dev/null +++ b/frontend/src/loaders/project-view-loader.ts @@ -0,0 +1,84 @@ +import { Params } from "react-router-dom"; +import { Deadline } from "../types/deadline"; +import { authenticatedFetch } from "../utils/authenticated-fetch"; +import { fetchMe } from "../utils/fetches/FetchMe"; +import i18next from "i18next"; + +const API_URL = import.meta.env.VITE_APP_API_HOST; + +/** + * + * @param param0 - params: Params + * @returns - projectData: projectData, + * courseData: courseData, + * me: me, + * assignmentText: assignmentText, + * isAdmin: isAdmin + */ +export default async function loadProjectViewData({ + params, +}: { + params: Params; +}) { + const me = await fetchMe(); + + const projectId = params.projectId; + + const assignmentResponse = await authenticatedFetch( + `${API_URL}/projects/${projectId}/assignment?lang=${i18next.resolvedLanguage}` + ); + + let assignmentText; + + if (assignmentResponse.ok) { + assignmentText = await assignmentResponse.text(); + } else { + throw new Response(assignmentResponse.statusText, { + status: assignmentResponse.status, + }); + } + + const response = await authenticatedFetch(`${API_URL}/projects/${projectId}`); + if (response.ok) { + const data = await response.json(); + const projectData = data["data"]; + + const transformedDeadlines = projectData.deadlines.map( + (deadlineArray: string[]): Deadline => ({ + description: deadlineArray[0], + deadline: deadlineArray[1], + }) + ); + + projectData["deadlines"] = transformedDeadlines; + + const courseResponse = await authenticatedFetch( + `${API_URL}/courses/${projectData.course_id}` + ); + + let courseData; + if (courseResponse.ok) { + courseData = (await courseResponse.json())["data"]; + } else { + throw new Response(response.statusText, { status: response.status }); + } + + courseData["admins"] = courseData["admins"].map((admin: string) => { + const urlSplit = admin.split("/"); + return urlSplit[urlSplit.length - 1]; + }); + + const isAdmin = + me.uid === courseData["teacher"] || courseData["admins"].includes(me.uid); + + return { + projectData: projectData, + courseData: courseData, + me: me, + assignmentText: assignmentText, + isAdmin: isAdmin, + }; + } else { + throw new Response(response.statusText, { status: response.status }); + } +} diff --git a/frontend/src/pages/project/projectView/ProjectView.tsx b/frontend/src/pages/project/projectView/ProjectView.tsx index f5f5df09..8d5a2484 100644 --- a/frontend/src/pages/project/projectView/ProjectView.tsx +++ b/frontend/src/pages/project/projectView/ProjectView.tsx @@ -6,27 +6,32 @@ import { CardHeader, Container, Fade, - Grid, IconButton, + Grid, + IconButton, Stack, TextField, Typography, } from "@mui/material"; -import {useCallback, useEffect, useState} from "react"; +import { useState } from "react"; import Markdown from "react-markdown"; -import {useLocation, useNavigate, useParams} from "react-router-dom"; +import { + useLoaderData, + useLocation, + useNavigate, + useParams, +} from "react-router-dom"; import SubmissionCard from "./SubmissionCard"; import { Course } from "../../../types/course"; import { Title } from "../../../components/Header/Title"; import { authenticatedFetch } from "../../../utils/authenticated-fetch"; import i18next from "i18next"; -import {useTranslation} from "react-i18next"; -import {Me} from "../../../types/me.ts"; -import {fetchMe} from "../../../utils/fetches/FetchMe.ts"; +import { useTranslation } from "react-i18next"; +import { Me } from "../../../types/me.ts"; import DeadlineGrid from "../../../components/DeadlineView/DeadlineGrid.tsx"; -import {Deadline} from "../../../types/deadline.ts"; -import EditIcon from '@mui/icons-material/Edit'; -import CheckIcon from '@mui/icons-material/Check'; -import CloseIcon from '@mui/icons-material/Close'; +import { Deadline } from "../../../types/deadline.ts"; +import EditIcon from "@mui/icons-material/Edit"; +import CheckIcon from "@mui/icons-material/Check"; +import CloseIcon from "@mui/icons-material/Close"; const API_URL = import.meta.env.VITE_APP_API_HOST; @@ -35,6 +40,7 @@ interface Project { description: string; regex_expressions: string[]; archived: string; + deadlines: Deadline[]; } /** @@ -43,129 +49,86 @@ interface Project { * and submissions of the current user for that project */ export default function ProjectView() { - const location = useLocation(); - const [me, setMe] = useState(null); - const { t } = useTranslation('translation', { keyPrefix: 'projectView' }); + const { projectData, courseData, assignmentText, isAdmin } = + useLoaderData() as { + me: Me; + projectData: Project; + courseData: Course; + assignmentText: string; + isAdmin: boolean; + }; + + const deadlines = projectData["deadlines"]; + + const { t } = useTranslation("translation", { keyPrefix: "projectView" }); const { projectId } = useParams<{ projectId: string }>(); - const [projectData, setProjectData] = useState(null); - const [courseData, setCourseData] = useState(null); - const [assignmentRawText, setAssignmentRawText] = useState(""); - const [deadlines, setDeadlines] = useState([]); - const [alertVisibility, setAlertVisibility] = useState(false) + const [alertVisibility, setAlertVisibility] = useState(false); const [edit, setEdit] = useState(false); - const [title, setTitle] = useState(""); - const [description, setDescription] = useState(""); + const [title, setTitle] = useState(projectData["title"]); + const [description, setDescription] = useState(projectData["description"]); - const navigate = useNavigate() + const navigate = useNavigate(); const deleteProject = () => { authenticatedFetch(`${API_URL}/projects/${projectId}`, { - method: "DELETE" + method: "DELETE", }); - navigate('/projects'); - } + navigate("/projects"); + }; const patchTitleAndDescription = async () => { setEdit(false); const formData = new FormData(); - formData.append('title', title); - formData.append('description', description); + formData.append("title", title); + formData.append("description", description); - const response = await authenticatedFetch(`${API_URL}/projects/${projectId}`, { - method: "PATCH", - body: formData - }); + const response = await authenticatedFetch( + `${API_URL}/projects/${projectId}`, + { + method: "PATCH", + body: formData, + } + ); // Check if the response is ok (status code 2xx) if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } - - updateProject(); - } + }; const discardEditTitle = () => { const title = projectData?.title; setEdit(false); - if (title) - setTitle(title); - - if (projectData?.description) - setDescription(projectData?.description); - } - - const updateProject = useCallback(async () => { - authenticatedFetch(`${API_URL}/projects/${projectId}`).then((response) => { - if (response.ok) { - response.json().then((data) => { - const projectData = data["data"]; - setProjectData(projectData); - setTitle(projectData.title); - setDescription(projectData.description); - - const transformedDeadlines = projectData.deadlines.map((deadlineArray: string[]): Deadline => ({ - description: deadlineArray[0], - deadline: deadlineArray[1] - })); - - setDeadlines(transformedDeadlines); + if (title) setTitle(title); - authenticatedFetch( - `${API_URL}/courses/${projectData.course_id}` - ).then((response) => { - if (response.ok) { - response.json().then((data) => { - setCourseData(data["data"]); - }); - } - }); - }); - } - }); - }, [projectId]); + if (projectData?.description) setDescription(projectData?.description); + }; const archiveProject = async () => { const newArchived = !projectData?.archived; const formData = new FormData(); - formData.append('archived', newArchived.toString()); - - await authenticatedFetch(`${API_URL}/projects/${projectId}`, { - method: "PATCH", - body: formData - }) + formData.append("archived", newArchived.toString()); - await updateProject(); - } - - useEffect(() => { - updateProject(); - - authenticatedFetch( - `${API_URL}/projects/${projectId}/assignment?lang=${i18next.resolvedLanguage}` - ).then((response) => { - if (response.ok) { - response.text().then((data) => setAssignmentRawText(data)); + const response = await authenticatedFetch( + `${API_URL}/projects/${projectId}`, + { + method: "PATCH", + body: formData, } - }); + ); - fetchMe().then((data) => { - setMe(data); - }); - - }, [projectId, updateProject]); + if (response.ok) { + navigate(0); + } + }; if (!projectId) return null; return ( - + {projectData && ( @@ -178,96 +141,114 @@ export default function ProjectView() { justifyContent="space-between" alignItems="center" > - { - !edit && <>{projectData.title} - } - { - edit && <> setTitle(event.target.value)}/> - } + {!edit && <>{projectData.title}} + {edit && ( + <> + setTitle(event.target.value)} + /> + + )} {courseData && ( - )}
} subheader={ - + - { - !edit - ? <>{projectData.description} - : edit && <> setDescription(event.target.value)}/> - } + {!edit ? ( + <> + {projectData.description} + + ) : ( + edit && ( + <> + + setDescription(event.target.value) + } + /> + + ) + )} } /> - {assignmentRawText} - - { me && me.role === "TEACHER" && ( - edit - ? ( - <> - - - - - - - ) - : ( - setEdit(true)}> - + {assignmentText} + + {isAdmin && + (edit ? ( + <> + + - ) - )} + + + + + ) : ( + setEdit(true)}> + + + ))} )} - {me && me.role === "TEACHER" && ( - - + - + - @@ -275,16 +256,21 @@ export default function ProjectView() { display="flex" flexDirection="row-reverse" pt={2} - width="100%"> - - -
+ +
{ @@ -293,24 +279,26 @@ export default function ProjectView() { }, 4000); }} > - - Are you sure you want to delete this project - + + {t("deleteProjectWarning")} + + - )} @@ -322,5 +310,4 @@ export default function ProjectView() { ); - } From 8b7aae1f0df59977a7236a69fbe08f6e04ce124e Mon Sep 17 00:00:00 2001 From: Aron Buzogany <108480125+AronBuzogany@users.noreply.github.com> Date: Thu, 23 May 2024 20:46:05 +0200 Subject: [PATCH 16/21] redirecting to homepage when user not logged in (#421) * redirecting to homepage when user not logged in * linting --- frontend/src/App.tsx | 1 + frontend/src/components/Header/Layout.tsx | 41 ++++++++++++++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6d2cad70..a3c39927 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -35,6 +35,7 @@ const router = createBrowserRouter( > } loader={fetchProjectPage} /> }> + } loader={fetchProjectPage} /> } loader={fetchProjectPage} /> { + if ( + !meData.loggedIn && + !( + location.pathname === "/" || + /\/([a-z]{2})?\/home/.test(location.pathname) + ) + ) { + navigate("/"); + } + }, [meData.loggedIn, location.pathname, navigate]); return ( <> @@ -16,3 +38,20 @@ export default function Layout(): JSX.Element { ); } + +const useEnsureLangCodeInPath = () => { + const location = useLocation(); + const navigate = useNavigate(); + + useEffect(() => { + const pathParts = location.pathname.split("/").filter(Boolean); + const langCode = i18next.resolvedLanguage; + + // Check if the URL starts with the lang code + if (pathParts[0] !== langCode) { + // Prepend the lang code to the path + const newPath = `/${langCode}/${pathParts.join("/")}`; + navigate(newPath); + } + }, [location, navigate]); +}; \ No newline at end of file From d3bcf1fffa706042a4aae683f24f4b1eda4f64c4 Mon Sep 17 00:00:00 2001 From: Aron Buzogany <108480125+AronBuzogany@users.noreply.github.com> Date: Thu, 23 May 2024 20:46:24 +0200 Subject: [PATCH 17/21] only fetching admins when needed (#420) --- .../src/components/Courses/CourseUtils.tsx | 104 ++++++++++-------- 1 file changed, 61 insertions(+), 43 deletions(-) diff --git a/frontend/src/components/Courses/CourseUtils.tsx b/frontend/src/components/Courses/CourseUtils.tsx index 8525f930..d06202ff 100644 --- a/frontend/src/components/Courses/CourseUtils.tsx +++ b/frontend/src/components/Courses/CourseUtils.tsx @@ -41,13 +41,15 @@ export function loggedInToken() { * @returns The username. */ export async function getUser(uid: string): Promise { - return authenticatedFetch(`${apiHost}/users/${getIdFromLink(uid)}`).then((response) => { - if (response.ok) { - return response.json().then((data) => { - return data.data; - }); + return authenticatedFetch(`${apiHost}/users/${getIdFromLink(uid)}`).then( + (response) => { + if (response.ok) { + return response.json().then((data) => { + return data.data; + }); + } } - }) + ); } /** @@ -124,14 +126,14 @@ const fetchData = async (url: string, params?: URLSearchParams) => { export const dataLoaderCourses = async () => { //const params = new URLSearchParams({ 'teacher': loggedInUid() }); - const courses = await fetchData(`courses`); + const courses = await fetchData(`courses`); const projects = await fetchProjectsCourse(courses); const me = await fetchMe(); - for( const c of courses){ - const teacher = await fetchData(`users/${c.teacher}`) - c.teacher = teacher.display_name + for (const c of courses) { + const teacher = await fetchData(`users/${c.teacher}`); + c.teacher = teacher.display_name; } - return {courses, projects, me} + return { courses, projects, me }; }; /** @@ -139,7 +141,7 @@ export const dataLoaderCourses = async () => { * @param courses - All the courses * @returns the projects */ -export async function fetchProjectsCourse (courses:Course[]) { +export async function fetchProjectsCourse(courses: Course[]) { const projectPromises = courses.map((course) => authenticatedFetch( `${apiHost}/projects?course_id=${getIdFromLink(course.course_id)}` @@ -149,30 +151,32 @@ export async function fetchProjectsCourse (courses:Course[]) { const projectResults = await Promise.all(projectPromises); const projectsMap: { [courseId: string]: ProjectDetail[] } = {}; for await (const [index, result] of projectResults.entries()) { - projectsMap[getIdFromLink(courses[index].course_id)] = await Promise.all(result.data.map(async (item: Project) => { - const projectRes = await authenticatedFetch(item.project_id); - if (projectRes.status !== 200) { - throw new Response("Failed to fetch project data", { - status: projectRes.status, - }); - } - const projectJson = await projectRes.json(); - const projectData = projectJson.data; - let projectDeadlines = []; - if (projectData.deadlines) { - projectDeadlines = projectData.deadlines.map( - ([description, dateString]: [string, string]) => ({ - description, - date: new Date(dateString), - }) - ); - } - const project: ProjectDetail = { - ...item, - deadlines: projectDeadlines, - }; - return project; - })); + projectsMap[getIdFromLink(courses[index].course_id)] = await Promise.all( + result.data.map(async (item: Project) => { + const projectRes = await authenticatedFetch(item.project_id); + if (projectRes.status !== 200) { + throw new Response("Failed to fetch project data", { + status: projectRes.status, + }); + } + const projectJson = await projectRes.json(); + const projectData = projectJson.data; + let projectDeadlines = []; + if (projectData.deadlines) { + projectDeadlines = projectData.deadlines.map( + ([description, dateString]: [string, string]) => ({ + description, + date: new Date(dateString), + }) + ); + } + const project: ProjectDetail = { + ...item, + deadlines: projectDeadlines, + }; + return project; + }) + ); } return { ...projectsMap }; } @@ -228,7 +232,7 @@ const dataLoaderStudents = async (courseId: string) => { const fetchMes = async (uids: string[]) => { return Promise.all(uids.map((uid) => getUser(uid))); -} +}; export const dataLoaderCourseDetail = async ({ params, @@ -239,14 +243,28 @@ export const dataLoaderCourseDetail = async ({ if (!courseId) { throw new Error("Course ID is undefined."); } + const me = await fetchMe(); + const course = await dataLoaderCourse(courseId); + + const courseAdminuids = course["admins"].map((admin: string) => { + const urlSplit = admin.split("/"); + return urlSplit[urlSplit.length - 1]; + }); + const projects = await dataLoaderProjects(courseId); - const admins = await dataLoaderAdmins(courseId); + let adminMes: Me[] = []; + if (me.uid === course.teacher || courseAdminuids.includes(me.uid)) { + const admins = await dataLoaderAdmins(courseId); + const adminUids = admins.map((admin: { uid: string }) => + getIdFromLink(admin.uid) + ); + adminMes = await fetchMes([course.teacher, ...adminUids]); + } const students = await dataLoaderStudents(courseId); - const admin_uids = admins.map((admin: {uid: string}) => getIdFromLink(admin.uid)); - const student_uids = students.map((student: {uid: string}) => getIdFromLink(student.uid)); - const adminMes = await fetchMes([course.teacher, ...admin_uids]); + const student_uids = students.map((student: { uid: string }) => + getIdFromLink(student.uid) + ); const studentMes = await fetchMes(student_uids); - const me = await fetchMe(); - return { course, projects, adminMes, studentMes, me}; + return { course, projects, adminMes, studentMes, me }; }; From dcb556fa208997b577ea18c766a35a6ed999051e Mon Sep 17 00:00:00 2001 From: Aron Buzogany <108480125+AronBuzogany@users.noreply.github.com> Date: Thu, 23 May 2024 21:01:03 +0200 Subject: [PATCH 18/21] Fixed url flickering (#423) * redirecting to homepage when user not logged in * linting * fixed url flickering --- frontend/src/components/Header/Layout.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/Header/Layout.tsx b/frontend/src/components/Header/Layout.tsx index 959d6166..2b3097c7 100644 --- a/frontend/src/components/Header/Layout.tsx +++ b/frontend/src/components/Header/Layout.tsx @@ -24,10 +24,11 @@ export default function Layout(): JSX.Element { !meData.loggedIn && !( location.pathname === "/" || - /\/([a-z]{2})?\/home/.test(location.pathname) + /\/([a-z]{2})\/home/.test(location.pathname) || + location.pathname === `/${i18next.resolvedLanguage}` ) ) { - navigate("/"); + navigate(`${i18next.resolvedLanguage}/home`); } }, [meData.loggedIn, location.pathname, navigate]); @@ -54,4 +55,4 @@ const useEnsureLangCodeInPath = () => { navigate(newPath); } }, [location, navigate]); -}; \ No newline at end of file +}; From 26692db4e542d23621f18f9ac8adf868028ba70e Mon Sep 17 00:00:00 2001 From: Warre Provoost <133233646+warreprovoost@users.noreply.github.com> Date: Thu, 23 May 2024 21:02:30 +0200 Subject: [PATCH 19/21] fix seeder path (#422) --- backend/seeder/seeder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/seeder/seeder.py b/backend/seeder/seeder.py index d56d0e0f..2e90341f 100644 --- a/backend/seeder/seeder.py +++ b/backend/seeder/seeder.py @@ -222,7 +222,7 @@ def populate_course_projects(session, course_id, students): # Write assignment.md file assignment_content = fake.text() assignment_file_path = os.path.join( - UPLOAD_URL, "projects", str(project_id), "assignment.md") + UPLOAD_URL, str(project_id), "assignment.md") os.makedirs(os.path.dirname(assignment_file_path), exist_ok=True) with open(assignment_file_path, "w", encoding="utf-8") as assignment_file: assignment_file.write(assignment_content) @@ -236,7 +236,7 @@ def populate_project_submissions(session, students, project_id): session.add_all(submissions) session.commit() for submission in submissions: - submission_directory = os.path.join(UPLOAD_URL, "projects", str( + submission_directory = os.path.join(UPLOAD_URL, str( project_id), "submissions", str(submission.submission_id), "submission") os.makedirs(submission_directory, exist_ok=True) submission_file_path = os.path.join( From 41567cecc4480802bddc04d031d3bc3a29aef719 Mon Sep 17 00:00:00 2001 From: Gerwoud Van den Eynden <62761483+Gerwoud@users.noreply.github.com> Date: Thu, 23 May 2024 21:06:10 +0200 Subject: [PATCH 20/21] added explanation (#425) --- documentation/docs/evaluators/custom_evaluator.md | 4 +++- .../current/evaluators/custom_evaluator.md | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/documentation/docs/evaluators/custom_evaluator.md b/documentation/docs/evaluators/custom_evaluator.md index d7dd2552..017ee51e 100644 --- a/documentation/docs/evaluators/custom_evaluator.md +++ b/documentation/docs/evaluators/custom_evaluator.md @@ -1 +1,3 @@ -# Custom evaluator \ No newline at end of file +# Custom evaluator +When selecting the custom runner the user has to provide his own Dockerfile. +This gives freedom to do everything you want with it, and it is completely customizable to your own needs. \ No newline at end of file diff --git a/documentation/i18n/nl/docusaurus-plugin-content-docs/current/evaluators/custom_evaluator.md b/documentation/i18n/nl/docusaurus-plugin-content-docs/current/evaluators/custom_evaluator.md index d7dd2552..b24c64c4 100644 --- a/documentation/i18n/nl/docusaurus-plugin-content-docs/current/evaluators/custom_evaluator.md +++ b/documentation/i18n/nl/docusaurus-plugin-content-docs/current/evaluators/custom_evaluator.md @@ -1 +1,2 @@ -# Custom evaluator \ No newline at end of file +# Custom evaluator +Bij het selecteren van de aangepaste runner moet de gebruiker zijn eigen Dockerfile aanleveren. Dit geeft de vrijheid om alles te doen wat je wilt, en het is volledig aan te passen aan je eigen behoeften. \ No newline at end of file From 5032fd1a47fcb8a58c866b31e5c2f6e9cbd684bd Mon Sep 17 00:00:00 2001 From: Gerwoud Van den Eynden <62761483+Gerwoud@users.noreply.github.com> Date: Thu, 23 May 2024 21:18:56 +0200 Subject: [PATCH 21/21] added general evaluator documentation (#424) * added general evaluator documentation * fix 1 * fix 2 * fix yallah * final fix i believe --- documentation/docs/evaluators/general_evaluator.md | 8 +++++++- .../current/evaluators/general_evaluator.md | 9 ++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/documentation/docs/evaluators/general_evaluator.md b/documentation/docs/evaluators/general_evaluator.md index eb8ccedd..0b644869 100644 --- a/documentation/docs/evaluators/general_evaluator.md +++ b/documentation/docs/evaluators/general_evaluator.md @@ -1 +1,7 @@ -# General evaluator \ No newline at end of file +# General evaluator +The general runner is capable of running the following languages with only the standard libraries included +- Python +- Ruby +- Java + +All the user has to is upload a run_tests.sh in the project files and the script will run when a submission is uploaded by a student. \ No newline at end of file diff --git a/documentation/i18n/nl/docusaurus-plugin-content-docs/current/evaluators/general_evaluator.md b/documentation/i18n/nl/docusaurus-plugin-content-docs/current/evaluators/general_evaluator.md index eb8ccedd..43c722fd 100644 --- a/documentation/i18n/nl/docusaurus-plugin-content-docs/current/evaluators/general_evaluator.md +++ b/documentation/i18n/nl/docusaurus-plugin-content-docs/current/evaluators/general_evaluator.md @@ -1 +1,8 @@ -# General evaluator \ No newline at end of file +# General evaluator +De algemene runner is in staat om de volgende talen uit te voeren met enkel de standaardbibliotheken inbegrepen +- Javascript +- Python +- Ruby +- Java + +Het enige wat de gebruiker hoeft te doen is een run_tests.sh-bestand in de projectbestanden te uploaden en het script wordt uitgevoerd wanneer een inzending door een student wordt geĆ¼pload. \ No newline at end of file