Skip to content

Commit

Permalink
Merge branch 'development' into frontend/header-tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Vucis authored May 23, 2024
2 parents d5e99a5 + 5032fd1 commit e9e3fb7
Show file tree
Hide file tree
Showing 42 changed files with 1,181 additions and 296 deletions.
20 changes: 18 additions & 2 deletions backend/db_construct.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
128 changes: 128 additions & 0 deletions backend/project/endpoints/projects/groups/group_student.py
Original file line number Diff line number Diff line change
@@ -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
127 changes: 127 additions & 0 deletions backend/project/endpoints/projects/groups/groups.py
Original file line number Diff line number Diff line change
@@ -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
7 changes: 6 additions & 1 deletion backend/project/endpoints/projects/project_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -38,3 +38,8 @@
'/projects/<int:project_id>/latest-per-user',
view_func=SubmissionPerUser.as_view('latest_per_user')
)

project_bp.add_url_rule(
'/projects/<int:project_id>/groups',
view_func=Groups.as_view('groups')
)
2 changes: 2 additions & 0 deletions backend/project/endpoints/projects/project_last_submission.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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

Expand Down Expand Up @@ -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.
Expand Down
12 changes: 9 additions & 3 deletions backend/project/endpoints/projects/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand All @@ -62,6 +62,12 @@ 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))
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

# Return the projects
data["message"] = "Successfully fetched the projects"
Expand Down
Loading

0 comments on commit e9e3fb7

Please sign in to comment.