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' into documentation/swagger-webserver
Browse files Browse the repository at this point in the history
# Conflicts:
#	backend/project/static/OpenAPI_Object.json
  • Loading branch information
Gerwoud committed Mar 27, 2024
2 parents ecb107e + 3336fd4 commit 4d06cfa
Show file tree
Hide file tree
Showing 21 changed files with 302 additions and 163 deletions.
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,21 @@
# UGent-3
# UGent-3 project peristerónas
![tests](https://github.com/SELab-2/UGent-3/actions/workflows/ci-test-frontend.yaml/badge.svg?branch=development)
![linter](https://github.com/SELab-2/UGent-3/actions/workflows/ci-linter-frontend.yaml/badge.svg?branch=development)
![tests](https://github.com/SELab-2/UGent-3/actions/workflows/ci-test-backend.yaml/badge.svg?branch=development)
![linter](https://github.com/SELab-2/UGent-3/actions/workflows/ci-linter-backend.yaml/badge.svg?branch=development)
## Introduction
Project peristerónas was created to aid both teachers and students in achieving a
clear overview of deadlines and projects that need to be submitted.

There's a separate functionality depending on if you're logged in as a teacher or as a student.
For students the main functionality is to have a user-friendly interface to submit projects and check the correctness of their submissions.

When a teacher is logged in they can get an overview of the projects he assigned and check how many students have already
handed in a correct solution for example. It's also possible to edit the project and to grade projects in peristerónas' interface.
## Usage
### Frontend
For the developer instructions of the frontend please refer to the [frontend readme](frontend/README.md)
where clear instructions can be found for usage, test cases, deployment and development.
### Backend
For the developer instructions of the backend please refer to the [backend readme](backend/README.md)
where clear instructions can be found for usage, test cases, deployment and development.
7 changes: 4 additions & 3 deletions backend/db_construct.sql
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
CREATE TYPE role AS ENUM ('STUDENT', 'TEACHER', 'ADMIN');

CREATE TYPE submission_status AS ENUM ('SUCCESS', 'LATE', 'FAIL', 'RUNNING');

CREATE TABLE users (
uid VARCHAR(255),
is_teacher BOOLEAN,
is_admin BOOLEAN,
role role NOT NULL,
PRIMARY KEY(uid)
);

Expand Down Expand Up @@ -57,7 +58,7 @@ CREATE TABLE submissions (
submission_id INT GENERATED ALWAYS AS IDENTITY,
uid VARCHAR(255) NOT NULL,
project_id INT NOT NULL,
grading INTEGER CHECK (grading >= 0 AND grading <= 20),
grading FLOAT CHECK (grading >= 0 AND grading <= 20),
submission_time TIMESTAMP WITH TIME ZONE NOT NULL,
submission_path VARCHAR(50) NOT NULL,
submission_status submission_status NOT NULL,
Expand Down
19 changes: 10 additions & 9 deletions backend/project/endpoints/courses/courses_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@

load_dotenv()
API_URL = getenv("API_HOST")
RESPONSE_URL = urljoin(f"{API_URL}/", "courses")
RESPONSE_URL = urljoin(API_URL + "/", "courses")
BASE_DB_ERROR = "Database error occurred while"

def execute_query_abort_if_db_error(query, url, query_all=False):
"""
Expand All @@ -35,8 +36,8 @@ def execute_query_abort_if_db_error(query, url, query_all=False):
result = query.all()
else:
result = query.first()
except SQLAlchemyError as e:
response = json_message(str(e))
except SQLAlchemyError:
response = json_message(f"{BASE_DB_ERROR} executing query")
response["url"] = url
abort(500, description=response)
return result
Expand All @@ -52,9 +53,9 @@ def add_abort_if_error(to_add, url):
"""
try:
db.session.add(to_add)
except SQLAlchemyError as e:
except SQLAlchemyError:
db.session.rollback()
response = json_message(str(e))
response = json_message(f"{BASE_DB_ERROR} adding object")
response["url"] = url
abort(500, description=response)

Expand All @@ -69,9 +70,9 @@ def delete_abort_if_error(to_delete, url):
"""
try:
db.session.delete(to_delete)
except SQLAlchemyError as e:
except SQLAlchemyError:
db.session.rollback()
response = json_message(str(e))
response = json_message(f"{BASE_DB_ERROR} deleting object")
response["url"] = url
abort(500, description=response)

Expand All @@ -82,9 +83,9 @@ def commit_abort_if_error(url):
"""
try:
db.session.commit()
except SQLAlchemyError as e:
except SQLAlchemyError:
db.session.rollback()
response = json_message(str(e))
response = json_message(f"{BASE_DB_ERROR} committing changes")
response["url"] = url
abort(500, description=response)

Expand Down
55 changes: 51 additions & 4 deletions backend/project/endpoints/projects/project_detail.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,27 @@
for example /projects/1 if the project id of
the corresponding project is 1
"""
from os import getenv
import os
import zipfile
from urllib.parse import urljoin

from flask import request
from flask_restful import Resource

from project.db_in import db

from project.models.project import Project
from project.utils.query_agent import query_by_id_from_model, delete_by_id_from_model, \
patch_by_id_from_model
from project.utils.authentication import authorize_teacher_or_project_admin, \
authorize_teacher_of_project, authorize_project_visible

API_URL = getenv('API_HOST')
from project.endpoints.projects.endpoint_parser import parse_project_params

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


class ProjectDetail(Resource):
"""
Expand Down Expand Up @@ -45,14 +52,54 @@ def patch(self, project_id):
Update method for updating a specific project
filtered by id of that specific project
"""
project_json = parse_project_params()

return patch_by_id_from_model(
output, status_code = patch_by_id_from_model(
Project,
"project_id",
project_id,
RESPONSE_URL,
request.json
project_json
)
if status_code != 200:
return output, status_code

if "assignment_file" in request.files:
file = request.files["assignment_file"]
filename = os.path.basename(file.filename)
project_upload_directory = os.path.join(f"{UPLOAD_FOLDER}", f"{project_id}")
os.makedirs(project_upload_directory, exist_ok=True)
try:
# remove the old file
try:
to_rem_files = os.listdir(project_upload_directory)
for to_rem_file in to_rem_files:
to_rem_file_path = os.path.join(project_upload_directory, to_rem_file)
if os.path.isfile(to_rem_file_path):
os.remove(to_rem_file_path)
except FileNotFoundError:
db.session.rollback()
return ({
"message": "Something went wrong deleting the old project files",
"url": f"{API_URL}/projects/{project_id}"
})

# removed all files now upload the new files
file.save(os.path.join(project_upload_directory, filename))
zip_location = os.path.join(project_upload_directory, filename)
with zipfile.ZipFile(zip_location) as upload_zip:
upload_zip.extractall(project_upload_directory)
project_json["assignment_file"] = filename
except zipfile.BadZipfile:
db.session.rollback()
return ({
"message":
"Please provide a valid .zip file for updating the instructions",
"url": f"{API_URL}/projects/{project_id}"
},
400)

return output, status_code

@authorize_teacher_of_project
def delete(self, project_id):
Expand Down
38 changes: 22 additions & 16 deletions backend/project/endpoints/projects/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
from flask import request, jsonify
from flask_restful import Resource

from project.db_in import db

from project.models.project import Project
from project.utils.query_agent import query_selected_from_model, create_model_instance
from project.utils.authentication import authorize_teacher
Expand All @@ -18,6 +20,7 @@
API_URL = os.getenv('API_HOST')
UPLOAD_FOLDER = os.getenv('UPLOAD_URL')


class ProjectsEndpoint(Resource):
"""
Class for projects endpoints
Expand Down Expand Up @@ -47,10 +50,12 @@ def post(self, teacher_id=None):
using flask_restfull parse lib
"""

file = request.files["assignment_file"]
project_json = parse_project_params()
filename = os.path.basename(file.filename)
project_json["assignment_file"] = filename
filename = None
if "assignment_file" in request.files:
file = request.files["assignment_file"]
filename = os.path.basename(file.filename)
project_json["assignment_file"] = filename

# save the file that is given with the request
try:
Expand All @@ -73,20 +78,21 @@ def post(self, teacher_id=None):
return new_project, status_code

project_upload_directory = os.path.join(f"{UPLOAD_FOLDER}", f"{new_project.project_id}")

os.makedirs(project_upload_directory, exist_ok=True)

file.save(os.path.join(project_upload_directory, filename))
try:
with zipfile.ZipFile(os.path.join(project_upload_directory, filename)) as upload_zip:
upload_zip.extractall(project_upload_directory)
except zipfile.BadZipfile:
return ({
"message": "Please provide a .zip file for uploading the instructions",
"url": f"{API_URL}/projects"
},
400)

if filename is not None:
try:
file.save(os.path.join(project_upload_directory, filename))
zip_location = os.path.join(project_upload_directory, filename)
with zipfile.ZipFile(zip_location) as upload_zip:
upload_zip.extractall(project_upload_directory)
except zipfile.BadZipfile:
os.remove(os.path.join(project_upload_directory, filename))
db.session.rollback()
return ({
"message": "Please provide a .zip file for uploading the instructions",
"url": f"{API_URL}/projects"
},
400)
return {
"message": "Project created succesfully",
"data": new_project,
Expand Down
12 changes: 9 additions & 3 deletions backend/project/endpoints/submissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,10 +212,16 @@ def patch(self, submission_id:int) -> dict[str, any]:
# Update the grading field
grading = request.form.get("grading")
if grading is not None:
if not (grading.isdigit() and 0 <= int(grading) <= 20):
data["message"] = "Invalid grading (grading=0-20)"
try:
grading_float = float(grading)
if 0 <= grading_float <= 20:
submission.grading = grading_float
else:
data["message"] = "Invalid grading (grading=0-20)"
return data, 400
except ValueError:
data["message"] = "Invalid grading (not a valid float)"
return data, 400
submission.grading = int(grading)

# Save the submission
session.commit()
Expand Down
45 changes: 20 additions & 25 deletions backend/project/endpoints/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from sqlalchemy.exc import SQLAlchemyError

from project import db
from project.models.user import User as userModel
from project.models.user import User as userModel, Role
from project.utils.authentication import login_required, authorize_user, \
authorize_admin, not_allowed

Expand All @@ -29,16 +29,13 @@ def get(self):
"""
try:
query = userModel.query
is_teacher = request.args.get('is_teacher')
is_admin = request.args.get('is_admin')

if is_teacher is not None:
query = query.filter(userModel.is_teacher == (is_teacher.lower() == 'true'))

if is_admin is not None:
query = query.filter(userModel.is_admin == (is_admin.lower() == 'true'))
role = request.args.get("role")
if role is not None:
role = Role[role.upper()]
query = query.filter(userModel.role == role)

users = query.all()
users = [user.to_dict() for user in users]

result = jsonify({"message": "Queried all users", "data": users,
"url":f"{API_URL}/users", "status_code": 200})
Expand All @@ -54,26 +51,25 @@ def post(self):
It should create a new user and return a success message.
"""
uid = request.json.get('uid')
is_teacher = request.json.get('is_teacher')
is_admin = request.json.get('is_admin')
role = request.json.get("role")
role = Role[role.upper()] if role is not None else None
url = f"{API_URL}/users"

if is_teacher is None or is_admin is None or uid is None:
if role is None or uid is None:
return {
"message": "Invalid request data!",
"correct_format": {
"uid": "User ID (string)",
"is_teacher": "Teacher status (boolean)",
"is_admin": "Admin status (boolean)"
},"url": url
"role": "User role (string)"
},"url": f"{API_URL}/users"
}, 400
try:
user = db.session.get(userModel, uid)
if user is not None:
# Bad request, error code could be 409 but is rarely used
return {"message": f"User {uid} already exists"}, 400
# Code to create a new user in the database using the uid, is_teacher, and is_admin
new_user = userModel(uid=uid, is_teacher=is_teacher, is_admin=is_admin)
# Code to create a new user in the database using the uid and role
new_user = userModel(uid=uid, role=role)
db.session.add(new_user)
db.session.commit()
return jsonify({"message": "User created successfully!",
Expand All @@ -99,7 +95,7 @@ def get(self, user_id):
if user is None:
return {"message": "User not found!","url": f"{API_URL}/users"}, 404

return jsonify({"message": "User queried","data":user,
return jsonify({"message": "User queried","data":user.to_dict(),
"url": f"{API_URL}/users/{user.uid}", "status_code": 200})
except SQLAlchemyError:
return {"message": "An error occurred while fetching the user",
Expand All @@ -114,22 +110,21 @@ def patch(self, user_id):
dict: A dictionary containing the message indicating the success
or failure of the update.
"""
is_teacher = request.json.get('is_teacher')
is_admin = request.json.get('is_admin')
role = request.json.get("role")
role = Role[role.upper()] if role is not None else None
try:
user = db.session.get(userModel, user_id)
if user is None:
return {"message": "User not found!","url": f"{API_URL}/users"}, 404

if is_teacher is not None:
user.is_teacher = is_teacher
if is_admin is not None:
user.is_admin = is_admin
if role is not None:
user.role = role

# Save the changes to the database
db.session.commit()
return jsonify({"message": "User updated successfully!",
"data": user, "url": f"{API_URL}/users/{user.uid}", "status_code": 200})
"data": user.to_dict(),
"url": f"{API_URL}/users/{user.uid}", "status_code": 200})
except SQLAlchemyError:
# every exception should result in a rollback
db.session.rollback()
Expand Down
3 changes: 2 additions & 1 deletion backend/project/models/submission.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
Integer,
CheckConstraint,
DateTime,
Float,
Enum as EnumField)
from project.db_in import db

Expand All @@ -35,7 +36,7 @@ class Submission(db.Model):
submission_id: int = Column(Integer, primary_key=True)
uid: str = Column(String(255), ForeignKey("users.uid"), nullable=False)
project_id: int = Column(Integer, ForeignKey("projects.project_id"), nullable=False)
grading: int = Column(Integer, CheckConstraint("grading >= 0 AND grading <= 20"))
grading: float = Column(Float, CheckConstraint("grading >= 0 AND grading <= 20"))
submission_time: DateTime = Column(DateTime(timezone=True), nullable=False)
submission_path: str = Column(String(50), nullable=False)
submission_status: SubmissionStatus = Column(
Expand Down
Loading

0 comments on commit 4d06cfa

Please sign in to comment.