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 backend/user_name_in_db
Browse files Browse the repository at this point in the history
  • Loading branch information
Vucis authored Mar 27, 2024
2 parents 590dc25 + f8fee40 commit 26daf2c
Show file tree
Hide file tree
Showing 28 changed files with 1,363 additions and 862 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.
5 changes: 3 additions & 2 deletions backend/db_construct.sql
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
CREATE TYPE role AS ENUM ('STUDENT', 'TEACHER', 'ADMIN');

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

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

Expand Down
2 changes: 2 additions & 0 deletions backend/project/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from .endpoints.projects.project_endpoint import project_bp
from .endpoints.submissions import submissions_bp
from .endpoints.courses.join_codes.join_codes_config import join_codes_bp
from .endpoints.docs.docs_endpoint import swagger_ui_blueprint

def create_app():
"""
Expand All @@ -25,6 +26,7 @@ def create_app():
app.register_blueprint(project_bp)
app.register_blueprint(submissions_bp)
app.register_blueprint(join_codes_bp)
app.register_blueprint(swagger_ui_blueprint)

return app

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
17 changes: 17 additions & 0 deletions backend/project/endpoints/docs/docs_endpoint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""
Module for defining the swagger docs
"""

from os import getenv
from flask_swagger_ui import get_swaggerui_blueprint

SWAGGER_URL = getenv("DOCS_URL")
API_URL = getenv("DOCS_JSON_PATH")

swagger_ui_blueprint = get_swaggerui_blueprint(
SWAGGER_URL,
f"/{API_URL}",
config={
'app_name': 'Pigeonhole API'
}
)
7 changes: 4 additions & 3 deletions backend/project/endpoints/index/index.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
"""Index api point"""
import os
from flask import Blueprint, send_from_directory
from flask import Blueprint, send_file
from flask_restful import Resource, Api

index_bp = Blueprint("index", __name__)
index_endpoint = Api(index_bp)

API_URL = os.getenv("DOCS_JSON_PATH")

class Index(Resource):
"""Api endpoint for the / route"""

Expand All @@ -14,8 +16,7 @@ def get(self):
Example of an api endpoint function that will respond to get requests made to
return a json data structure with key Message and value Hello World!
"""
dir_path = os.path.dirname(os.path.realpath(__file__))
return send_from_directory(dir_path, "OpenAPI_Object.json")
return send_file(API_URL)


index_bp.add_url_rule("/", view_func=Index.as_view("index"))
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
31 changes: 23 additions & 8 deletions backend/project/models/user.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,34 @@
"""User model"""

from enum import Enum
from dataclasses import dataclass
from sqlalchemy import Boolean, Column, String
from sqlalchemy import Column, String, Enum as EnumField
from project.db_in import db

class Role(Enum):
"""This class defines the roles of a user"""
STUDENT = 0
TEACHER = 1
ADMIN = 2

@dataclass
class User(db.Model):
"""This class defines the users table,
a user has a uid,
a display_name,
is_teacher and is_admin booleans because a user
can be either a student, admin or teacher"""
"""
a user has a uid,
a display_name and a role, a user
can be either a student,admin or teacher
"""

__tablename__ = "users"
uid: str = Column(String(255), primary_key=True)
display_name: str = Column(String(255))
is_teacher: bool = Column(Boolean)
is_admin: bool = Column(Boolean)
role: Role = Column(EnumField(Role), nullable=False)
def to_dict(self):
"""
Converts a User to a serializable dict
"""
return {
'uid': self.uid,
'role': self.role.name, # Convert the enum to a string
'display_name': self.display_name
}
Loading

0 comments on commit 26daf2c

Please sign in to comment.