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

Commit

Permalink
Merge branch 'development' of https://github.com/SELab-2/UGent-3 into…
Browse files Browse the repository at this point in the history
… frontend-testing-setup
  • Loading branch information
Vucis committed May 20, 2024
2 parents 0fc8403 + 587423b commit ddae7e9
Show file tree
Hide file tree
Showing 93 changed files with 17,354 additions and 646 deletions.
4 changes: 2 additions & 2 deletions backend/db_construct.sql
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ CREATE TYPE deadline AS(

CREATE TABLE projects (
project_id INT GENERATED ALWAYS AS IDENTITY,
title VARCHAR(50) NOT NULL,
title VARCHAR(100) NOT NULL,
description TEXT NOT NULL,
deadlines deadline[],
course_id INT NOT NULL,
Expand All @@ -65,7 +65,7 @@ CREATE TABLE submissions (
project_id INT NOT NULL,
grading FLOAT CHECK (grading >= 0 AND grading <= 20),
submission_time TIMESTAMP WITH TIME ZONE NOT NULL,
submission_path VARCHAR(50) NOT NULL,
submission_path VARCHAR(255) NOT NULL,
submission_status submission_status NOT NULL,
PRIMARY KEY(submission_id),
CONSTRAINT fk_project FOREIGN KEY(project_id) REFERENCES projects(project_id) ON DELETE CASCADE,
Expand Down
1 change: 1 addition & 0 deletions backend/dev-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ pytest
pylint
pylint-flask
pyyaml
faker
70 changes: 60 additions & 10 deletions backend/project/endpoints/courses/courses.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,21 @@

from os import getenv
from urllib.parse import urljoin
from dataclasses import fields
from dotenv import load_dotenv

from flask import request
from flask_restful import Resource

from sqlalchemy import union, select
from sqlalchemy.exc import SQLAlchemyError

from project.models.course import Course
from project.utils.query_agent import query_selected_from_model, insert_into_model
from project.utils.authentication import login_required, authorize_teacher
from project.models.course_relation import CourseAdmin, CourseStudent
from project.utils.query_agent import insert_into_model
from project.utils.authentication import login_required_return_uid, authorize_teacher
from project.endpoints.courses.courses_utils import check_data
from project.db_in import db

load_dotenv()
API_URL = getenv("API_HOST")
Expand All @@ -24,20 +30,64 @@
class CourseForUser(Resource):
"""Api endpoint for the /courses link"""

@login_required
def get(self):
@login_required_return_uid
def get(self, uid=None):
""" "
Get function for /courses this will be the main endpoint
to get all courses and filter by given query parameter like /courses?parameter=...
parameters can be either one of the following: teacher,ufora_id,name.
"""

return query_selected_from_model(
Course,
RESPONSE_URL,
url_mapper={"course_id": RESPONSE_URL},
filters=request.args
)
try:
filter_params = {
key: value for key, value
in request.args.to_dict().items()
if key in {f.name for f in fields(Course)}
}

# Start with a base query
base_query = select(Course)

# Apply filters dynamically if they are provided
for param, value in filter_params.items():
if value:
if param in Course.__table__.columns:
attribute = getattr(Course, param)
base_query = base_query.filter(attribute == value)

# Define the role-specific queries
student_courses = base_query.join(
CourseStudent,
Course.course_id == CourseStudent.course_id).filter(
CourseStudent.uid == uid)
admin_courses = base_query.join(
CourseAdmin,
Course.course_id == CourseAdmin.course_id).filter(
CourseAdmin.uid == uid)
teacher_courses = base_query.filter(Course.teacher == uid)

# Combine the select statements using union to remove duplicates
all_courses_query = union(student_courses, admin_courses, teacher_courses)

# Execute the union query and fetch all results as Course instances
courses = db.session.execute(all_courses_query).mappings().all()
courses_data = [dict(course) for course in courses]

for course in courses_data:
course["course_id"] = urljoin(f"{RESPONSE_URL}/", str(course['course_id']))

return {
"data": courses_data,
"url": RESPONSE_URL,
"message": "Courses fetched successfully"
}

except SQLAlchemyError:
db.session.rollback()
return {
"message": "An error occurred while fetching the courses",
"url": RESPONSE_URL
}, 500

@authorize_teacher
def post(self, teacher_id=None):
Expand Down
5 changes: 4 additions & 1 deletion backend/project/endpoints/courses/join.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,10 @@ def post(self, uid=None): # pylint: disable=too-many-return-statements
relation = course_relation.query.filter_by(course_id=course_id, uid=uid).first()
if relation:
response["message"] = "User already in course"
return response, 400
response["data"] = {
"course_id": course_id
}
return response, 409
except SQLAlchemyError:
response["message"] = "Internal server error"
return response, 500
Expand Down
20 changes: 17 additions & 3 deletions backend/project/endpoints/projects/project_assignment_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@
import os
from urllib.parse import urljoin

from flask import send_from_directory
from flask import send_from_directory, request

from flask_restful import Resource

from project.utils.authentication import authorize_project_visible

API_URL = os.getenv('API_HOST')
RESPONSE_URL = urljoin(API_URL, "projects")
UPLOAD_FOLDER = os.getenv('UPLOAD_URL')
UPLOAD_FOLDER = os.getenv('UPLOAD_FOLDER')

ASSIGNMENT_FILE_NAME = "assignment.md"

Expand All @@ -28,8 +28,22 @@ def get(self, project_id):
Get the assignment files of a project
"""

language = request.args.get('lang')
directory_path = os.path.abspath(os.path.join(UPLOAD_FOLDER, str(project_id)))
assignment_file = os.path.join(directory_path, ASSIGNMENT_FILE_NAME)
file_name = ASSIGNMENT_FILE_NAME
if language:
potential_file = f"assignment_{language}.md"
if os.path.isfile(os.path.join(directory_path, potential_file)):
file_name = potential_file
else:
# Find any .md file that starts with "assignment"
for filename in os.listdir(directory_path):
if filename.startswith("assignment") and filename.endswith(".md"):
file_name = filename
break


assignment_file = os.path.join(directory_path, file_name)

if not os.path.isfile(assignment_file):
# no file is found so return 404
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,6 @@ def get_last_submissions_per_user(project_id):
(Submission.submission_time == latest_submissions.c.max_time)
).all()

if not submissions:
return {"message": "No submissions found", "url": BASE_URL}, 404

return {"message": "Resource fetched succesfully", "data": submissions}, 200

class SubmissionDownload(Resource):
Expand Down
9 changes: 7 additions & 2 deletions backend/project/endpoints/projects/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from project.utils.query_agent import create_model_instance
from project.utils.authentication import login_required_return_uid, authorize_teacher
from project.endpoints.projects.endpoint_parser import parse_project_params
from project.utils.models.course_utils import is_teacher_of_course
from project.utils.models.project_utils import get_course_of_project

API_URL = os.getenv('API_HOST')
Expand Down Expand Up @@ -53,7 +54,8 @@ def get(self, uid=None):
filters = dict(request.args)
conditions = []
for key, value in filters.items():
conditions.append(getattr(Project, key) == value)
if key in Project.__table__.columns:
conditions.append(getattr(Project, key) == value)

# Get the projects
projects = Project.query
Expand All @@ -80,8 +82,11 @@ def post(self, teacher_id=None):
Post functionality for project
using flask_restfull parse lib
"""

project_json = parse_project_params()

if not is_teacher_of_course(teacher_id, project_json["course_id"]):
return {"message":"You are not the teacher of this course"}, 403

filename = None
if "assignment_file" in request.files:
file = request.files["assignment_file"]
Expand Down
37 changes: 8 additions & 29 deletions backend/project/endpoints/submissions/submission_detail.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@
from sqlalchemy import exc
from project.db_in import db
from project.models.submission import Submission
from project.utils.query_agent import delete_by_id_from_model
from project.utils.models.submission_utils import submission_response
from project.utils.authentication import (
authorize_submission_request,
authorize_grader,
authorize_submission_author)
authorize_grader
)

API_HOST = getenv("API_HOST")
UPLOAD_FOLDER = getenv("UPLOAD_FOLDER")
Expand Down Expand Up @@ -88,6 +88,10 @@ def patch(self, submission_id:int) -> dict[str, any]:
return data, 404

# Update the grading field
if set(request.form.keys()) - {"grading"}:
data["message"] = "Invalid data field given, only 'grading' is allowed"
return data, 400

grading = request.form.get("grading")
if grading is not None:
try:
Expand All @@ -106,36 +110,11 @@ def patch(self, submission_id:int) -> dict[str, any]:

data["message"] = f"Submission (submission_id={submission_id}) patched"
data["url"] = urljoin(f"{BASE_URL}/", str(submission.submission_id))
data["data"] = {
"id": urljoin(f"{BASE_URL}/", str(submission.submission_id)),
"user": urljoin(f"{API_HOST}/", f"/users/{submission.uid}"),
"project": urljoin(f"{API_HOST}/", f"/projects/{submission.project_id}"),
"grading": submission.grading,
"time": submission.submission_time,
"status": submission.submission_status
}
data["data"] = submission_response(submission, API_HOST)
return data, 200

except exc.SQLAlchemyError:
session.rollback()
data["message"] = \
f"An error occurred while patching submission (submission_id={submission_id})"
return data, 500

@authorize_submission_author
def delete(self, submission_id: int) -> dict[str, any]:
"""Delete a submission given a submission ID
Args:
submission_id (int): Submission ID
Returns:
dict[str, any]: A message
"""

return delete_by_id_from_model(
Submission,
"submission_id",
submission_id,
BASE_URL
)
5 changes: 4 additions & 1 deletion backend/project/endpoints/submissions/submission_download.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
from flask import Response, stream_with_context
from flask_restful import Resource
from project.models.submission import Submission
from project.utils.authentication import authorize_submission_request
from project.db_in import db

API_HOST = getenv("API_HOST")
UPLOAD_FOLDER = getenv("UPLOAD_FOLDER")
Expand All @@ -18,11 +20,12 @@ class SubmissionDownload(Resource):
"""
Resource to download a submission.
"""
@authorize_submission_request
def get(self, submission_id: int):
"""
Download a submission as a zip file.
"""
submission = Submission.query.get(submission_id)
submission = db.session.get(Submission, submission_id)
if submission is None:
return {
"message": f"Submission (submission_id={submission_id}) not found",
Expand Down
Loading

0 comments on commit ddae7e9

Please sign in to comment.