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 (