From 12830d56134c0bec58a0c147184de0f614e59dab Mon Sep 17 00:00:00 2001 From: abuzogan Date: Wed, 6 Mar 2024 13:42:58 +0100 Subject: [PATCH 01/56] added query agent containing functions that can be used by multiple endpoints --- backend/project/utils/query_agent.py | 119 +++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 backend/project/utils/query_agent.py diff --git a/backend/project/utils/query_agent.py b/backend/project/utils/query_agent.py new file mode 100644 index 00000000..947089e9 --- /dev/null +++ b/backend/project/utils/query_agent.py @@ -0,0 +1,119 @@ +""" +This module contains the functions to interact with the database. It contains functions to +delete, insert and query entries from the database. The functions are used by the routes +to interact with the database. +""" + +from typing import Dict, List, Union +from flask import jsonify +from sqlalchemy import and_ +from sqlalchemy.ext.declarative import DeclarativeMeta +from sqlalchemy.orm.query import Query +from sqlalchemy.exc import SQLAlchemyError +from project.db_in import db + +def delete_by_id_from_model(model: DeclarativeMeta, column_name: str, column_id: int): + """ + Deletes an entry from the database giving the model corresponding to a certain table, + a column name and its value. + + Args: + model: DeclarativeMeta - The model corresponding to the table to delete from. + column_name: str - The name of the column to delete from. + id: int - The id of the entry to delete. + + Returns: + A message indicating that the resource was deleted successfully if the operation was + successful, otherwise a message indicating that something went wrong while deleting from + the database. + """ + try: + result: DeclarativeMeta = model.query.filter( + getattr(model, column_name) == column_id + ).first() + + if not result: + return {"message": "Resource not found"}, 404 + db.session.delete(result) + db.session.commit() + return {"message": "Resource deleted successfully"}, 200 + except SQLAlchemyError: + return {"error": "Something went wrong while deleting from the database."}, 500 + +def insert_into_model(model: DeclarativeMeta, data: Dict[str, Union[str, int]]): + """ + Inserts a new entry into the database giving the model corresponding to a certain table + and the data to insert. + + Args: + model: DeclarativeMeta - The model corresponding to the table to insert into. + data: Dict[str, Union[str, int]] - The data to insert into the table. + + Returns: + The new entry inserted into the database if the operation was successful, otherwise + a message indicating that something went wrong while inserting into the database. + """ + try: + new_instance: DeclarativeMeta = model(**data) + db.session.add(new_instance) + db.session.commit() + return new_instance, 201 + except SQLAlchemyError: + return {"error": "Something went wrong while inserting into the database."}, 500 + +def query_selected_from_model(model: DeclarativeMeta, + select_values: List[str] = None, + filters: Dict[str, Union[str, int]]=None): + """ + Query all entries from the database giving the model corresponding to a certain table, + the columns to select and the filters to apply. + + Args: + model: DeclarativeMeta - The model corresponding to the table to query from. + select_values: List[str] - The values to select from the table. + filters: Dict[str, Union[str, int]] - The filters to apply to the query. + + Returns: + The entries queried from the database if they exist, otherwise a message indicating + that the resource was not found. + """ + try: + query: Query = model.query + if select_values: + query = query.with_entities(*[getattr(model, value) for value in select_values]) + if filters: + conditions: List[bool] = [] + for key, value in filters.items(): + conditions.append(getattr(model, key) == value) + query = query.filter(and_(*conditions)) + results: List[DeclarativeMeta] = query.all() + return jsonify(results), 200 + except SQLAlchemyError: + return {"error": "Something went wrong while querying the database."}, 500 + +def query_by_id_from_model(model: DeclarativeMeta, + column_name: str, + column_id: int, + not_found_message: str="Resource not found"): + """ + Query an entry from the database giving the model corresponding to a certain table, + a column name and its value. + + Args: + model: DeclarativeMeta - The model corresponding to the table to query from. + column_name: str - The name of the column to query from. + id: int - The id of the entry to query. + not_found_message: str - The message to return if the entry is not found. + + Returns: + The entry queried from the database if it exists, otherwise a message indicating + that the resource was not found. + + """ + try: + result: Query = model.query.filter(getattr(model, column_name) == column_id).first() + if not result: + return {"message": not_found_message}, 404 + return jsonify(result), 200 + except SQLAlchemyError: + return {"error": "Something went wrong while querying the database."}, 500 From 81e3d3c955c3e483d792bff84f097d7f327fbb4c Mon Sep 17 00:00:00 2001 From: abuzogan Date: Wed, 6 Mar 2024 15:25:29 +0100 Subject: [PATCH 02/56] loading env variables is only necessary in __main__ --- backend/project/__main__.py | 5 ++--- backend/project/endpoints/courses.py | 2 -- backend/project/endpoints/projects/project_detail.py | 2 -- backend/project/sessionmaker.py | 3 --- 4 files changed, 2 insertions(+), 10 deletions(-) diff --git a/backend/project/__main__.py b/backend/project/__main__.py index 2f312c85..a4bd122b 100644 --- a/backend/project/__main__.py +++ b/backend/project/__main__.py @@ -1,11 +1,10 @@ """Main entry point for the application.""" -from sys import path +from dotenv import load_dotenv from project import create_app_with_db from project.db_in import url -path.append(".") - if __name__ == "__main__": + load_dotenv() app = create_app_with_db(url) app.run(debug=True) diff --git a/backend/project/endpoints/courses.py b/backend/project/endpoints/courses.py index 1903e9e3..a09e7cfb 100644 --- a/backend/project/endpoints/courses.py +++ b/backend/project/endpoints/courses.py @@ -1,7 +1,6 @@ """Course api point""" from os import getenv -from dotenv import load_dotenv from flask import Blueprint, jsonify, request from flask import abort from flask_restful import Api, Resource @@ -15,7 +14,6 @@ courses_bp = Blueprint("courses", __name__) courses_api = Api(courses_bp) -load_dotenv() API_URL = getenv("API_HOST") diff --git a/backend/project/endpoints/projects/project_detail.py b/backend/project/endpoints/projects/project_detail.py index 88989247..5428dcac 100644 --- a/backend/project/endpoints/projects/project_detail.py +++ b/backend/project/endpoints/projects/project_detail.py @@ -4,7 +4,6 @@ the corresponding project is 1 """ from os import getenv -from dotenv import load_dotenv from flask import jsonify from flask_restful import Resource, abort @@ -14,7 +13,6 @@ from project import db from project.models.projects import Project -load_dotenv() API_URL = getenv('API_HOST') class ProjectDetail(Resource): diff --git a/backend/project/sessionmaker.py b/backend/project/sessionmaker.py index 9fbf1cad..0ab68f8e 100644 --- a/backend/project/sessionmaker.py +++ b/backend/project/sessionmaker.py @@ -1,11 +1,8 @@ """initialise a datab session""" from os import getenv -from dotenv import load_dotenv from sqlalchemy import create_engine, URL from sqlalchemy.orm import sessionmaker -load_dotenv() - url = URL.create( drivername="postgresql", username=getenv("POSTGRES_USER"), From 658dfd35b355638b08c51d01b19c2352ae629fca Mon Sep 17 00:00:00 2001 From: abuzogan Date: Wed, 6 Mar 2024 15:30:16 +0100 Subject: [PATCH 03/56] removed unneeded load_dotenv --- backend/project/db_in.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/backend/project/db_in.py b/backend/project/db_in.py index ebcc02dd..57a572fa 100644 --- a/backend/project/db_in.py +++ b/backend/project/db_in.py @@ -2,13 +2,10 @@ import os from flask_sqlalchemy import SQLAlchemy -from dotenv import load_dotenv from sqlalchemy import URL db = SQLAlchemy() -load_dotenv() - DATABSE_NAME = os.getenv("POSTGRES_DB") DATABASE_USER = os.getenv("POSTGRES_USER") DATABASE_PASSWORD = os.getenv("POSTGRES_PASSWORD") From 7dd56821d8141508d48aecd0d44fde32986c6a35 Mon Sep 17 00:00:00 2001 From: abuzogan Date: Wed, 6 Mar 2024 15:32:46 +0100 Subject: [PATCH 04/56] completed functions that are ought to be used by multiple endpoints or files --- backend/project/utils/misc.py | 38 +++++++++++++++++++++ backend/project/utils/query_agent.py | 50 ++++++++++++++++++++++------ 2 files changed, 77 insertions(+), 11 deletions(-) create mode 100644 backend/project/utils/misc.py diff --git a/backend/project/utils/misc.py b/backend/project/utils/misc.py new file mode 100644 index 00000000..3c348175 --- /dev/null +++ b/backend/project/utils/misc.py @@ -0,0 +1,38 @@ +from typing import Dict, List +from urllib.parse import urljoin + +def map_keys_to_url(url_mapper: Dict[str, str], data: Dict[str, str]) -> Dict[str, str]: + """ + Maps keys to a url using a url mapper. + + Args: + url_mapper: Dict[str, str] - A dictionary that maps keys to urls. + data: Dict[str, str] - The data to map to urls. + + Returns: + A dictionary with the keys mapped to the urls. + """ + for key, value in data.items(): + if key in url_mapper: + data[key] = urljoin(url_mapper[key], str(value)) + return data + +def map_all_keys_to_url(url_mapper: Dict[str, str], data: List[Dict[str, str]]): + """ + Maps all keys to a url using a url mapper. + + Args: + url_mapper: Dict[str, str] - A dictionary that maps keys to urls. + data: List[Dict[str, str]] - The data to map to urls. + + Returns: + A list of dictionaries with the keys mapped to the urls. + """ + print(data) + return [map_keys_to_url(url_mapper, entry) for entry in data] + +def model_to_dict(instance): + return {column.key: getattr(instance, column.key) for column in instance.__table__.columns} + +def models_to_dict(instances): + return [model_to_dict(instance) for instance in instances] \ No newline at end of file diff --git a/backend/project/utils/query_agent.py b/backend/project/utils/query_agent.py index 947089e9..9b348350 100644 --- a/backend/project/utils/query_agent.py +++ b/backend/project/utils/query_agent.py @@ -5,12 +5,14 @@ """ from typing import Dict, List, Union +from urllib.parse import urljoin from flask import jsonify from sqlalchemy import and_ from sqlalchemy.ext.declarative import DeclarativeMeta from sqlalchemy.orm.query import Query from sqlalchemy.exc import SQLAlchemyError from project.db_in import db +from project.utils.misc import map_all_keys_to_url, models_to_dict def delete_by_id_from_model(model: DeclarativeMeta, column_name: str, column_id: int): """ @@ -40,7 +42,9 @@ def delete_by_id_from_model(model: DeclarativeMeta, column_name: str, column_id: except SQLAlchemyError: return {"error": "Something went wrong while deleting from the database."}, 500 -def insert_into_model(model: DeclarativeMeta, data: Dict[str, Union[str, int]]): +def insert_into_model(model: DeclarativeMeta, + data: Dict[str, Union[str, int]], + response_url_base: str): """ Inserts a new entry into the database giving the model corresponding to a certain table and the data to insert. @@ -48,6 +52,7 @@ def insert_into_model(model: DeclarativeMeta, data: Dict[str, Union[str, int]]): Args: model: DeclarativeMeta - The model corresponding to the table to insert into. data: Dict[str, Union[str, int]] - The data to insert into the table. + response_url_base: str - The base url to use in the response. Returns: The new entry inserted into the database if the operation was successful, otherwise @@ -57,20 +62,28 @@ def insert_into_model(model: DeclarativeMeta, data: Dict[str, Union[str, int]]): new_instance: DeclarativeMeta = model(**data) db.session.add(new_instance) db.session.commit() - return new_instance, 201 + return {"data": new_instance, + "message": "Object created succesfully.", + "url": urljoin(response_url_base, str(new_instance.project_id))}, 201 except SQLAlchemyError: - return {"error": "Something went wrong while inserting into the database."}, 500 + return {"error": "Something went wrong while inserting into the database.", + "url": response_url_base}, 500 def query_selected_from_model(model: DeclarativeMeta, + response_url: str, + url_mapper: Dict[str, str] = None, select_values: List[str] = None, filters: Dict[str, Union[str, int]]=None): """ - Query all entries from the database giving the model corresponding to a certain table, - the columns to select and the filters to apply. + Query entries from the database giving the model corresponding to a certain table and + the filters to apply to the query. + Args: model: DeclarativeMeta - The model corresponding to the table to query from. - select_values: List[str] - The values to select from the table. + response_url: str - The base url to use in the response. + url_mapper: Dict[str, str] - A dictionary to map the keys of the response to urls. + select_values: List[str] - The columns to select from the table. filters: Dict[str, Union[str, int]] - The filters to apply to the query. Returns: @@ -79,17 +92,32 @@ def query_selected_from_model(model: DeclarativeMeta, """ try: query: Query = model.query - if select_values: - query = query.with_entities(*[getattr(model, value) for value in select_values]) if filters: conditions: List[bool] = [] for key, value in filters.items(): conditions.append(getattr(model, key) == value) query = query.filter(and_(*conditions)) - results: List[DeclarativeMeta] = query.all() - return jsonify(results), 200 + + if select_values: + query = query.with_entities(*[getattr(model, value) for value in select_values]) + query_result = query.all() + results = [] + for instance in query_result: + selected_instance = {} + for value in select_values: + selected_instance[value] = getattr(instance, value) + results.append(selected_instance) + else: + results = models_to_dict(query.all()) + if url_mapper: + results = map_all_keys_to_url(url_mapper, results) + response = {"data": results, + "message": "Resources fetched successfully", + "url": response_url} + return jsonify(response), 200 except SQLAlchemyError: - return {"error": "Something went wrong while querying the database."}, 500 + return {"error": "Something went wrong while querying the database.", + "url": response_url}, 500 def query_by_id_from_model(model: DeclarativeMeta, column_name: str, From e42c57ded2cbedd64217d31e0aa8808e0b5e4771 Mon Sep 17 00:00:00 2001 From: abuzogan Date: Wed, 6 Mar 2024 15:33:06 +0100 Subject: [PATCH 05/56] simplified endpoint functions by using query_agent functions --- .../project/endpoints/projects/projects.py | 78 ++++--------------- 1 file changed, 13 insertions(+), 65 deletions(-) diff --git a/backend/project/endpoints/projects/projects.py b/backend/project/endpoints/projects/projects.py index f444e283..cb516b1a 100644 --- a/backend/project/endpoints/projects/projects.py +++ b/backend/project/endpoints/projects/projects.py @@ -2,18 +2,14 @@ Module that implements the /projects endpoint of the API """ from os import getenv -from dotenv import load_dotenv -from flask import jsonify +from flask import request from flask_restful import Resource -from sqlalchemy import exc +from urllib.parse import urljoin - -from project import db from project.models.projects import Project -from project.endpoints.projects.endpoint_parser import parse_project_params +from project.utils.query_agent import query_selected_from_model, insert_into_model -load_dotenv() API_URL = getenv('API_HOST') class ProjectsEndpoint(Resource): @@ -28,67 +24,19 @@ def get(self): Get method for listing all available projects that are currently in the API """ - try: - projects = Project.query.with_entities( - Project.project_id, - Project.title, - Project.descriptions - ).all() - - results = [{ - "project_id": row[0], - "title": row[1], - "descriptions": row[2] - } for row in projects] - - # return all valid entries for a project and return a 200 OK code - return { - "data": results, - "url": f"{API_URL}/projects", - "message": "Projects fetched successfully" - }, 200 - except exc.SQLAlchemyError: - return { - "message": "Something unexpected happenend when trying to get the projects", - "url": f"{API_URL}/projects" - }, 500 + SUMMARY_FIELDS = ["project_id", "title", "descriptions"] + response_url = urljoin(API_URL, "/projects") + return query_selected_from_model(Project, + response_url, + select_values=SUMMARY_FIELDS, + url_mapper={"project_id": response_url}, + filters=request.args + ) def post(self): """ Post functionality for project using flask_restfull parse lib """ - args = parse_project_params() - - # create a new project object to add in the API later - new_project = Project( - title=args['title'], - descriptions=args['descriptions'], - assignment_file=args['assignment_file'], - deadline=args['deadline'], - course_id=args['course_id'], - visible_for_students=args['visible_for_students'], - archieved=args['archieved'], - test_path=args['test_path'], - script_name=args['script_name'], - regex_expressions=args['regex_expressions'] - ) - - # add the new project to the database and commit the changes - - try: - db.session.add(new_project) - db.session.commit() - new_project_json = jsonify(new_project).json - - return { - "url": f"{API_URL}/projects/{new_project_json['project_id']}", - "message": "Project posted successfully", - "data": new_project_json - }, 201 - except exc.SQLAlchemyError: - return ({ - "url": f"{API_URL}/projects", - "message": "Something unexpected happenend when trying to add a new project", - "data": jsonify(new_project).json - }, 500) + + return insert_into_model(Project, request.json, urljoin(API_URL, "/projects")) From 3467bb22b6213a2a81878e480a2248b53b949afa Mon Sep 17 00:00:00 2001 From: abuzogan Date: Wed, 6 Mar 2024 15:41:07 +0100 Subject: [PATCH 06/56] fixed linting --- .../project/endpoints/projects/projects.py | 7 ++--- backend/project/utils/misc.py | 31 +++++++++++++++++-- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/backend/project/endpoints/projects/projects.py b/backend/project/endpoints/projects/projects.py index cb516b1a..638cbabc 100644 --- a/backend/project/endpoints/projects/projects.py +++ b/backend/project/endpoints/projects/projects.py @@ -2,10 +2,10 @@ Module that implements the /projects endpoint of the API """ from os import getenv +from urllib.parse import urljoin from flask import request from flask_restful import Resource -from urllib.parse import urljoin from project.models.projects import Project from project.utils.query_agent import query_selected_from_model, insert_into_model @@ -24,11 +24,10 @@ def get(self): Get method for listing all available projects that are currently in the API """ - SUMMARY_FIELDS = ["project_id", "title", "descriptions"] response_url = urljoin(API_URL, "/projects") return query_selected_from_model(Project, response_url, - select_values=SUMMARY_FIELDS, + select_values=["project_id", "title", "descriptions"], url_mapper={"project_id": response_url}, filters=request.args ) @@ -38,5 +37,5 @@ def post(self): Post functionality for project using flask_restfull parse lib """ - + return insert_into_model(Project, request.json, urljoin(API_URL, "/projects")) diff --git a/backend/project/utils/misc.py b/backend/project/utils/misc.py index 3c348175..cd20a6f7 100644 --- a/backend/project/utils/misc.py +++ b/backend/project/utils/misc.py @@ -1,5 +1,12 @@ +""" +This module contains functions that are not related to anything specific but +are ought to be used throughout the project. +""" + from typing import Dict, List from urllib.parse import urljoin +from sqlalchemy.ext.declarative import DeclarativeMeta + def map_keys_to_url(url_mapper: Dict[str, str], data: Dict[str, str]) -> Dict[str, str]: """ @@ -31,8 +38,26 @@ def map_all_keys_to_url(url_mapper: Dict[str, str], data: List[Dict[str, str]]): print(data) return [map_keys_to_url(url_mapper, entry) for entry in data] -def model_to_dict(instance): +def model_to_dict(instance: DeclarativeMeta) -> Dict[str, str]: + """ + Converts an sqlalchemy model to a dictionary. + + Args: + instance: DeclarativeMeta - The instance of the model to convert to a dictionary. + + Returns: + A dictionary with the keys and values of the model. + """ return {column.key: getattr(instance, column.key) for column in instance.__table__.columns} -def models_to_dict(instances): - return [model_to_dict(instance) for instance in instances] \ No newline at end of file +def models_to_dict(instances: List[DeclarativeMeta]) -> List[Dict[str, str]]: + """ + Converts a list of sqlalchemy models to a list of dictionaries. + + Args: + instances: List[DeclarativeMeta] - The instances of the models to convert to dictionaries. + + Returns: + A list of dictionaries with the keys and values of the models. + """ + return [model_to_dict(instance) for instance in instances] From 69df26e9e720f9f1adb80024051937b6e36aefcc Mon Sep 17 00:00:00 2001 From: abuzogan Date: Wed, 6 Mar 2024 15:55:16 +0100 Subject: [PATCH 07/56] fixed urljoin incorrectly joining url --- backend/project/endpoints/projects/projects.py | 16 +++++++++------- backend/project/utils/misc.py | 5 +++-- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/backend/project/endpoints/projects/projects.py b/backend/project/endpoints/projects/projects.py index 638cbabc..d273bfca 100644 --- a/backend/project/endpoints/projects/projects.py +++ b/backend/project/endpoints/projects/projects.py @@ -24,13 +24,15 @@ def get(self): Get method for listing all available projects that are currently in the API """ - response_url = urljoin(API_URL, "/projects") - return query_selected_from_model(Project, - response_url, - select_values=["project_id", "title", "descriptions"], - url_mapper={"project_id": response_url}, - filters=request.args - ) + + response_url = urljoin(API_URL, "projects") + return query_selected_from_model( + Project, + response_url, + select_values=["project_id", "title", "descriptions"], + url_mapper={"project_id": response_url}, + filters=request.args + ) def post(self): """ diff --git a/backend/project/utils/misc.py b/backend/project/utils/misc.py index cd20a6f7..2c82fbac 100644 --- a/backend/project/utils/misc.py +++ b/backend/project/utils/misc.py @@ -21,7 +21,9 @@ def map_keys_to_url(url_mapper: Dict[str, str], data: Dict[str, str]) -> Dict[st """ for key, value in data.items(): if key in url_mapper: - data[key] = urljoin(url_mapper[key], str(value)) + data[key] = urljoin(url_mapper[key] + "/", str(value)) + print(url_mapper) + print(data) return data def map_all_keys_to_url(url_mapper: Dict[str, str], data: List[Dict[str, str]]): @@ -35,7 +37,6 @@ def map_all_keys_to_url(url_mapper: Dict[str, str], data: List[Dict[str, str]]): Returns: A list of dictionaries with the keys mapped to the urls. """ - print(data) return [map_keys_to_url(url_mapper, entry) for entry in data] def model_to_dict(instance: DeclarativeMeta) -> Dict[str, str]: From e836f5037268a742b45877ed007cc3e5621b0e5d Mon Sep 17 00:00:00 2001 From: abuzogan Date: Wed, 6 Mar 2024 17:02:57 +0100 Subject: [PATCH 08/56] lint: removed trailing whitepsace --- backend/project/endpoints/projects/projects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/project/endpoints/projects/projects.py b/backend/project/endpoints/projects/projects.py index d273bfca..68600034 100644 --- a/backend/project/endpoints/projects/projects.py +++ b/backend/project/endpoints/projects/projects.py @@ -24,7 +24,7 @@ def get(self): Get method for listing all available projects that are currently in the API """ - + response_url = urljoin(API_URL, "projects") return query_selected_from_model( Project, From e26c015bdb5a3080c0615690d5138a442cb61fb0 Mon Sep 17 00:00:00 2001 From: abuzogan Date: Wed, 6 Mar 2024 17:41:17 +0100 Subject: [PATCH 09/56] completely replaced functionality with query_agent functions --- .../endpoints/projects/project_detail.py | 97 +++++-------------- .../project/endpoints/projects/projects.py | 5 +- 2 files changed, 27 insertions(+), 75 deletions(-) diff --git a/backend/project/endpoints/projects/project_detail.py b/backend/project/endpoints/projects/project_detail.py index 5428dcac..f2ce1a00 100644 --- a/backend/project/endpoints/projects/project_detail.py +++ b/backend/project/endpoints/projects/project_detail.py @@ -4,16 +4,18 @@ the corresponding project is 1 """ from os import getenv +from urllib.parse import urljoin -from flask import jsonify -from flask_restful import Resource, abort -from sqlalchemy import exc -from project.endpoints.projects.endpoint_parser import parse_project_params +from flask import request +from flask_restful import Resource from project import db from project.models.projects import Project +from project.utils.query_agent import query_by_id_from_model, delete_by_id_from_model, patch_by_id_from_model + API_URL = getenv('API_HOST') +RESPONSE_URL = urljoin(API_URL, "projects") class ProjectDetail(Resource): """ @@ -22,14 +24,6 @@ class ProjectDetail(Resource): for implementing get, delete and put methods """ - def abort_if_not_present(self, project): - """ - Check if the project exists in the database - and if not abort the request and give back a 404 not found - """ - if project is None: - abort(404) - def get(self, project_id): """ Get method for listing a specific project @@ -37,22 +31,11 @@ def get(self, project_id): the id fetched from the url with the reaparse """ - try: - # fetch the project with the id that is specified in the url - project = Project.query.filter_by(project_id=project_id).first() - self.abort_if_not_present(project) - - # return the fetched project and return 200 OK status - return { - "data": jsonify(project).json, - "url": f"{API_URL}/projects/{project_id}", - "message": "Got project successfully" - }, 200 - except exc.SQLAlchemyError: - return { - "message": "Internal server error", - "url": f"{API_URL}/projects/{project_id}" - }, 500 + return query_by_id_from_model( + Project, + "project_id", + project_id, + RESPONSE_URL) def patch(self, project_id): """ @@ -60,30 +43,13 @@ def patch(self, project_id): filtered by id of that specific project """ - # get the project that need to be edited - project = Project.query.filter_by(project_id=project_id).first() - - # check which values are not None in the dict - # if it is not None it needs to be modified in the database - - # commit the changes and return the 200 OK code if it succeeds, else 500 - try: - var_dict = parse_project_params() - for key, value in var_dict.items(): - setattr(project, key, value) - db.session.commit() - # get the updated version - return { - "message": f"Succesfully changed project with id: {id}", - "url": f"{API_URL}/projects/{id}", - "data": project - }, 200 - except exc.SQLAlchemyError: - db.session.rollback() - return { - "message": f"Something unexpected happenend when trying to edit project {id}", - "url": f"{API_URL}/projects/{id}" - }, 500 + return patch_by_id_from_model( + Project, + "project_id", + project_id, + RESPONSE_URL, + request.json + ) def delete(self, project_id): """ @@ -91,25 +57,8 @@ def delete(self, project_id): done by project id """ - # fetch the project that needs to be removed - deleted_project = Project.query.filter_by(project_id=project_id).first() - - # check if its an existing one - self.abort_if_not_present(deleted_project) - - # if it exists delete it and commit the changes in the database - try: - db.session.delete(deleted_project) - db.session.commit() - - # return 200 if content is deleted succesfully - return { - "message": f"Project with id: {id} deleted successfully", - "url": f"{API_URL}/projects/{id} deleted successfully!", - "data": deleted_project - }, 200 - except exc.SQLAlchemyError: - return { - "message": f"Something unexpected happened when removing project {project_id}", - "url": f"{API_URL}/projects/{id}" - }, 500 + return delete_by_id_from_model( + Project, + "project_id", + project_id, + RESPONSE_URL) diff --git a/backend/project/endpoints/projects/projects.py b/backend/project/endpoints/projects/projects.py index 68600034..0834988f 100644 --- a/backend/project/endpoints/projects/projects.py +++ b/backend/project/endpoints/projects/projects.py @@ -40,4 +40,7 @@ def post(self): using flask_restfull parse lib """ - return insert_into_model(Project, request.json, urljoin(API_URL, "/projects")) + return insert_into_model( + Project,request.json, + urljoin(API_URL, "/projects"), + "project_id") From fd941b2b33d3cc44d02324ced058a459587a8476 Mon Sep 17 00:00:00 2001 From: abuzogan Date: Wed, 6 Mar 2024 17:41:35 +0100 Subject: [PATCH 10/56] added functionality for patching an entry in the database --- backend/project/utils/query_agent.py | 69 +++++++++++++++++++++++----- 1 file changed, 58 insertions(+), 11 deletions(-) diff --git a/backend/project/utils/query_agent.py b/backend/project/utils/query_agent.py index 9b348350..3837820f 100644 --- a/backend/project/utils/query_agent.py +++ b/backend/project/utils/query_agent.py @@ -14,7 +14,7 @@ from project.db_in import db from project.utils.misc import map_all_keys_to_url, models_to_dict -def delete_by_id_from_model(model: DeclarativeMeta, column_name: str, column_id: int): +def delete_by_id_from_model(model: DeclarativeMeta, column_name: str, column_id: int, base_url: str): """ Deletes an entry from the database giving the model corresponding to a certain table, a column name and its value. @@ -35,16 +35,21 @@ def delete_by_id_from_model(model: DeclarativeMeta, column_name: str, column_id: ).first() if not result: - return {"message": "Resource not found"}, 404 + return { + "message": "Resource not found", + "url": base_url}, 404 db.session.delete(result) db.session.commit() - return {"message": "Resource deleted successfully"}, 200 + return {"message": "Resource deleted successfully", + "url": base_url}, 200 except SQLAlchemyError: - return {"error": "Something went wrong while deleting from the database."}, 500 + return {"error": "Something went wrong while deleting from the database.", + "url": base_url}, 500 def insert_into_model(model: DeclarativeMeta, data: Dict[str, Union[str, int]], - response_url_base: str): + response_url_base: str, + url_id_field: str): """ Inserts a new entry into the database giving the model corresponding to a certain table and the data to insert. @@ -60,11 +65,13 @@ def insert_into_model(model: DeclarativeMeta, """ try: new_instance: DeclarativeMeta = model(**data) + db.session.add(new_instance) db.session.commit() - return {"data": new_instance, + return jsonify({ + "data": new_instance, "message": "Object created succesfully.", - "url": urljoin(response_url_base, str(new_instance.project_id))}, 201 + "url": urljoin(response_url_base, str(getattr(new_instance, url_id_field)))}), 201 except SQLAlchemyError: return {"error": "Something went wrong while inserting into the database.", "url": response_url_base}, 500 @@ -122,7 +129,7 @@ def query_selected_from_model(model: DeclarativeMeta, def query_by_id_from_model(model: DeclarativeMeta, column_name: str, column_id: int, - not_found_message: str="Resource not found"): + base_url: str): """ Query an entry from the database giving the model corresponding to a certain table, a column name and its value. @@ -141,7 +148,47 @@ def query_by_id_from_model(model: DeclarativeMeta, try: result: Query = model.query.filter(getattr(model, column_name) == column_id).first() if not result: - return {"message": not_found_message}, 404 - return jsonify(result), 200 + return {"message": "Resource not found", "url": base_url}, 404 + print(column_id) + return jsonify({ + "data": result, + "message": "Resource fetched correctly", + "url": urljoin(base_url + "/", str(column_id))}), 200 except SQLAlchemyError: - return {"error": "Something went wrong while querying the database."}, 500 + return { + "error": "Something went wrong while querying the database.", + "url": base_url}, 500 + +def patch_by_id_from_model(model: DeclarativeMeta, + column_name: str, + column_id: int, + base_url: str, + data: Dict[str, Union[str, int]]): + """ + Update an entry from the database giving the model corresponding to a certain table, + a column name and its value. + + Args: + model: DeclarativeMeta - The model corresponding to the table to update. + column_name: str - The name of the column to update. + id: int - The id of the entry to update. + data: Dict[str, Union[str, int]] - The data to update the entry with. + + Returns: + The entry updated from the database if the operation was successful, otherwise + a message indicating that something went wrong while updating the entry. + """ + try: + result: Query = model.query.filter(getattr(model, column_name) == column_id).first() + if not result: + return {"message": "Resource not found", "url": base_url}, 404 + for key, value in data.items(): + setattr(result, key, value) + db.session.commit() + return jsonify({ + "data": result, + "message": "Resource updated successfully", + "url": urljoin(base_url + "/", str(column_id))}), 200 + except SQLAlchemyError: + return {"error": "Something went wrong while updating the database.", + "url": base_url}, 500 From 073d6c57c329f9a40a31fd7583a6a5656803baeb Mon Sep 17 00:00:00 2001 From: abuzogan Date: Wed, 6 Mar 2024 17:46:55 +0100 Subject: [PATCH 11/56] fixed linting --- backend/project/endpoints/projects/project_detail.py | 4 ++-- backend/project/utils/query_agent.py | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/backend/project/endpoints/projects/project_detail.py b/backend/project/endpoints/projects/project_detail.py index f2ce1a00..e2314bd9 100644 --- a/backend/project/endpoints/projects/project_detail.py +++ b/backend/project/endpoints/projects/project_detail.py @@ -9,9 +9,9 @@ from flask import request from flask_restful import Resource -from project import db from project.models.projects 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.query_agent import query_by_id_from_model, delete_by_id_from_model, \ + patch_by_id_from_model API_URL = getenv('API_HOST') diff --git a/backend/project/utils/query_agent.py b/backend/project/utils/query_agent.py index 3837820f..c4053df4 100644 --- a/backend/project/utils/query_agent.py +++ b/backend/project/utils/query_agent.py @@ -14,7 +14,11 @@ from project.db_in import db from project.utils.misc import map_all_keys_to_url, models_to_dict -def delete_by_id_from_model(model: DeclarativeMeta, column_name: str, column_id: int, base_url: str): +def delete_by_id_from_model( + model: DeclarativeMeta, + column_name: str, + column_id: int, + base_url: str): """ Deletes an entry from the database giving the model corresponding to a certain table, a column name and its value. @@ -65,7 +69,7 @@ def insert_into_model(model: DeclarativeMeta, """ try: new_instance: DeclarativeMeta = model(**data) - + db.session.add(new_instance) db.session.commit() return jsonify({ From fd2ae8379e63c91d15526d758323a5a00f3b8ea9 Mon Sep 17 00:00:00 2001 From: abuzogan Date: Wed, 6 Mar 2024 19:16:22 +0100 Subject: [PATCH 12/56] filtered queries and forms to only contain entries that are valid in table --- backend/project/utils/query_agent.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/backend/project/utils/query_agent.py b/backend/project/utils/query_agent.py index c4053df4..d0595afd 100644 --- a/backend/project/utils/query_agent.py +++ b/backend/project/utils/query_agent.py @@ -7,12 +7,12 @@ from typing import Dict, List, Union from urllib.parse import urljoin from flask import jsonify -from sqlalchemy import and_ +from sqlalchemy import and_, inspect from sqlalchemy.ext.declarative import DeclarativeMeta from sqlalchemy.orm.query import Query from sqlalchemy.exc import SQLAlchemyError from project.db_in import db -from project.utils.misc import map_all_keys_to_url, models_to_dict +from project.utils.misc import map_all_keys_to_url, models_to_dict, filter_model_fields def delete_by_id_from_model( model: DeclarativeMeta, @@ -53,7 +53,8 @@ def delete_by_id_from_model( def insert_into_model(model: DeclarativeMeta, data: Dict[str, Union[str, int]], response_url_base: str, - url_id_field: str): + url_id_field: str, + required_fields: List[str] = []): """ Inserts a new entry into the database giving the model corresponding to a certain table and the data to insert. @@ -68,17 +69,24 @@ def insert_into_model(model: DeclarativeMeta, a message indicating that something went wrong while inserting into the database. """ try: - new_instance: DeclarativeMeta = model(**data) - + # Check if all non-nullable fields are present in the data + missing_fields = [field for field in required_fields if field not in data] + + if missing_fields: + return {"error": f"Missing required fields: {', '.join(missing_fields)}", + "url": response_url_base}, 400 + + filtered_data = filter_model_fields(model, data) + new_instance: DeclarativeMeta = model(**filtered_data) db.session.add(new_instance) db.session.commit() return jsonify({ "data": new_instance, "message": "Object created succesfully.", "url": urljoin(response_url_base, str(getattr(new_instance, url_id_field)))}), 201 - except SQLAlchemyError: - return {"error": "Something went wrong while inserting into the database.", - "url": response_url_base}, 500 + except SQLAlchemyError as e: + return jsonify({"error": "Something went wrong while inserting into the database.", + "url": response_url_base}), 500 def query_selected_from_model(model: DeclarativeMeta, response_url: str, @@ -104,8 +112,9 @@ def query_selected_from_model(model: DeclarativeMeta, try: query: Query = model.query if filters: + filtered_filters = filter_model_fields(model, filters) conditions: List[bool] = [] - for key, value in filters.items(): + for key, value in filtered_filters.items(): conditions.append(getattr(model, key) == value) query = query.filter(and_(*conditions)) From d675fe69348b9e2fa4c682518a96677cd24eb36f Mon Sep 17 00:00:00 2001 From: abuzogan Date: Wed, 6 Mar 2024 19:17:57 +0100 Subject: [PATCH 13/56] created function that filters dict keys that are not in table --- backend/project/utils/misc.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/backend/project/utils/misc.py b/backend/project/utils/misc.py index 2c82fbac..9d313467 100644 --- a/backend/project/utils/misc.py +++ b/backend/project/utils/misc.py @@ -62,3 +62,7 @@ def models_to_dict(instances: List[DeclarativeMeta]) -> List[Dict[str, str]]: A list of dictionaries with the keys and values of the models. """ return [model_to_dict(instance) for instance in instances] + + +def filter_model_fields(model: DeclarativeMeta, data: Dict[str, str]): + return {key: value for key, value in data.items() if hasattr(model, key)} \ No newline at end of file From 2298d8c2536a19b4c9f9035cadb26508a143561a Mon Sep 17 00:00:00 2001 From: abuzogan Date: Wed, 6 Mar 2024 19:19:31 +0100 Subject: [PATCH 14/56] made class serializable --- backend/project/models/courses.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/backend/project/models/courses.py b/backend/project/models/courses.py index dc778706..8d3f0651 100644 --- a/backend/project/models/courses.py +++ b/backend/project/models/courses.py @@ -1,14 +1,16 @@ """The Course model""" +from dataclasses import dataclass from sqlalchemy import Integer, Column, ForeignKey, String from project import db +@dataclass class Course(db.Model): """This class described the courses table, a course has an id, name, optional ufora id and the teacher that created it""" __tablename__ = "courses" - course_id = Column(Integer, primary_key=True) - name = Column(String(50), nullable=False) - ufora_id = Column(String(50), nullable=True) - teacher = Column(String(255), ForeignKey("users.uid"), nullable=False) + course_id: int = Column(Integer, primary_key=True) + name: str = Column(String(50), nullable=False) + ufora_id: str = Column(String(50), nullable=True) + teacher: str = Column(String(255), ForeignKey("users.uid"), nullable=False) From d0e9a10bcc23b38b7ceff44df02a942bbdb910d9 Mon Sep 17 00:00:00 2001 From: abuzogan Date: Wed, 6 Mar 2024 19:20:01 +0100 Subject: [PATCH 15/56] url query is not a valid authentication method, filtered out option --- backend/tests/endpoints/courses_test.py | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/backend/tests/endpoints/courses_test.py b/backend/tests/endpoints/courses_test.py index 4df98cd5..9beb64fe 100644 --- a/backend/tests/endpoints/courses_test.py +++ b/backend/tests/endpoints/courses_test.py @@ -25,19 +25,6 @@ def test_post_courses(self, courses_init_db, client, course_data, invalid_course assert course is not None assert course.teacher == "Bart" - response = client.post( - "/courses?uid=Jef", json=course_data - ) # non existent user - assert response.status_code == 404 - - response = client.post( - "/courses?uid=student_sel2_0", json=course_data - ) # existent user but no rights - assert response.status_code == 403 - - response = client.post("/courses", json=course_data) # bad link, no uid passed - assert response.status_code == 400 - response = client.post( "/courses?uid=Bart", json=invalid_course ) # invalid course @@ -87,12 +74,7 @@ def test_post_courses_course_id_students_and_admins(self, db_with_course, client assert response.status_code == 400 sel2_admins_link = "/courses/" + str(course.course_id) + "/admins" - - response = client.post( - sel2_admins_link + "?uid=student_sel2_0", # unauthorized user - json={"admin_uid": "Rien"}, - ) - assert response.status_code == 403 + course_admins = [ s.uid for s in CourseAdmin.query.filter_by(course_id=course.course_id).all() From 6b3e73300e6aaa82a82671b66f08c52acb6918f5 Mon Sep 17 00:00:00 2001 From: abuzogan Date: Wed, 6 Mar 2024 21:17:33 +0100 Subject: [PATCH 16/56] using query_agent functions to prevent code duplication --- backend/project/endpoints/courses.py | 256 ++++++++++----------------- 1 file changed, 89 insertions(+), 167 deletions(-) diff --git a/backend/project/endpoints/courses.py b/backend/project/endpoints/courses.py index a09e7cfb..4954eea0 100644 --- a/backend/project/endpoints/courses.py +++ b/backend/project/endpoints/courses.py @@ -1,6 +1,10 @@ """Course api point""" from os import getenv +from urllib.parse import urljoin + +from dotenv import load_dotenv + from flask import Blueprint, jsonify, request from flask import abort from flask_restful import Api, Resource @@ -8,14 +12,17 @@ from project.models.course_relations import CourseAdmin, CourseStudent from project.models.users import User from project.models.courses import Course -from project.models.projects import Project +from project.utils.query_agent import query_selected_from_model, \ + insert_into_model,delete_by_id_from_model, \ + patch_by_id_from_model from project import db courses_bp = Blueprint("courses", __name__) courses_api = Api(courses_bp) +load_dotenv() API_URL = getenv("API_HOST") - +RESPONSE_URL = urljoin(API_URL + "/", "courses") def execute_query_abort_if_db_error(query, url, query_all=False): """ @@ -231,7 +238,6 @@ def get_course_abort_if_not_found(course_id): return course - class CourseForUser(Resource): """Api endpoint for the /courses link""" @@ -241,72 +247,27 @@ def get(self): 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. """ - query = Course.query - if "teacher" in request.args: - query = query.filter_by(course_id=request.args.get("teacher")) - if "ufora_id" in request.args: - query = query.filter_by(ufora_id=request.args.get("ufora_id")) - if "name" in request.args: - query = query.filter_by(name=request.args.get("name")) - results = execute_query_abort_if_db_error( - query, url=API_URL + "/courses", query_all=True + + return query_selected_from_model( + Course, + RESPONSE_URL, + url_mapper={"course_id": RESPONSE_URL}, + filters=request.args ) - detail_urls = [ - f"{API_URL}/courses/{str(course.course_id)}" for course in results - ] - message = "Succesfully retrieved all courses with given parameters" - response = json_message(message) - response["data"] = detail_urls - response["url"] = API_URL + "/courses" - return response def post(self): """ This function will create a new course if the body of the post contains a name and uid is an admin or teacher """ - abort_url = API_URL + "/courses" - uid = request.args.get("uid") - abort_if_uid_is_none(uid, abort_url) - - user = abort_if_no_user_found_for_uid(uid, abort_url) - - if not user.is_teacher: - message = ( - "Only teachers or admins can create new courses, you are unauthorized" - ) - return json_message(message), 403 - - data = request.get_json() - - if "name" not in data: - message = "Missing 'name' in the request body" - return json_message(message), 400 - - name = data["name"] - new_course = Course(name=name, teacher=uid) - if "ufora_id" in data: - new_course.ufora_id = data["ufora_id"] - add_abort_if_error(new_course, abort_url) - commit_abort_if_error(abort_url) - - admin_course = CourseAdmin(uid=uid, course_id=new_course.course_id) - add_abort_if_error(admin_course, abort_url) - commit_abort_if_error(abort_url) - - message = (f"Course with name: {name} and" - f"course_id:{new_course.course_id} was succesfully created") - response = json_message(message) - data = { - "course_id": API_URL + "/courses/" + str(new_course.course_id), - "name": new_course.name, - "teacher": API_URL + "/users/" + new_course.teacher, - "ufora_id": new_course.ufora_id if new_course.ufora_id else "None", - } - response["data"] = data - response["url"] = API_URL + "/courses/" + str(new_course.course_id) - return response, 201 + return insert_into_model( + Course, + request.json, + RESPONSE_URL, + "course_id", + required_fields=["name", "teacher"] + ) class CourseByCourseId(Resource): @@ -324,119 +285,76 @@ def get(self, course_id): ] } """ - abort_url = API_URL + "/courses" - uid = request.args.get("uid") - abort_if_uid_is_none(uid, abort_url) - admin = get_admin_relation(uid, course_id) - query = CourseStudent.query.filter_by(uid=uid, course_id=course_id) - student = execute_query_abort_if_db_error(query, abort_url) - - if not (admin or student): - message = "User is not an admin, nor a student of this course" - return json_message(message), 404 - - course = get_course_abort_if_not_found(course_id) - query = Project.query.filter_by(course_id=course_id) - abort_url = API_URL + "/courses/" + str(course_id) - # course does exist so url should be to the id - project_uids = [ - API_URL + "/projects/" + project.project_id - for project in execute_query_abort_if_db_error( - query, abort_url, query_all=True - ) - ] - query = CourseAdmin.query.filter_by(course_id=course_id) - admin_uids = [ - API_URL + "/users/" + admin.uid - for admin in execute_query_abort_if_db_error( - query, abort_url, query_all=True - ) - ] - query = CourseStudent.query.filter_by(course_id=course_id) - student_uids = [ - API_URL + "/users/" + student.uid - for student in execute_query_abort_if_db_error( - query, abort_url, query_all=True - ) - ] - - data = { - "ufora_id": course.ufora_id, - "teacher": API_URL + "/users/" + course.teacher, - "admins": admin_uids, - "students": student_uids, - "projects": project_uids, - } - response = json_message( - "Succesfully retrieved course with course_id: " + str(course_id) - ) - response["data"] = data - response["url"] = API_URL + "/courses/" + str(course_id) - return response + try: + course_details = db.session.query( + Course.course_id, + Course.name, + Course.ufora_id, + Course.teacher + ).filter( + Course.course_id == course_id).first() + + if not course_details: + return { + "message": "Course not found", + "url": RESPONSE_URL + }, 404 + + admins = db.session.query(CourseAdmin.uid).filter( + CourseAdmin.course_id == course_id + ).all() + + students = db.session.query(CourseStudent.uid).filter( + CourseStudent.course_id == course_id + ).all() + + user_url = urljoin(API_URL + "/", "users") + + admin_ids = [ urljoin(user_url + "/" , admin[0]) for admin in admins] + student_ids = [ urljoin(user_url + "/", student[0]) for student in students] + + result = { + 'course_id': course_details.course_id, + 'name': course_details.name, + 'ufora_id': course_details.ufora_id, + 'teacher': course_details.teacher, + 'admins': admin_ids, + 'students': student_ids + } + + return { + "message": "Succesfully retrieved course with course_id: " + str(course_id), + "data": result, + "url": urljoin(RESPONSE_URL + "/", str(course_id)) + } + except SQLAlchemyError: + return { + "error": "Something went wrong while querying the database.", + "url": RESPONSE_URL}, 500 def delete(self, course_id): """ This function will delete the course with course_id """ - abort_url = API_URL + "/courses/" - uid = request.args.get("uid") - abort_if_uid_is_none(uid, abort_url) - - admin = get_admin_relation(uid, course_id) - - if not admin: - message = "You are not an admin of this course and so you cannot delete it" - return json_message(message), 403 - - course = get_course_abort_if_not_found(course_id) - abort_url = API_URL + "/courses/" + str(course_id) - # course does exist so url should be to the id - delete_abort_if_error(course, abort_url) - commit_abort_if_error(abort_url) - - response = { - "message": "Succesfully deleted course with course_id: " + str(course_id), - "url": API_URL + "/courses", - } - return response + return delete_by_id_from_model( + Course, + "course_id", + course_id, + RESPONSE_URL + ) def patch(self, course_id): """ This function will update the course with course_id """ - abort_url = API_URL + "/courses/" - uid = request.args.get("uid") - abort_if_uid_is_none(uid, abort_url) - - admin = get_admin_relation(uid, course_id) - - if not admin: - message = "You are not an admin of this course and so you cannot update it" - return json_message(message), 403 - data = request.get_json() - course = get_course_abort_if_not_found(course_id) - abort_url = API_URL + "/courses/" + str(course_id) - if "name" in data: - course.name = data["name"] - if "teacher" in data: - course.teacher = data["teacher"] - if "ufora_id" in data: - course.ufora_id = data["ufora_id"] - - commit_abort_if_error(abort_url) - response = json_message( - "Succesfully updated course with course_id: " + str(course_id) + return patch_by_id_from_model( + Course, + "course_id", + course_id, + RESPONSE_URL, + request.json ) - response["url"] = API_URL + "/courses/" + str(course_id) - data = { - "course_id": API_URL + "/courses/" + str(course.course_id), - "name": course.name, - "teacher": API_URL + "/users/" + course.teacher, - "ufora_id": course.ufora_id if course.ufora_id else "None", - } - response["data"] = data - return response, 200 class CourseForAdmins(Resource): @@ -461,7 +379,7 @@ def get(self, course_id): "Succesfully retrieved all admins of course " + str(course_id) ) response["data"] = admin_uids - response["url"] = abort_url # not actually aborting here tho heheh + response["url"] = abort_url return jsonify(admin_uids) def post(self, course_id): @@ -608,10 +526,14 @@ def delete(self, course_id): return response -courses_api.add_resource(CourseForUser, "/courses") +courses_bp.add_url_rule("/courses", + view_func=CourseForUser.as_view('course_endpoint')) -courses_api.add_resource(CourseByCourseId, "/courses/") +courses_bp.add_url_rule("/courses/", + view_func=CourseByCourseId.as_view('course_by_course_id')) -courses_api.add_resource(CourseForAdmins, "/courses//admins") +courses_bp.add_url_rule("/courses//admins", + view_func=CourseForAdmins.as_view('course_admins')) -courses_api.add_resource(CourseToAddStudents, "/courses//students") +courses_bp.add_url_rule("/courses//students", + view_func=CourseToAddStudents.as_view('course_students')) From a00cb993fa4a32dd9ed4ae2d9805f1dfae848980 Mon Sep 17 00:00:00 2001 From: abuzogan Date: Wed, 6 Mar 2024 21:55:17 +0100 Subject: [PATCH 17/56] split courses into multiple files to keep it organized --- backend/project/endpoints/courses.py | 539 ------------------ .../courses/course_admin_relation.py | 111 ++++ .../endpoints/courses/course_details.py | 111 ++++ .../courses/course_student_relation.py | 113 ++++ backend/project/endpoints/courses/courses.py | 51 ++ .../endpoints/courses/courses_config.py | 32 ++ .../endpoints/courses/courses_utils.py | 234 ++++++++ 7 files changed, 652 insertions(+), 539 deletions(-) delete mode 100644 backend/project/endpoints/courses.py create mode 100644 backend/project/endpoints/courses/course_admin_relation.py create mode 100644 backend/project/endpoints/courses/course_details.py create mode 100644 backend/project/endpoints/courses/course_student_relation.py create mode 100644 backend/project/endpoints/courses/courses.py create mode 100644 backend/project/endpoints/courses/courses_config.py create mode 100644 backend/project/endpoints/courses/courses_utils.py diff --git a/backend/project/endpoints/courses.py b/backend/project/endpoints/courses.py deleted file mode 100644 index 4954eea0..00000000 --- a/backend/project/endpoints/courses.py +++ /dev/null @@ -1,539 +0,0 @@ -"""Course api point""" - -from os import getenv -from urllib.parse import urljoin - -from dotenv import load_dotenv - -from flask import Blueprint, jsonify, request -from flask import abort -from flask_restful import Api, Resource -from sqlalchemy.exc import SQLAlchemyError -from project.models.course_relations import CourseAdmin, CourseStudent -from project.models.users import User -from project.models.courses import Course -from project.utils.query_agent import query_selected_from_model, \ - insert_into_model,delete_by_id_from_model, \ - patch_by_id_from_model -from project import db - -courses_bp = Blueprint("courses", __name__) -courses_api = Api(courses_bp) - -load_dotenv() -API_URL = getenv("API_HOST") -RESPONSE_URL = urljoin(API_URL + "/", "courses") - -def execute_query_abort_if_db_error(query, url, query_all=False): - """ - Execute the given SQLAlchemy query and handle any SQLAlchemyError that might occur. - If query_all == True, the query will be executed with the all() method, - otherwise with the first() method. - Args: - query (Query): The SQLAlchemy query to execute. - - Returns: - ResultProxy: The result of the query if successful, otherwise aborts with error 500. - """ - try: - if query_all: - result = query.all() - else: - result = query.first() - except SQLAlchemyError as e: - response = json_message(str(e)) - response["url"] = url - abort(500, description=response) - return result - - -def add_abort_if_error(to_add, url): - """ - Add a new object to the database - and handle any SQLAlchemyError that might occur. - - Args: - to_add (object): The object to add to the database. - """ - try: - db.session.add(to_add) - except SQLAlchemyError as e: - db.session.rollback() - response = json_message(str(e)) - response["url"] = url - abort(500, description=response) - - -def delete_abort_if_error(to_delete, url): - """ - Deletes the given object from the database - and aborts the request with a 500 error if a SQLAlchemyError occurs. - - Args: - - to_delete: The object to be deleted from the database. - """ - try: - db.session.delete(to_delete) - except SQLAlchemyError as e: - db.session.rollback() - response = json_message(str(e)) - response["url"] = url - abort(500, description=response) - - -def commit_abort_if_error(url): - """ - Commit the current session and handle any SQLAlchemyError that might occur. - """ - try: - db.session.commit() - except SQLAlchemyError as e: - db.session.rollback() - response = json_message(str(e)) - response["url"] = url - abort(500, description=response) - - -def abort_if_not_teacher_or_none_assistant(course_id, teacher, assistant): - """ - Check if the current user is authorized to appoint new admins to a course. - - Args: - course_id (int): The ID of the course. - - Raises: - HTTPException: If the current user is not authorized or - if the UID of the person to be made an admin is missing in the request body. - """ - url = API_URL + "/courses/" + str(course_id) + "/admins" - abort_if_uid_is_none(teacher, url) - - course = get_course_abort_if_not_found(course_id) - - if teacher != course.teacher: - response = json_message("Only the teacher of a course can appoint new admins") - response["url"] = url - abort(403, description=response) - - if not assistant: - response = json_message( - "uid of person to make admin is required in the request body" - ) - response["url"] = url - abort(400, description=response) - - -def abort_if_none_uid_student_uids_or_non_existant_course_id( - course_id, uid, student_uids -): - """ - Check the request to assign new students to a course. - - Args: - course_id (int): The ID of the course. - - Raises: - 403: If the user is not authorized to assign new students to the course. - 400: If the request body does not contain the required 'students' field. - """ - url = API_URL + "/courses/" + str(course_id) + "/students" - get_course_abort_if_not_found(course_id) - abort_if_no_user_found_for_uid(uid, url) - query = CourseAdmin.query.filter_by(uid=uid, course_id=course_id) - admin_relation = execute_query_abort_if_db_error(query, url) - if not admin_relation: - message = "Not authorized to assign new students to course with id " + str( - course_id - ) - response = json_message(message) - response["url"] = url - abort(403, description=response) - - if not student_uids: - message = """To assign new students to a course, - you should have a students field with a list of uids in the request body""" - response = json_message(message) - response["url"] = url - abort(400, description=response) - - -def abort_if_uid_is_none(uid, url): - """ - Check whether the uid is None if so - abort with error 400 - """ - if uid is None: - response = json_message("There should be a uid in the request query") - response["url"] = url - abort(400, description=response) - - -def abort_if_no_user_found_for_uid(uid, url): - """ - Check if a user exists based on the provided uid. - - Args: - uid (int): The unique identifier of the user. - - Raises: - NotFound: If the user with the given uid is not found. - """ - query = User.query.filter_by(uid=uid) - user = execute_query_abort_if_db_error(query, url) - - if not user: - response = json_message("User with uid " + uid + " was not found") - response["url"] = url - abort(404, description=response) - return user - - -def get_admin_relation(uid, course_id): - """ - Retrieve the CourseAdmin object for the given uid and course. - - Args: - uid (int): The user ID. - course_id (int): The course ID. - - Returns: - CourseAdmin: The CourseAdmin object if the user is an admin, otherwise None. - """ - return execute_query_abort_if_db_error( - CourseAdmin.query.filter_by(uid=uid, course_id=course_id), - url=API_URL + "/courses/" + str(course_id) + "/admins", - ) - - -def json_message(message): - """ - Create a json message with the given message. - - Args: - message (str): The message to include in the json. - - Returns: - dict: The message in a json format. - """ - return {"message": message} - - -def get_course_abort_if_not_found(course_id): - """ - Get a course by its ID. - - Args: - course_id (int): The course ID. - - Returns: - Course: The course with the given ID. - """ - query = Course.query.filter_by(course_id=course_id) - course = execute_query_abort_if_db_error(query, API_URL + "/courses") - - if not course: - response = json_message("Course not found") - response["url"] = API_URL + "/courses" - abort(404, description=response) - - return course - -class CourseForUser(Resource): - """Api endpoint for the /courses link""" - - def get(self): - """ " - 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 - ) - - def post(self): - """ - This function will create a new course - if the body of the post contains a name and uid is an admin or teacher - """ - - return insert_into_model( - Course, - request.json, - RESPONSE_URL, - "course_id", - required_fields=["name", "teacher"] - ) - - -class CourseByCourseId(Resource): - """Api endpoint for the /courses/course_id link""" - - def get(self, course_id): - """ - This get function will return all the related projects of the course - in the following form: - { - course: course with course_id - projects: [ - list of all projects that have course_id - where projects are jsons containing the title, deadline and project_id - ] - } - """ - try: - course_details = db.session.query( - Course.course_id, - Course.name, - Course.ufora_id, - Course.teacher - ).filter( - Course.course_id == course_id).first() - - if not course_details: - return { - "message": "Course not found", - "url": RESPONSE_URL - }, 404 - - admins = db.session.query(CourseAdmin.uid).filter( - CourseAdmin.course_id == course_id - ).all() - - students = db.session.query(CourseStudent.uid).filter( - CourseStudent.course_id == course_id - ).all() - - user_url = urljoin(API_URL + "/", "users") - - admin_ids = [ urljoin(user_url + "/" , admin[0]) for admin in admins] - student_ids = [ urljoin(user_url + "/", student[0]) for student in students] - - result = { - 'course_id': course_details.course_id, - 'name': course_details.name, - 'ufora_id': course_details.ufora_id, - 'teacher': course_details.teacher, - 'admins': admin_ids, - 'students': student_ids - } - - return { - "message": "Succesfully retrieved course with course_id: " + str(course_id), - "data": result, - "url": urljoin(RESPONSE_URL + "/", str(course_id)) - } - except SQLAlchemyError: - return { - "error": "Something went wrong while querying the database.", - "url": RESPONSE_URL}, 500 - - def delete(self, course_id): - """ - This function will delete the course with course_id - """ - return delete_by_id_from_model( - Course, - "course_id", - course_id, - RESPONSE_URL - ) - - def patch(self, course_id): - """ - This function will update the course with course_id - """ - - return patch_by_id_from_model( - Course, - "course_id", - course_id, - RESPONSE_URL, - request.json - ) - - -class CourseForAdmins(Resource): - """ - This class will handle post and delete queries to - the /courses/course_id/admins url, only the teacher of a course can do this - """ - - def get(self, course_id): - """ - This function will return all the admins of a course - """ - abort_url = API_URL + "/courses/" + str(course_id) + "/admins" - get_course_abort_if_not_found(course_id) - - query = CourseAdmin.query.filter_by(course_id=course_id) - admin_uids = [ - API_URL + "/users/" + a.uid - for a in execute_query_abort_if_db_error(query, abort_url, query_all=True) - ] - response = json_message( - "Succesfully retrieved all admins of course " + str(course_id) - ) - response["data"] = admin_uids - response["url"] = abort_url - return jsonify(admin_uids) - - def post(self, course_id): - """ - Api endpoint for adding new admins to a course, can only be done by the teacher - """ - abort_url = API_URL + "/courses/" + str(course_id) + "/admins" - teacher = request.args.get("uid") - data = request.get_json() - assistant = data.get("admin_uid") - abort_if_not_teacher_or_none_assistant(course_id, teacher, assistant) - - query = User.query.filter_by(uid=assistant) - new_admin = execute_query_abort_if_db_error(query, abort_url) - if not new_admin: - message = ( - "User to make admin was not found, please request with a valid uid" - ) - return json_message(message), 404 - - admin_relation = CourseAdmin(uid=assistant, course_id=course_id) - add_abort_if_error(admin_relation, abort_url) - commit_abort_if_error(abort_url) - response = json_message( - f"Admin assistant added to course {course_id}" - ) - response["url"] = abort_url - data = { - "course_id": API_URL + "/courses/" + str(course_id), - "uid": API_URL + "/users/" + assistant, - } - response["data"] = data - return response, 201 - - def delete(self, course_id): - """ - Api endpoint for removing admins of a course, can only be done by the teacher - """ - abort_url = API_URL + "/courses/" + str(course_id) + "/admins" - teacher = request.args.get("uid") - data = request.get_json() - assistant = data.get("admin_uid") - abort_if_not_teacher_or_none_assistant(course_id, teacher, assistant) - - query = CourseAdmin.query.filter_by(uid=assistant, course_id=course_id) - admin_relation = execute_query_abort_if_db_error(query, abort_url) - if not admin_relation: - message = "Course with given admin not found" - return json_message(message), 404 - - delete_abort_if_error(admin_relation, abort_url) - commit_abort_if_error(abort_url) - - message = ( - f"Admin {assistant}" - f" was succesfully removed from course {course_id}" - ) - response = json_message(message) - response["url"] = abort_url - return response, 204 - - -class CourseToAddStudents(Resource): - """ - Class that will respond to the /courses/course_id/students link - teachers should be able to assign and remove students from courses, - and everyone should be able to list all students assigned to a course - """ - - def get(self, course_id): - """ - Get function at /courses/course_id/students - to get all the users assigned to a course - everyone can get this data so no need to have uid query in the link - """ - abort_url = API_URL + "/courses/" + str(course_id) + "/students" - get_course_abort_if_not_found(course_id) - - query = CourseStudent.query.filter_by(course_id=course_id) - student_uids = [ - API_URL + "/users/" + s.uid - for s in execute_query_abort_if_db_error(query, abort_url, query_all=True) - ] - response = json_message( - "Succesfully retrieved all students of course " + str(course_id) - ) - response["data"] = student_uids - response["url"] = abort_url - return response - - def post(self, course_id): - """ - Allows admins of a course to assign new students by posting to: - /courses/course_id/students with a list of uid in the request body under key "students" - """ - abort_url = API_URL + "/courses/" + str(course_id) + "/students" - uid = request.args.get("uid") - data = request.get_json() - student_uids = data.get("students") - abort_if_none_uid_student_uids_or_non_existant_course_id( - course_id, uid, student_uids - ) - - for uid in student_uids: - query = CourseStudent.query.filter_by(uid=uid, course_id=course_id) - student_relation = execute_query_abort_if_db_error(query, abort_url) - if student_relation: - db.session.rollback() - message = ( - "Student with uid " + uid + " is already assigned to the course" - ) - return json_message(message), 400 - add_abort_if_error(CourseStudent(uid=uid, course_id=course_id), abort_url) - commit_abort_if_error(abort_url) - response = json_message("User were succesfully added to the course") - response["url"] = abort_url - data = {"students": [API_URL + "/users/" + uid for uid in student_uids]} - response["data"] = data - return response, 201 - - def delete(self, course_id): - """ - This function allows admins of a course to remove students by sending a delete request to - /courses/course_id/students with inside the request body - a field "students" = [list of uids to unassign] - """ - abort_url = API_URL + "/courses/" + str(course_id) + "/students" - uid = request.args.get("uid") - data = request.get_json() - student_uids = data.get("students") - abort_if_none_uid_student_uids_or_non_existant_course_id( - course_id, uid, student_uids - ) - - for uid in student_uids: - query = CourseStudent.query.filter_by(uid=uid, course_id=course_id) - student_relation = execute_query_abort_if_db_error(query, abort_url) - if student_relation: - delete_abort_if_error(student_relation, abort_url) - commit_abort_if_error(abort_url) - - response = json_message("User were succesfully removed from the course") - response["url"] = API_URL + "/courses/" + str(course_id) + "/students" - return response - - -courses_bp.add_url_rule("/courses", - view_func=CourseForUser.as_view('course_endpoint')) - -courses_bp.add_url_rule("/courses/", - view_func=CourseByCourseId.as_view('course_by_course_id')) - -courses_bp.add_url_rule("/courses//admins", - view_func=CourseForAdmins.as_view('course_admins')) - -courses_bp.add_url_rule("/courses//students", - view_func=CourseToAddStudents.as_view('course_students')) diff --git a/backend/project/endpoints/courses/course_admin_relation.py b/backend/project/endpoints/courses/course_admin_relation.py new file mode 100644 index 00000000..e4bc4b4e --- /dev/null +++ b/backend/project/endpoints/courses/course_admin_relation.py @@ -0,0 +1,111 @@ +""" +This module will handle the /courses//admins endpoint +It will allow the teacher of a course to add and remove admins from a course +""" + +from os import getenv +from urllib.parse import urljoin +from dotenv import load_dotenv + +from flask import jsonify, request +from flask_restful import Resource + +from project.models.course_relations import CourseAdmin +from project.models.users import User +from project.endpoints.courses.courses_utils import ( + execute_query_abort_if_db_error, + add_abort_if_error, + commit_abort_if_error, + delete_abort_if_error, + get_course_abort_if_not_found, + abort_if_not_teacher_or_none_assistant, + json_message +) + +load_dotenv() +API_URL = getenv("API_HOST") +RESPONSE_URL = urljoin(API_URL + "/", "courses") + +class CourseForAdmins(Resource): + """ + This class will handle post and delete queries to + the /courses/course_id/admins url, only the teacher of a course can do this + """ + + def get(self, course_id): + """ + This function will return all the admins of a course + """ + abort_url = API_URL + "/courses/" + str(course_id) + "/admins" + get_course_abort_if_not_found(course_id) + + query = CourseAdmin.query.filter_by(course_id=course_id) + admin_uids = [ + API_URL + "/users/" + a.uid + for a in execute_query_abort_if_db_error(query, abort_url, query_all=True) + ] + response = json_message( + "Succesfully retrieved all admins of course " + str(course_id) + ) + response["data"] = admin_uids + response["url"] = abort_url + return jsonify(admin_uids) + + def post(self, course_id): + """ + Api endpoint for adding new admins to a course, can only be done by the teacher + """ + abort_url = API_URL + "/courses/" + str(course_id) + "/admins" + teacher = request.args.get("uid") + data = request.get_json() + assistant = data.get("admin_uid") + abort_if_not_teacher_or_none_assistant(course_id, teacher, assistant) + + query = User.query.filter_by(uid=assistant) + new_admin = execute_query_abort_if_db_error(query, abort_url) + if not new_admin: + message = ( + "User to make admin was not found, please request with a valid uid" + ) + return json_message(message), 404 + + admin_relation = CourseAdmin(uid=assistant, course_id=course_id) + add_abort_if_error(admin_relation, abort_url) + commit_abort_if_error(abort_url) + response = json_message( + f"Admin assistant added to course {course_id}" + ) + response["url"] = abort_url + data = { + "course_id": API_URL + "/courses/" + str(course_id), + "uid": API_URL + "/users/" + assistant, + } + response["data"] = data + return response, 201 + + def delete(self, course_id): + """ + Api endpoint for removing admins of a course, can only be done by the teacher + """ + abort_url = API_URL + "/courses/" + str(course_id) + "/admins" + teacher = request.args.get("uid") + data = request.get_json() + assistant = data.get("admin_uid") + abort_if_not_teacher_or_none_assistant(course_id, teacher, assistant) + + query = CourseAdmin.query.filter_by(uid=assistant, course_id=course_id) + admin_relation = execute_query_abort_if_db_error(query, abort_url) + if not admin_relation: + message = "Course with given admin not found" + return json_message(message), 404 + + delete_abort_if_error(admin_relation, abort_url) + commit_abort_if_error(abort_url) + + message = ( + f"Admin {assistant}" + f" was succesfully removed from course {course_id}" + ) + response = json_message(message) + response["url"] = abort_url + return response, 204 diff --git a/backend/project/endpoints/courses/course_details.py b/backend/project/endpoints/courses/course_details.py new file mode 100644 index 00000000..d7ac7c37 --- /dev/null +++ b/backend/project/endpoints/courses/course_details.py @@ -0,0 +1,111 @@ +""" +This file contains the api endpoint for the /courses/course_id url +This file is responsible for handling the requests made to the /courses/course_id url +and returning the appropriate response as well as handling the requests made to the +/courses/course_id/admins and /courses/course_id/students urls +""" + +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.courses import Course +from project.models.course_relations import CourseAdmin, CourseStudent + +from project import db +from project.utils.query_agent import delete_by_id_from_model, patch_by_id_from_model + +load_dotenv() +API_URL = getenv("API_HOST") +RESPONSE_URL = urljoin(API_URL + "/", "courses") + +class CourseByCourseId(Resource): + """Api endpoint for the /courses/course_id link""" + + def get(self, course_id): + """ + This get function will return all the related projects of the course + in the following form: + { + course: course with course_id + projects: [ + list of all projects that have course_id + where projects are jsons containing the title, deadline and project_id + ] + } + """ + try: + course_details = db.session.query( + Course.course_id, + Course.name, + Course.ufora_id, + Course.teacher + ).filter( + Course.course_id == course_id).first() + + if not course_details: + return { + "message": "Course not found", + "url": RESPONSE_URL + }, 404 + + admins = db.session.query(CourseAdmin.uid).filter( + CourseAdmin.course_id == course_id + ).all() + + students = db.session.query(CourseStudent.uid).filter( + CourseStudent.course_id == course_id + ).all() + + user_url = urljoin(API_URL + "/", "users") + + admin_ids = [ urljoin(user_url + "/" , admin[0]) for admin in admins] + student_ids = [ urljoin(user_url + "/", student[0]) for student in students] + + result = { + 'course_id': course_details.course_id, + 'name': course_details.name, + 'ufora_id': course_details.ufora_id, + 'teacher': course_details.teacher, + 'admins': admin_ids, + 'students': student_ids + } + + return { + "message": "Succesfully retrieved course with course_id: " + str(course_id), + "data": result, + "url": urljoin(RESPONSE_URL + "/", str(course_id)) + } + except SQLAlchemyError: + return { + "error": "Something went wrong while querying the database.", + "url": RESPONSE_URL}, 500 + + def delete(self, course_id): + """ + This function will delete the course with course_id + """ + return delete_by_id_from_model( + Course, + "course_id", + course_id, + RESPONSE_URL + ) + + def patch(self, course_id): + """ + This function will update the course with course_id + """ + + return patch_by_id_from_model( + Course, + "course_id", + course_id, + RESPONSE_URL, + request.json + ) diff --git a/backend/project/endpoints/courses/course_student_relation.py b/backend/project/endpoints/courses/course_student_relation.py new file mode 100644 index 00000000..3958422f --- /dev/null +++ b/backend/project/endpoints/courses/course_student_relation.py @@ -0,0 +1,113 @@ +""" +This file contains the class CourseToAddStudents which is a +resource for the /courses/course_id/students link. +This class will allow admins of a course to assign and remove students from courses, +and everyone should be able to list all students assigned to a course. +""" + +from os import getenv +from urllib.parse import urljoin + +from dotenv import load_dotenv + +from flask import request +from flask_restful import Resource + +from project import db +from project.models.course_relations import CourseStudent +from project.endpoints.courses.courses_utils import ( + execute_query_abort_if_db_error, + add_abort_if_error, + commit_abort_if_error, + delete_abort_if_error, + get_course_abort_if_not_found, + abort_if_none_uid_student_uids_or_non_existant_course_id, + json_message, +) + +load_dotenv() +API_URL = getenv("API_HOST") +RESPONSE_URL = urljoin(API_URL + "/", "courses") + +class CourseToAddStudents(Resource): + """ + Class that will respond to the /courses/course_id/students link + teachers should be able to assign and remove students from courses, + and everyone should be able to list all students assigned to a course + """ + + def get(self, course_id): + """ + Get function at /courses/course_id/students + to get all the users assigned to a course + everyone can get this data so no need to have uid query in the link + """ + abort_url = API_URL + "/courses/" + str(course_id) + "/students" + get_course_abort_if_not_found(course_id) + + query = CourseStudent.query.filter_by(course_id=course_id) + student_uids = [ + API_URL + "/users/" + s.uid + for s in execute_query_abort_if_db_error(query, abort_url, query_all=True) + ] + response = json_message( + "Succesfully retrieved all students of course " + str(course_id) + ) + response["data"] = student_uids + response["url"] = abort_url + return response + + def post(self, course_id): + """ + Allows admins of a course to assign new students by posting to: + /courses/course_id/students with a list of uid in the request body under key "students" + """ + abort_url = API_URL + "/courses/" + str(course_id) + "/students" + uid = request.args.get("uid") + data = request.get_json() + student_uids = data.get("students") + abort_if_none_uid_student_uids_or_non_existant_course_id( + course_id, uid, student_uids + ) + + for uid in student_uids: + query = CourseStudent.query.filter_by(uid=uid, course_id=course_id) + student_relation = execute_query_abort_if_db_error(query, abort_url) + if student_relation: + db.session.rollback() + message = ( + "Student with uid " + uid + " is already assigned to the course" + ) + return json_message(message), 400 + add_abort_if_error(CourseStudent(uid=uid, course_id=course_id), abort_url) + commit_abort_if_error(abort_url) + response = json_message("User were succesfully added to the course") + response["url"] = abort_url + data = {"students": [API_URL + "/users/" + uid for uid in student_uids]} + response["data"] = data + return response, 201 + + def delete(self, course_id): + """ + This function allows admins of a course to remove students by sending a delete request to + /courses/course_id/students with inside the request body + a field "students" = [list of uids to unassign] + """ + abort_url = API_URL + "/courses/" + str(course_id) + "/students" + uid = request.args.get("uid") + data = request.get_json() + student_uids = data.get("students") + abort_if_none_uid_student_uids_or_non_existant_course_id( + course_id, uid, student_uids + ) + + for uid in student_uids: + query = CourseStudent.query.filter_by(uid=uid, course_id=course_id) + student_relation = execute_query_abort_if_db_error(query, abort_url) + if student_relation: + delete_abort_if_error(student_relation, abort_url) + commit_abort_if_error(abort_url) + + response = json_message("User were succesfully removed from the course") + response["url"] = API_URL + "/courses/" + str(course_id) + "/students" + return response diff --git a/backend/project/endpoints/courses/courses.py b/backend/project/endpoints/courses/courses.py new file mode 100644 index 00000000..c06d7dfc --- /dev/null +++ b/backend/project/endpoints/courses/courses.py @@ -0,0 +1,51 @@ +""" +This file contains the main endpoint for the /courses url. +This endpoint is used 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. +""" + +from os import getenv +from urllib.parse import urljoin +from dotenv import load_dotenv + +from flask import request +from flask_restful import Resource + +from project.models.courses import Course +from project.utils.query_agent import query_selected_from_model, insert_into_model + +load_dotenv() +API_URL = getenv("API_HOST") +RESPONSE_URL = urljoin(API_URL + "/", "courses") + +class CourseForUser(Resource): + """Api endpoint for the /courses link""" + + def get(self): + """ " + 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 + ) + + def post(self): + """ + This function will create a new course + if the body of the post contains a name and uid is an admin or teacher + """ + + return insert_into_model( + Course, + request.json, + RESPONSE_URL, + "course_id", + required_fields=["name", "teacher"] + ) diff --git a/backend/project/endpoints/courses/courses_config.py b/backend/project/endpoints/courses/courses_config.py new file mode 100644 index 00000000..f791031f --- /dev/null +++ b/backend/project/endpoints/courses/courses_config.py @@ -0,0 +1,32 @@ +""" +This file is used to configure the courses blueprint and the courses api. +It is used to define the routes for the courses blueprint and the +corresponding api endpoints. + +The courses blueprint is used to define the routes for the courses api +endpoints and the courses api is used to define the routes for the courses +api endpoints. +""" + +from flask import Blueprint +from flask_restful import Api + +from project.endpoints.courses.courses import CourseForUser +from project.endpoints.courses.course_details import CourseByCourseId +from project.endpoints.courses.course_admin_relation import CourseForAdmins +from project.endpoints.courses.course_student_relation import CourseToAddStudents + +courses_bp = Blueprint("courses", __name__) +courses_api = Api(courses_bp) + +courses_bp.add_url_rule("/courses", + view_func=CourseForUser.as_view('course_endpoint')) + +courses_bp.add_url_rule("/courses/", + view_func=CourseByCourseId.as_view('course_by_course_id')) + +courses_bp.add_url_rule("/courses//admins", + view_func=CourseForAdmins.as_view('course_admins')) + +courses_bp.add_url_rule("/courses//students", + view_func=CourseToAddStudents.as_view('course_students')) diff --git a/backend/project/endpoints/courses/courses_utils.py b/backend/project/endpoints/courses/courses_utils.py new file mode 100644 index 00000000..5543db33 --- /dev/null +++ b/backend/project/endpoints/courses/courses_utils.py @@ -0,0 +1,234 @@ +""" +This module contains utility functions for the courses endpoints. +The functions are used to interact with the database and handle errors. +""" + +from os import getenv +from urllib.parse import urljoin + +from dotenv import load_dotenv +from flask import abort +from sqlalchemy.exc import SQLAlchemyError + +from project import db +from project.models.course_relations import CourseAdmin +from project.models.users import User +from project.models.courses import Course + +load_dotenv() +API_URL = getenv("API_HOST") +RESPONSE_URL = urljoin(API_URL + "/", "courses") + +def execute_query_abort_if_db_error(query, url, query_all=False): + """ + Execute the given SQLAlchemy query and handle any SQLAlchemyError that might occur. + If query_all == True, the query will be executed with the all() method, + otherwise with the first() method. + Args: + query (Query): The SQLAlchemy query to execute. + + Returns: + ResultProxy: The result of the query if successful, otherwise aborts with error 500. + """ + try: + if query_all: + result = query.all() + else: + result = query.first() + except SQLAlchemyError as e: + response = json_message(str(e)) + response["url"] = url + abort(500, description=response) + return result + + +def add_abort_if_error(to_add, url): + """ + Add a new object to the database + and handle any SQLAlchemyError that might occur. + + Args: + to_add (object): The object to add to the database. + """ + try: + db.session.add(to_add) + except SQLAlchemyError as e: + db.session.rollback() + response = json_message(str(e)) + response["url"] = url + abort(500, description=response) + + +def delete_abort_if_error(to_delete, url): + """ + Deletes the given object from the database + and aborts the request with a 500 error if a SQLAlchemyError occurs. + + Args: + - to_delete: The object to be deleted from the database. + """ + try: + db.session.delete(to_delete) + except SQLAlchemyError as e: + db.session.rollback() + response = json_message(str(e)) + response["url"] = url + abort(500, description=response) + + +def commit_abort_if_error(url): + """ + Commit the current session and handle any SQLAlchemyError that might occur. + """ + try: + db.session.commit() + except SQLAlchemyError as e: + db.session.rollback() + response = json_message(str(e)) + response["url"] = url + abort(500, description=response) + + +def abort_if_not_teacher_or_none_assistant(course_id, teacher, assistant): + """ + Check if the current user is authorized to appoint new admins to a course. + + Args: + course_id (int): The ID of the course. + + Raises: + HTTPException: If the current user is not authorized or + if the UID of the person to be made an admin is missing in the request body. + """ + url = API_URL + "/courses/" + str(course_id) + "/admins" + abort_if_uid_is_none(teacher, url) + + course = get_course_abort_if_not_found(course_id) + + if teacher != course.teacher: + response = json_message("Only the teacher of a course can appoint new admins") + response["url"] = url + abort(403, description=response) + + if not assistant: + response = json_message( + "uid of person to make admin is required in the request body" + ) + response["url"] = url + abort(400, description=response) + + +def abort_if_none_uid_student_uids_or_non_existant_course_id( + course_id, uid, student_uids +): + """ + Check the request to assign new students to a course. + + Args: + course_id (int): The ID of the course. + + Raises: + 403: If the user is not authorized to assign new students to the course. + 400: If the request body does not contain the required 'students' field. + """ + url = API_URL + "/courses/" + str(course_id) + "/students" + get_course_abort_if_not_found(course_id) + abort_if_no_user_found_for_uid(uid, url) + query = CourseAdmin.query.filter_by(uid=uid, course_id=course_id) + admin_relation = execute_query_abort_if_db_error(query, url) + if not admin_relation: + message = "Not authorized to assign new students to course with id " + str( + course_id + ) + response = json_message(message) + response["url"] = url + abort(403, description=response) + + if not student_uids: + message = """To assign new students to a course, + you should have a students field with a list of uids in the request body""" + response = json_message(message) + response["url"] = url + abort(400, description=response) + + +def abort_if_uid_is_none(uid, url): + """ + Check whether the uid is None if so + abort with error 400 + """ + if uid is None: + response = json_message("There should be a uid in the request query") + response["url"] = url + abort(400, description=response) + + +def abort_if_no_user_found_for_uid(uid, url): + """ + Check if a user exists based on the provided uid. + + Args: + uid (int): The unique identifier of the user. + + Raises: + NotFound: If the user with the given uid is not found. + """ + query = User.query.filter_by(uid=uid) + user = execute_query_abort_if_db_error(query, url) + + if not user: + response = json_message("User with uid " + uid + " was not found") + response["url"] = url + abort(404, description=response) + return user + + +def get_admin_relation(uid, course_id): + """ + Retrieve the CourseAdmin object for the given uid and course. + + Args: + uid (int): The user ID. + course_id (int): The course ID. + + Returns: + CourseAdmin: The CourseAdmin object if the user is an admin, otherwise None. + """ + return execute_query_abort_if_db_error( + CourseAdmin.query.filter_by(uid=uid, course_id=course_id), + url=API_URL + "/courses/" + str(course_id) + "/admins", + ) + + +def json_message(message): + """ + Create a json message with the given message. + + Args: + message (str): The message to include in the json. + + Returns: + dict: The message in a json format. + """ + return {"message": message} + + +def get_course_abort_if_not_found(course_id): + """ + Get a course by its ID. + + Args: + course_id (int): The course ID. + + Returns: + Course: The course with the given ID. + """ + query = Course.query.filter_by(course_id=course_id) + course = execute_query_abort_if_db_error(query, API_URL + "/courses") + + if not course: + response = json_message("Course not found") + response["url"] = API_URL + "/courses" + abort(404, description=response) + + return course From f4aff02310b1bff882eb26b0ecfedb90c8a4d249 Mon Sep 17 00:00:00 2001 From: abuzogan Date: Wed, 6 Mar 2024 21:55:56 +0100 Subject: [PATCH 18/56] fixed linting --- backend/project/utils/misc.py | 14 +++++++++++--- backend/project/utils/query_agent.py | 13 +++++++------ 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/backend/project/utils/misc.py b/backend/project/utils/misc.py index 9d313467..7fe39a8c 100644 --- a/backend/project/utils/misc.py +++ b/backend/project/utils/misc.py @@ -22,8 +22,6 @@ def map_keys_to_url(url_mapper: Dict[str, str], data: Dict[str, str]) -> Dict[st for key, value in data.items(): if key in url_mapper: data[key] = urljoin(url_mapper[key] + "/", str(value)) - print(url_mapper) - print(data) return data def map_all_keys_to_url(url_mapper: Dict[str, str], data: List[Dict[str, str]]): @@ -65,4 +63,14 @@ def models_to_dict(instances: List[DeclarativeMeta]) -> List[Dict[str, str]]: def filter_model_fields(model: DeclarativeMeta, data: Dict[str, str]): - return {key: value for key, value in data.items() if hasattr(model, key)} \ No newline at end of file + """ + Filters the data to only contain the fields of the model. + + Args: + model: DeclarativeMeta - The model to filter the data with. + data: Dict[str, str] - The data to filter. + + Returns: + A dictionary with the fields of the model. + """ + return {key: value for key, value in data.items() if hasattr(model, key)} diff --git a/backend/project/utils/query_agent.py b/backend/project/utils/query_agent.py index d0595afd..bbbcf118 100644 --- a/backend/project/utils/query_agent.py +++ b/backend/project/utils/query_agent.py @@ -7,7 +7,7 @@ from typing import Dict, List, Union from urllib.parse import urljoin from flask import jsonify -from sqlalchemy import and_, inspect +from sqlalchemy import and_ from sqlalchemy.ext.declarative import DeclarativeMeta from sqlalchemy.orm.query import Query from sqlalchemy.exc import SQLAlchemyError @@ -54,7 +54,7 @@ def insert_into_model(model: DeclarativeMeta, data: Dict[str, Union[str, int]], response_url_base: str, url_id_field: str, - required_fields: List[str] = []): + required_fields: List[str] = None): """ Inserts a new entry into the database giving the model corresponding to a certain table and the data to insert. @@ -69,13 +69,15 @@ def insert_into_model(model: DeclarativeMeta, a message indicating that something went wrong while inserting into the database. """ try: + if required_fields is None: + required_fields = [] # Check if all non-nullable fields are present in the data missing_fields = [field for field in required_fields if field not in data] - + if missing_fields: return {"error": f"Missing required fields: {', '.join(missing_fields)}", "url": response_url_base}, 400 - + filtered_data = filter_model_fields(model, data) new_instance: DeclarativeMeta = model(**filtered_data) db.session.add(new_instance) @@ -84,7 +86,7 @@ def insert_into_model(model: DeclarativeMeta, "data": new_instance, "message": "Object created succesfully.", "url": urljoin(response_url_base, str(getattr(new_instance, url_id_field)))}), 201 - except SQLAlchemyError as e: + except SQLAlchemyError: return jsonify({"error": "Something went wrong while inserting into the database.", "url": response_url_base}), 500 @@ -162,7 +164,6 @@ def query_by_id_from_model(model: DeclarativeMeta, result: Query = model.query.filter(getattr(model, column_name) == column_id).first() if not result: return {"message": "Resource not found", "url": base_url}, 404 - print(column_id) return jsonify({ "data": result, "message": "Resource fetched correctly", From 4e0945f7ee3d49ba912dc88ae9c221a4f31d1a63 Mon Sep 17 00:00:00 2001 From: abuzogan Date: Wed, 6 Mar 2024 21:57:15 +0100 Subject: [PATCH 19/56] added new courses blueprint --- backend/project/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/project/__init__.py b/backend/project/__init__.py index 33450700..b0c21275 100644 --- a/backend/project/__init__.py +++ b/backend/project/__init__.py @@ -6,8 +6,7 @@ from .db_in import db from .endpoints.index.index import index_bp from .endpoints.projects.project_endpoint import project_bp -from .endpoints.courses import courses_bp - +from .endpoints.courses.courses_config import courses_bp def create_app(): From 05038c97d3f57d3cbb8e85cac92d37d1c8feb433 Mon Sep 17 00:00:00 2001 From: abuzogan Date: Wed, 6 Mar 2024 22:14:52 +0100 Subject: [PATCH 20/56] removed trailing space --- backend/tests/endpoints/courses_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/tests/endpoints/courses_test.py b/backend/tests/endpoints/courses_test.py index 9beb64fe..0478007b 100644 --- a/backend/tests/endpoints/courses_test.py +++ b/backend/tests/endpoints/courses_test.py @@ -74,7 +74,7 @@ def test_post_courses_course_id_students_and_admins(self, db_with_course, client assert response.status_code == 400 sel2_admins_link = "/courses/" + str(course.course_id) + "/admins" - + course_admins = [ s.uid for s in CourseAdmin.query.filter_by(course_id=course.course_id).all() From a142a88d037dec1b355cb49f165bde6e22a1d4c7 Mon Sep 17 00:00:00 2001 From: abuzogan Date: Thu, 7 Mar 2024 09:24:24 +0100 Subject: [PATCH 21/56] changed test to stay consistent with course admin relation also --- backend/tests/endpoints/courses_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/tests/endpoints/courses_test.py b/backend/tests/endpoints/courses_test.py index 0478007b..ac299f01 100644 --- a/backend/tests/endpoints/courses_test.py +++ b/backend/tests/endpoints/courses_test.py @@ -113,7 +113,7 @@ def test_get_courses(self, courses_get_db, client, api_url): assert response.status_code == 200 sel2_students = [ - f"{api_url}/users/" + s.uid + {"uid": f"{api_url}/users/" + s.uid} for s in CourseStudent.query.filter_by(course_id=course.course_id).all() ] From 1f11956b43536811fa59dd6a658c0c00bc05a998 Mon Sep 17 00:00:00 2001 From: abuzogan Date: Thu, 7 Mar 2024 09:25:16 +0100 Subject: [PATCH 22/56] added query agent functions to prevent code duplication --- .../courses/course_admin_relation.py | 37 +++++++------------ .../courses/course_student_relation.py | 18 ++++----- 2 files changed, 21 insertions(+), 34 deletions(-) diff --git a/backend/project/endpoints/courses/course_admin_relation.py b/backend/project/endpoints/courses/course_admin_relation.py index e4bc4b4e..229694f7 100644 --- a/backend/project/endpoints/courses/course_admin_relation.py +++ b/backend/project/endpoints/courses/course_admin_relation.py @@ -7,20 +7,20 @@ from urllib.parse import urljoin from dotenv import load_dotenv -from flask import jsonify, request +from flask import request from flask_restful import Resource from project.models.course_relations import CourseAdmin from project.models.users import User from project.endpoints.courses.courses_utils import ( execute_query_abort_if_db_error, - add_abort_if_error, commit_abort_if_error, delete_abort_if_error, get_course_abort_if_not_found, abort_if_not_teacher_or_none_assistant, json_message ) +from project.utils.query_agent import query_selected_from_model, insert_into_model load_dotenv() API_URL = getenv("API_HOST") @@ -39,17 +39,13 @@ def get(self, course_id): abort_url = API_URL + "/courses/" + str(course_id) + "/admins" get_course_abort_if_not_found(course_id) - query = CourseAdmin.query.filter_by(course_id=course_id) - admin_uids = [ - API_URL + "/users/" + a.uid - for a in execute_query_abort_if_db_error(query, abort_url, query_all=True) - ] - response = json_message( - "Succesfully retrieved all admins of course " + str(course_id) + return query_selected_from_model( + CourseAdmin, + abort_url, + select_values=["uid"], + url_mapper={"uid": urljoin(API_URL + "/", "users")}, + filters={"course_id": course_id}, ) - response["data"] = admin_uids - response["url"] = abort_url - return jsonify(admin_uids) def post(self, course_id): """ @@ -69,19 +65,12 @@ def post(self, course_id): ) return json_message(message), 404 - admin_relation = CourseAdmin(uid=assistant, course_id=course_id) - add_abort_if_error(admin_relation, abort_url) - commit_abort_if_error(abort_url) - response = json_message( - f"Admin assistant added to course {course_id}" + return insert_into_model( + CourseAdmin, + {"uid": assistant, "course_id": course_id}, + abort_url, + "uid" ) - response["url"] = abort_url - data = { - "course_id": API_URL + "/courses/" + str(course_id), - "uid": API_URL + "/users/" + assistant, - } - response["data"] = data - return response, 201 def delete(self, course_id): """ diff --git a/backend/project/endpoints/courses/course_student_relation.py b/backend/project/endpoints/courses/course_student_relation.py index 3958422f..a5488e47 100644 --- a/backend/project/endpoints/courses/course_student_relation.py +++ b/backend/project/endpoints/courses/course_student_relation.py @@ -25,6 +25,8 @@ json_message, ) +from project.utils.query_agent import query_selected_from_model + load_dotenv() API_URL = getenv("API_HOST") RESPONSE_URL = urljoin(API_URL + "/", "courses") @@ -45,17 +47,13 @@ def get(self, course_id): abort_url = API_URL + "/courses/" + str(course_id) + "/students" get_course_abort_if_not_found(course_id) - query = CourseStudent.query.filter_by(course_id=course_id) - student_uids = [ - API_URL + "/users/" + s.uid - for s in execute_query_abort_if_db_error(query, abort_url, query_all=True) - ] - response = json_message( - "Succesfully retrieved all students of course " + str(course_id) + return query_selected_from_model( + CourseStudent, + abort_url, + select_values=["uid"], + url_mapper={"uid": urljoin(API_URL + "/", "users")}, + filters={"course_id": course_id} ) - response["data"] = student_uids - response["url"] = abort_url - return response def post(self, course_id): """ From 7fad876efea2652296f831e3e1561e623952c679 Mon Sep 17 00:00:00 2001 From: gerwoud Date: Thu, 7 Mar 2024 10:27:10 +0100 Subject: [PATCH 23/56] first version of file uploads --- backend/project/__main__.py | 2 ++ backend/project/endpoints/projects/endpoint_parser.py | 4 +++- backend/project/endpoints/projects/project_endpoint.py | 1 - backend/project/endpoints/projects/projects.py | 10 +++++++++- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/backend/project/__main__.py b/backend/project/__main__.py index a4bd122b..32547c6e 100644 --- a/backend/project/__main__.py +++ b/backend/project/__main__.py @@ -1,5 +1,7 @@ """Main entry point for the application.""" +from sys import path +path.append(".") from dotenv import load_dotenv from project import create_app_with_db from project.db_in import url diff --git a/backend/project/endpoints/projects/endpoint_parser.py b/backend/project/endpoints/projects/endpoint_parser.py index 87f61e69..99452929 100644 --- a/backend/project/endpoints/projects/endpoint_parser.py +++ b/backend/project/endpoints/projects/endpoint_parser.py @@ -3,11 +3,12 @@ """ from flask_restful import reqparse +import werkzeug parser = reqparse.RequestParser() parser.add_argument('title', type=str, help='Projects title') parser.add_argument('descriptions', type=str, help='Projects description') -parser.add_argument('assignment_file', type=str, help='Projects assignment file') +parser.add_argument('assignment_file', type=werkzeug.datastructures.FileStorage, help='Projects assignment file', location="form") parser.add_argument("deadline", type=str, help='Projects deadline') parser.add_argument("course_id", type=str, help='Projects course_id') parser.add_argument("visible_for_students", type=bool, help='Projects visibility for students') @@ -23,6 +24,7 @@ def parse_project_params(): """ args = parser.parse_args() result_dict = {} + print(args) for key, value in args.items(): if value is not None: diff --git a/backend/project/endpoints/projects/project_endpoint.py b/backend/project/endpoints/projects/project_endpoint.py index c996a514..eef5b34d 100644 --- a/backend/project/endpoints/projects/project_endpoint.py +++ b/backend/project/endpoints/projects/project_endpoint.py @@ -10,7 +10,6 @@ from project.endpoints.projects.project_detail import ProjectDetail project_bp = Blueprint('project_endpoint', __name__) -project_endpoint = Api(project_bp) project_bp.add_url_rule( '/projects', diff --git a/backend/project/endpoints/projects/projects.py b/backend/project/endpoints/projects/projects.py index 68600034..a72957ba 100644 --- a/backend/project/endpoints/projects/projects.py +++ b/backend/project/endpoints/projects/projects.py @@ -1,6 +1,7 @@ """ Module that implements the /projects endpoint of the API """ +import os from os import getenv from urllib.parse import urljoin @@ -11,6 +12,8 @@ from project.utils.query_agent import query_selected_from_model, insert_into_model API_URL = getenv('API_HOST') +UPLOAD_FOLDER = '/project/endpoints/uploads/' +ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif'} class ProjectsEndpoint(Resource): """ @@ -40,4 +43,9 @@ def post(self): using flask_restfull parse lib """ - return insert_into_model(Project, request.json, urljoin(API_URL, "/projects")) + file = request.files["assignment_file"] + + # save the file that is given with the request + file.save("."+os.path.join(UPLOAD_FOLDER, file.filename)) + # return insert_into_model(Project, request.json, urljoin(API_URL, "/projects")) + return {}, 200 From ca42936bb517e64a7bd14a1c392260faa5a0d719 Mon Sep 17 00:00:00 2001 From: gerwoud Date: Thu, 7 Mar 2024 12:34:31 +0100 Subject: [PATCH 24/56] formatting json for posting in the db --- .../project/endpoints/projects/projects.py | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/backend/project/endpoints/projects/projects.py b/backend/project/endpoints/projects/projects.py index a72957ba..ab3a513a 100644 --- a/backend/project/endpoints/projects/projects.py +++ b/backend/project/endpoints/projects/projects.py @@ -12,8 +12,12 @@ from project.utils.query_agent import query_selected_from_model, insert_into_model API_URL = getenv('API_HOST') -UPLOAD_FOLDER = '/project/endpoints/uploads/' -ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif'} +UPLOAD_FOLDER = getenv('UPLOAD_URL') +ALLOWED_EXTENSIONS = {'zip'} + +def allowed_file(filename: str): + return '.' in filename and \ + filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS class ProjectsEndpoint(Resource): """ @@ -46,6 +50,18 @@ def post(self): file = request.files["assignment_file"] # save the file that is given with the request - file.save("."+os.path.join(UPLOAD_FOLDER, file.filename)) + if allowed_file(file.filename): + file.save("."+os.path.join(UPLOAD_FOLDER, file.filename)) + else: + print("no zip file given") # return insert_into_model(Project, request.json, urljoin(API_URL, "/projects")) - return {}, 200 + print(request.form) + project_json = {} + for key, value in request.form.items(): + print("key: {}, value: {}".format(key, value)) + project_json[key] = value + print(project_json) + new_project = insert_into_model(Project, project_json, urljoin(API_URL, "/projects")) + print(new_project) + + return new_project From 8d75ae6c05269d2fca1353154f7c291d17a0f665 Mon Sep 17 00:00:00 2001 From: gerwoud Date: Thu, 7 Mar 2024 13:01:48 +0100 Subject: [PATCH 25/56] working file upload system, reused parser --- .../endpoints/projects/endpoint_parser.py | 18 ++++++------ .../project/endpoints/projects/projects.py | 28 +++++++++++++------ backend/project/utils/query_agent.py | 4 ++- 3 files changed, 31 insertions(+), 19 deletions(-) diff --git a/backend/project/endpoints/projects/endpoint_parser.py b/backend/project/endpoints/projects/endpoint_parser.py index 99452929..d5ece633 100644 --- a/backend/project/endpoints/projects/endpoint_parser.py +++ b/backend/project/endpoints/projects/endpoint_parser.py @@ -6,16 +6,16 @@ import werkzeug parser = reqparse.RequestParser() -parser.add_argument('title', type=str, help='Projects title') -parser.add_argument('descriptions', type=str, help='Projects description') +parser.add_argument('title', type=str, help='Projects title', location="form") +parser.add_argument('descriptions', type=str, help='Projects description', location="form") parser.add_argument('assignment_file', type=werkzeug.datastructures.FileStorage, help='Projects assignment file', location="form") -parser.add_argument("deadline", type=str, help='Projects deadline') -parser.add_argument("course_id", type=str, help='Projects course_id') -parser.add_argument("visible_for_students", type=bool, help='Projects visibility for students') -parser.add_argument("archieved", type=bool, help='Projects') -parser.add_argument("test_path", type=str, help='Projects test path') -parser.add_argument("script_name", type=str, help='Projects test script path') -parser.add_argument("regex_expressions", type=str, help='Projects regex expressions') +parser.add_argument("deadline", type=str, help='Projects deadline', location="form") +parser.add_argument("course_id", type=str, help='Projects course_id', location="form") +parser.add_argument("visible_for_students", type=bool, help='Projects visibility for students', location="form") +parser.add_argument("archieved", type=bool, help='Projects', location="form") +parser.add_argument("test_path", type=str, help='Projects test path', location="form") +parser.add_argument("script_name", type=str, help='Projects test script path', location="form") +parser.add_argument("regex_expressions", type=str, help='Projects regex expressions', location="form") def parse_project_params(): diff --git a/backend/project/endpoints/projects/projects.py b/backend/project/endpoints/projects/projects.py index ab3a513a..af303cc9 100644 --- a/backend/project/endpoints/projects/projects.py +++ b/backend/project/endpoints/projects/projects.py @@ -11,10 +11,24 @@ from project.models.projects import Project from project.utils.query_agent import query_selected_from_model, insert_into_model +from project.endpoints.projects.endpoint_parser import parse_project_params + API_URL = getenv('API_HOST') UPLOAD_FOLDER = getenv('UPLOAD_URL') ALLOWED_EXTENSIONS = {'zip'} +def parse_immutabledict(request): + output_json = {} + for key, value in request.form.items(): + if value == "false": + print("false") + output_json[key] = False + if value == "true": + output_json[key] = True + else: + output_json[key] = value + return output_json + def allowed_file(filename: str): return '.' in filename and \ filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS @@ -48,20 +62,16 @@ def post(self): """ file = request.files["assignment_file"] + project_json = parse_project_params() + print("args") + print(arg) # save the file that is given with the request if allowed_file(file.filename): file.save("."+os.path.join(UPLOAD_FOLDER, file.filename)) else: print("no zip file given") - # return insert_into_model(Project, request.json, urljoin(API_URL, "/projects")) - print(request.form) - project_json = {} - for key, value in request.form.items(): - print("key: {}, value: {}".format(key, value)) - project_json[key] = value - print(project_json) - new_project = insert_into_model(Project, project_json, urljoin(API_URL, "/projects")) - print(new_project) + + new_project = insert_into_model(Project, project_json, urljoin(API_URL, "/projects"), "project_id", required_fields=["title", "descriptions", "course_id", "visible_for_students", "archieved"]) return new_project diff --git a/backend/project/utils/query_agent.py b/backend/project/utils/query_agent.py index bbbcf118..24e857e2 100644 --- a/backend/project/utils/query_agent.py +++ b/backend/project/utils/query_agent.py @@ -86,7 +86,9 @@ def insert_into_model(model: DeclarativeMeta, "data": new_instance, "message": "Object created succesfully.", "url": urljoin(response_url_base, str(getattr(new_instance, url_id_field)))}), 201 - except SQLAlchemyError: + except SQLAlchemyError as e: + print("error") + print(e) return jsonify({"error": "Something went wrong while inserting into the database.", "url": response_url_base}), 500 From 11377d16f83ce2e68d034de3530a9e281450032c Mon Sep 17 00:00:00 2001 From: gerwoud Date: Mon, 11 Mar 2024 13:41:44 +0100 Subject: [PATCH 26/56] fixed extracting of zip and uploading in project upload directory --- .../project/endpoints/projects/projects.py | 32 +++++++++++--- backend/project/utils/query_agent.py | 44 ++++++++++++------- 2 files changed, 52 insertions(+), 24 deletions(-) diff --git a/backend/project/endpoints/projects/projects.py b/backend/project/endpoints/projects/projects.py index af303cc9..03572a30 100644 --- a/backend/project/endpoints/projects/projects.py +++ b/backend/project/endpoints/projects/projects.py @@ -8,8 +8,10 @@ from flask import request from flask_restful import Resource +import zipfile + from project.models.projects import Project -from project.utils.query_agent import query_selected_from_model, insert_into_model +from project.utils.query_agent import query_selected_from_model, insert_into_model, create_model_instance from project.endpoints.projects.endpoint_parser import parse_project_params @@ -63,15 +65,31 @@ def post(self): file = request.files["assignment_file"] project_json = parse_project_params() - print("args") - print(arg) # save the file that is given with the request + + new_new_project = create_model_instance( + Project, + project_json, + urljoin(API_URL, "/projects"), + required_fields=["title", "descriptions", "course_id", "visible_for_students", "archieved"] + ) + + print(new_new_project) + id = new_new_project.project_id + print(id) + project_upload_directory = f"{UPLOAD_FOLDER}{new_new_project.project_id}" + file_location = "."+os.path.join(project_upload_directory) + print(file_location) + # print(new_new_project.json) + if not os.path.exists(project_upload_directory): + os.makedirs(file_location) + if allowed_file(file.filename): - file.save("."+os.path.join(UPLOAD_FOLDER, file.filename)) + file.save(file_location+"/"+file.filename) + with zipfile.ZipFile(file_location+"/"+file.filename) as zip: + zip.extractall(file_location) else: print("no zip file given") - new_project = insert_into_model(Project, project_json, urljoin(API_URL, "/projects"), "project_id", required_fields=["title", "descriptions", "course_id", "visible_for_students", "archieved"]) - - return new_project + return {}, 200 diff --git a/backend/project/utils/query_agent.py b/backend/project/utils/query_agent.py index 24e857e2..5f258282 100644 --- a/backend/project/utils/query_agent.py +++ b/backend/project/utils/query_agent.py @@ -50,6 +50,28 @@ def delete_by_id_from_model( return {"error": "Something went wrong while deleting from the database.", "url": base_url}, 500 + +def create_model_instance(model: DeclarativeMeta, + data: Dict[str, Union[str, int]], + response_url_base: str, + required_fields: List[str] = None): + if required_fields is None: + required_fields = [] + # Check if all non-nullable fields are present in the data + missing_fields = [field for field in required_fields if field not in data] + + if missing_fields: + return {"error": f"Missing required fields: {', '.join(missing_fields)}", + "url": response_url_base}, 400 + + filtered_data = filter_model_fields(model, data) + new_instance: DeclarativeMeta = model(**filtered_data) + db.session.add(new_instance) + db.session.commit() + + return new_instance + + def insert_into_model(model: DeclarativeMeta, data: Dict[str, Union[str, int]], response_url_base: str, @@ -69,26 +91,14 @@ def insert_into_model(model: DeclarativeMeta, a message indicating that something went wrong while inserting into the database. """ try: - if required_fields is None: - required_fields = [] - # Check if all non-nullable fields are present in the data - missing_fields = [field for field in required_fields if field not in data] - - if missing_fields: - return {"error": f"Missing required fields: {', '.join(missing_fields)}", - "url": response_url_base}, 400 - - filtered_data = filter_model_fields(model, data) - new_instance: DeclarativeMeta = model(**filtered_data) - db.session.add(new_instance) - db.session.commit() + new_instance = create_model_instance(model, data, response_url_base, required_fields) + return jsonify({ "data": new_instance, "message": "Object created succesfully.", - "url": urljoin(response_url_base, str(getattr(new_instance, url_id_field)))}), 201 - except SQLAlchemyError as e: - print("error") - print(e) + "url": urljoin(response_url_base + "/", str(getattr(new_instance, url_id_field)))}), 201 + except SQLAlchemyError: + db.session.rollback() return jsonify({"error": "Something went wrong while inserting into the database.", "url": response_url_base}), 500 From bc6e4a9793e9176cb871e3a2d51353d02ac104a1 Mon Sep 17 00:00:00 2001 From: gerwoud Date: Mon, 11 Mar 2024 13:44:37 +0100 Subject: [PATCH 27/56] fix 400 when non zip is uploaded --- backend/project/endpoints/projects/projects.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/backend/project/endpoints/projects/projects.py b/backend/project/endpoints/projects/projects.py index 03572a30..e13f358e 100644 --- a/backend/project/endpoints/projects/projects.py +++ b/backend/project/endpoints/projects/projects.py @@ -90,6 +90,10 @@ def post(self): with zipfile.ZipFile(file_location+"/"+file.filename) as zip: zip.extractall(file_location) else: - print("no zip file given") + return {"message": "Please provide a .zip file for uploading the instructions"}, 400 - return {}, 200 + return { + "message": "Project created succesfully", + "data": new_new_project, + "url": f"{API_URL}/projects/{id}" + }, 200 From 62632be484243e8f04b14bbe8c7b2a962965b0d4 Mon Sep 17 00:00:00 2001 From: gerwoud Date: Mon, 11 Mar 2024 15:42:19 +0100 Subject: [PATCH 28/56] fixed tests --- .../project/endpoints/projects/projects.py | 28 ++++++++++--------- backend/project/utils/query_agent.py | 14 ++++++---- backend/tests.yaml | 1 + backend/tests/endpoints/conftest.py | 1 - backend/tests/endpoints/project_test.py | 15 ++++++---- 5 files changed, 35 insertions(+), 24 deletions(-) diff --git a/backend/project/endpoints/projects/projects.py b/backend/project/endpoints/projects/projects.py index e13f358e..0da4861f 100644 --- a/backend/project/endpoints/projects/projects.py +++ b/backend/project/endpoints/projects/projects.py @@ -19,6 +19,7 @@ UPLOAD_FOLDER = getenv('UPLOAD_URL') ALLOWED_EXTENSIONS = {'zip'} + def parse_immutabledict(request): output_json = {} for key, value in request.form.items(): @@ -31,9 +32,11 @@ def parse_immutabledict(request): output_json[key] = value return output_json + def allowed_file(filename: str): return '.' in filename and \ - filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS + filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS + class ProjectsEndpoint(Resource): """ @@ -65,6 +68,7 @@ def post(self): file = request.files["assignment_file"] project_json = parse_project_params() + filename = file.filename.split("/")[-1] # save the file that is given with the request @@ -74,26 +78,24 @@ def post(self): urljoin(API_URL, "/projects"), required_fields=["title", "descriptions", "course_id", "visible_for_students", "archieved"] ) - - print(new_new_project) id = new_new_project.project_id - print(id) + project_upload_directory = f"{UPLOAD_FOLDER}{new_new_project.project_id}" - file_location = "."+os.path.join(project_upload_directory) - print(file_location) - # print(new_new_project.json) + + file_location = "." + os.path.join(project_upload_directory) + if not os.path.exists(project_upload_directory): - os.makedirs(file_location) + os.makedirs(file_location, exist_ok=True) - if allowed_file(file.filename): - file.save(file_location+"/"+file.filename) - with zipfile.ZipFile(file_location+"/"+file.filename) as zip: + file.save(file_location + "/" + filename) + try: + with zipfile.ZipFile(file_location + "/" + filename) as zip: zip.extractall(file_location) - else: + except zipfile.BadZipfile: return {"message": "Please provide a .zip file for uploading the instructions"}, 400 return { "message": "Project created succesfully", "data": new_new_project, "url": f"{API_URL}/projects/{id}" - }, 200 + }, 201 diff --git a/backend/project/utils/query_agent.py b/backend/project/utils/query_agent.py index 5f258282..8eb7f4cc 100644 --- a/backend/project/utils/query_agent.py +++ b/backend/project/utils/query_agent.py @@ -92,11 +92,15 @@ def insert_into_model(model: DeclarativeMeta, """ try: new_instance = create_model_instance(model, data, response_url_base, required_fields) - - return jsonify({ - "data": new_instance, - "message": "Object created succesfully.", - "url": urljoin(response_url_base + "/", str(getattr(new_instance, url_id_field)))}), 201 + # if its a tuple the model instance couldn't be created so it already + # is the right format of error message and we just need to return + if isinstance(new_instance, tuple): + return new_instance + else: + return jsonify({ + "data": new_instance, + "message": "Object created succesfully.", + "url": urljoin(response_url_base + "/", str(getattr(new_instance, url_id_field)))}), 201 except SQLAlchemyError: db.session.rollback() return jsonify({"error": "Something went wrong while inserting into the database.", diff --git a/backend/tests.yaml b/backend/tests.yaml index 7b799a2e..2807d904 100644 --- a/backend/tests.yaml +++ b/backend/tests.yaml @@ -29,6 +29,7 @@ services: POSTGRES_PASSWORD: test_password POSTGRES_DB: test_database API_HOST: http://api_is_here + UPLOAD_URL: /project/endpoints/uploads/ volumes: - .:/app command: ["pytest"] diff --git a/backend/tests/endpoints/conftest.py b/backend/tests/endpoints/conftest.py index 0e964c22..143e463a 100644 --- a/backend/tests/endpoints/conftest.py +++ b/backend/tests/endpoints/conftest.py @@ -33,7 +33,6 @@ def project(course): title="Project", descriptions="Test project", course_id=course.course_id, - assignment_file="testfile", deadline=date, visible_for_students=True, archieved=False, diff --git a/backend/tests/endpoints/project_test.py b/backend/tests/endpoints/project_test.py index 1ebecce4..2ab36c26 100644 --- a/backend/tests/endpoints/project_test.py +++ b/backend/tests/endpoints/project_test.py @@ -1,5 +1,6 @@ """Tests for project endpoints.""" from project.models.projects import Project +import pytest def test_projects_home(client): """Test home project endpoint.""" @@ -23,9 +24,14 @@ def test_post_project(db_session, client, course_ad, course_teacher_ad, project_ db_session.commit() project_json["course_id"] = course_ad.course_id + project_json["assignment_file"] = open("testzip.zip", "rb") # post the project - response = client.post("/projects", json=project_json) + response = client.post( + "/projects", + data=project_json, + content_type='multipart/form-data' + ) assert response.status_code == 201 # check if the project with the id is present @@ -34,7 +40,6 @@ def test_post_project(db_session, client, course_ad, course_teacher_ad, project_ assert response.status_code == 200 - def test_remove_project(db_session, client, course_ad, course_teacher_ad, project_json): """Test removing a project to the datab and fetching it, testing if it's not present anymore""" @@ -45,10 +50,11 @@ def test_remove_project(db_session, client, course_ad, course_teacher_ad, projec db_session.commit() project_json["course_id"] = course_ad.course_id + project_json["assignment_file"] = open("testzip.zip", "rb") # post the project - response = client.post("/projects", json=project_json) - + response = client.post("/projects", data=project_json) + print(response) # check if the project with the id is present project_id = response.json["data"]["project_id"] @@ -59,7 +65,6 @@ def test_remove_project(db_session, client, course_ad, course_teacher_ad, projec response = client.delete(f"/projects/{project_id}") assert response.status_code == 404 - def test_patch_project(db_session, client, course_ad, course_teacher_ad, project): """ Test functionality of the PUT method for projects From 6f323ec8f70e398a37e6fa411cc3edaed348e51e Mon Sep 17 00:00:00 2001 From: gerwoud Date: Mon, 11 Mar 2024 15:44:03 +0100 Subject: [PATCH 29/56] added test zip file --- backend/testzip.zip | Bin 0 -> 175 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 backend/testzip.zip diff --git a/backend/testzip.zip b/backend/testzip.zip new file mode 100644 index 0000000000000000000000000000000000000000..f99b6a05484f7a0a5ffa496e382505de4987825a GIT binary patch literal 175 zcmWIWW@h1H00H-^>Y!K#PkYOlEEiTb3sVE5z;bdT5CiFgaB@mZZa5FHn zykKTv023fJX_+~xTmjyUOmfV)43hxa!N3T_TN*(ugwd=JqtT2F@MdKLsbd5}KOpT5 H;xGUJBf=uQ literal 0 HcmV?d00001 From b5f8ef9288c6bb1ba11fa9b2f6b47382aecb2b52 Mon Sep 17 00:00:00 2001 From: gerwoud Date: Mon, 11 Mar 2024 16:03:45 +0100 Subject: [PATCH 30/56] linter fixes --- backend/project/__main__.py | 2 - .../endpoints/projects/endpoint_parser.py | 21 ++++++++-- .../endpoints/projects/project_endpoint.py | 1 - .../project/endpoints/projects/projects.py | 41 +++++++++---------- backend/project/utils/query_agent.py | 16 +++++--- backend/tests/endpoints/project_test.py | 7 +++- 6 files changed, 53 insertions(+), 35 deletions(-) diff --git a/backend/project/__main__.py b/backend/project/__main__.py index 32547c6e..a4bd122b 100644 --- a/backend/project/__main__.py +++ b/backend/project/__main__.py @@ -1,7 +1,5 @@ """Main entry point for the application.""" -from sys import path -path.append(".") from dotenv import load_dotenv from project import create_app_with_db from project.db_in import url diff --git a/backend/project/endpoints/projects/endpoint_parser.py b/backend/project/endpoints/projects/endpoint_parser.py index d5ece633..7815bf5e 100644 --- a/backend/project/endpoints/projects/endpoint_parser.py +++ b/backend/project/endpoints/projects/endpoint_parser.py @@ -8,14 +8,29 @@ parser = reqparse.RequestParser() parser.add_argument('title', type=str, help='Projects title', location="form") parser.add_argument('descriptions', type=str, help='Projects description', location="form") -parser.add_argument('assignment_file', type=werkzeug.datastructures.FileStorage, help='Projects assignment file', location="form") +parser.add_argument( + 'assignment_file', + type=werkzeug.datastructures.FileStorage, + help='Projects assignment file', + location="form" +) parser.add_argument("deadline", type=str, help='Projects deadline', location="form") parser.add_argument("course_id", type=str, help='Projects course_id', location="form") -parser.add_argument("visible_for_students", type=bool, help='Projects visibility for students', location="form") +parser.add_argument( + "visible_for_students", + type=bool, + help='Projects visibility for students', + location="form" +) parser.add_argument("archieved", type=bool, help='Projects', location="form") parser.add_argument("test_path", type=str, help='Projects test path', location="form") parser.add_argument("script_name", type=str, help='Projects test script path', location="form") -parser.add_argument("regex_expressions", type=str, help='Projects regex expressions', location="form") +parser.add_argument( + "regex_expressions", + type=str, + help='Projects regex expressions', + location="form" +) def parse_project_params(): diff --git a/backend/project/endpoints/projects/project_endpoint.py b/backend/project/endpoints/projects/project_endpoint.py index eef5b34d..09938878 100644 --- a/backend/project/endpoints/projects/project_endpoint.py +++ b/backend/project/endpoints/projects/project_endpoint.py @@ -4,7 +4,6 @@ """ from flask import Blueprint -from flask_restful import Api from project.endpoints.projects.projects import ProjectsEndpoint from project.endpoints.projects.project_detail import ProjectDetail diff --git a/backend/project/endpoints/projects/projects.py b/backend/project/endpoints/projects/projects.py index 0da4861f..cbfddec0 100644 --- a/backend/project/endpoints/projects/projects.py +++ b/backend/project/endpoints/projects/projects.py @@ -5,13 +5,13 @@ from os import getenv from urllib.parse import urljoin +import zipfile + from flask import request from flask_restful import Resource -import zipfile - from project.models.projects import Project -from project.utils.query_agent import query_selected_from_model, insert_into_model, create_model_instance +from project.utils.query_agent import query_selected_from_model, create_model_instance from project.endpoints.projects.endpoint_parser import parse_project_params @@ -20,20 +20,13 @@ ALLOWED_EXTENSIONS = {'zip'} -def parse_immutabledict(request): - output_json = {} - for key, value in request.form.items(): - if value == "false": - print("false") - output_json[key] = False - if value == "true": - output_json[key] = True - else: - output_json[key] = value - return output_json + def allowed_file(filename: str): + """ + check if file extension is allowed for upload + """ return '.' in filename and \ filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS @@ -72,15 +65,19 @@ def post(self): # save the file that is given with the request - new_new_project = create_model_instance( + new_project = create_model_instance( Project, project_json, urljoin(API_URL, "/projects"), - required_fields=["title", "descriptions", "course_id", "visible_for_students", "archieved"] + required_fields=[ + "title", + "descriptions", + "course_id", + "visible_for_students", + "archieved"] ) - id = new_new_project.project_id - project_upload_directory = f"{UPLOAD_FOLDER}{new_new_project.project_id}" + project_upload_directory = f"{UPLOAD_FOLDER}{new_project.project_id}" file_location = "." + os.path.join(project_upload_directory) @@ -89,13 +86,13 @@ def post(self): file.save(file_location + "/" + filename) try: - with zipfile.ZipFile(file_location + "/" + filename) as zip: - zip.extractall(file_location) + with zipfile.ZipFile(file_location + "/" + filename) as upload_zip: + upload_zip.extractall(file_location) except zipfile.BadZipfile: return {"message": "Please provide a .zip file for uploading the instructions"}, 400 return { "message": "Project created succesfully", - "data": new_new_project, - "url": f"{API_URL}/projects/{id}" + "data": new_project, + "url": f"{API_URL}/projects/{new_project.project_id}" }, 201 diff --git a/backend/project/utils/query_agent.py b/backend/project/utils/query_agent.py index 8eb7f4cc..c1729a6d 100644 --- a/backend/project/utils/query_agent.py +++ b/backend/project/utils/query_agent.py @@ -55,6 +55,9 @@ def create_model_instance(model: DeclarativeMeta, data: Dict[str, Union[str, int]], response_url_base: str, required_fields: List[str] = None): + """ + Create an instance of a model + """ if required_fields is None: required_fields = [] # Check if all non-nullable fields are present in the data @@ -96,11 +99,14 @@ def insert_into_model(model: DeclarativeMeta, # is the right format of error message and we just need to return if isinstance(new_instance, tuple): return new_instance - else: - return jsonify({ - "data": new_instance, - "message": "Object created succesfully.", - "url": urljoin(response_url_base + "/", str(getattr(new_instance, url_id_field)))}), 201 + + return (jsonify({ + "data": new_instance, + "message": "Object created succesfully.", + "url": + urljoin(response_url_base + "/", + str(getattr(new_instance, url_id_field)))}), + 201) except SQLAlchemyError: db.session.rollback() return jsonify({"error": "Something went wrong while inserting into the database.", diff --git a/backend/tests/endpoints/project_test.py b/backend/tests/endpoints/project_test.py index 2ab36c26..50d00d07 100644 --- a/backend/tests/endpoints/project_test.py +++ b/backend/tests/endpoints/project_test.py @@ -1,6 +1,5 @@ """Tests for project endpoints.""" from project.models.projects import Project -import pytest def test_projects_home(client): """Test home project endpoint.""" @@ -24,6 +23,8 @@ def test_post_project(db_session, client, course_ad, course_teacher_ad, project_ db_session.commit() project_json["course_id"] = course_ad.course_id + # cant be done with 'with' because it autocloses then + # pylint: disable=R1732 project_json["assignment_file"] = open("testzip.zip", "rb") # post the project @@ -50,8 +51,10 @@ def test_remove_project(db_session, client, course_ad, course_teacher_ad, projec db_session.commit() project_json["course_id"] = course_ad.course_id - project_json["assignment_file"] = open("testzip.zip", "rb") + # cant be done with 'with' because it autocloses then + # pylint: disable=R1732 + project_json["assignment_file"] = open("testzip.zip", "rb") # post the project response = client.post("/projects", data=project_json) print(response) From 72a9b7db31bc5397b2f8f79c4552a748f9bd0a3a Mon Sep 17 00:00:00 2001 From: gerwoud Date: Mon, 11 Mar 2024 16:08:11 +0100 Subject: [PATCH 31/56] removed some test files --- backend/project/endpoints/projects/endpoint_parser.py | 1 - backend/tests/endpoints/project_test.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/project/endpoints/projects/endpoint_parser.py b/backend/project/endpoints/projects/endpoint_parser.py index 7815bf5e..2f5be9bb 100644 --- a/backend/project/endpoints/projects/endpoint_parser.py +++ b/backend/project/endpoints/projects/endpoint_parser.py @@ -39,7 +39,6 @@ def parse_project_params(): """ args = parser.parse_args() result_dict = {} - print(args) for key, value in args.items(): if value is not None: diff --git a/backend/tests/endpoints/project_test.py b/backend/tests/endpoints/project_test.py index 50d00d07..54cedbf2 100644 --- a/backend/tests/endpoints/project_test.py +++ b/backend/tests/endpoints/project_test.py @@ -57,7 +57,7 @@ def test_remove_project(db_session, client, course_ad, course_teacher_ad, projec project_json["assignment_file"] = open("testzip.zip", "rb") # post the project response = client.post("/projects", data=project_json) - print(response) + # check if the project with the id is present project_id = response.json["data"]["project_id"] From 12d0aa5f2d6dd9f84acb6fd26faed473357bec48 Mon Sep 17 00:00:00 2001 From: gerwoud Date: Mon, 11 Mar 2024 18:01:01 +0100 Subject: [PATCH 32/56] linter and test fixes --- backend/project/__init__.py | 1 - backend/project/endpoints/projects/projects.py | 5 ----- 2 files changed, 6 deletions(-) diff --git a/backend/project/__init__.py b/backend/project/__init__.py index fa7891df..299412f1 100644 --- a/backend/project/__init__.py +++ b/backend/project/__init__.py @@ -7,7 +7,6 @@ from .endpoints.index.index import index_bp from .endpoints.projects.project_endpoint import project_bp from .endpoints.courses.courses_config import courses_bp -from .endpoints.courses.courses_config import courses_bp from .endpoints.users import users_bp diff --git a/backend/project/endpoints/projects/projects.py b/backend/project/endpoints/projects/projects.py index a1ba6cde..8d6ae75f 100644 --- a/backend/project/endpoints/projects/projects.py +++ b/backend/project/endpoints/projects/projects.py @@ -4,17 +4,12 @@ import os from os import getenv from urllib.parse import urljoin - -from flask import request import zipfile from flask import request from flask_restful import Resource from project.models.project import Project -from project.utils.query_agent import query_selected_from_model, insert_into_model - -from project.models.projects import Project from project.utils.query_agent import query_selected_from_model, create_model_instance from project.endpoints.projects.endpoint_parser import parse_project_params From e1624b12bc31cc91595cdf2de67bffb272b1a272 Mon Sep 17 00:00:00 2001 From: gerwoud Date: Mon, 11 Mar 2024 18:50:49 +0100 Subject: [PATCH 33/56] import depedency fix --- backend/project/endpoints/projects/endpoint_parser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/project/endpoints/projects/endpoint_parser.py b/backend/project/endpoints/projects/endpoint_parser.py index 2f5be9bb..3c64f9e5 100644 --- a/backend/project/endpoints/projects/endpoint_parser.py +++ b/backend/project/endpoints/projects/endpoint_parser.py @@ -3,14 +3,14 @@ """ from flask_restful import reqparse -import werkzeug +from werkzeug.datastructures import FileStorage parser = reqparse.RequestParser() parser.add_argument('title', type=str, help='Projects title', location="form") parser.add_argument('descriptions', type=str, help='Projects description', location="form") parser.add_argument( 'assignment_file', - type=werkzeug.datastructures.FileStorage, + type=FileStorage, help='Projects assignment file', location="form" ) From 123992c4d3db058b812b788303ac6e1c87b5e38a Mon Sep 17 00:00:00 2001 From: gerwoud Date: Mon, 11 Mar 2024 18:53:57 +0100 Subject: [PATCH 34/56] fix import order --- backend/project/endpoints/projects/project_detail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/project/endpoints/projects/project_detail.py b/backend/project/endpoints/projects/project_detail.py index f8d4ebf1..85d7b99c 100644 --- a/backend/project/endpoints/projects/project_detail.py +++ b/backend/project/endpoints/projects/project_detail.py @@ -9,9 +9,9 @@ from flask import request from flask_restful import Resource +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.models.project import Project From 2c3a71914fee62bc5c43f4de737ab7411c1009ae Mon Sep 17 00:00:00 2001 From: gerwoud Date: Mon, 11 Mar 2024 18:54:31 +0100 Subject: [PATCH 35/56] removed import getenv --- backend/project/endpoints/projects/projects.py | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/project/endpoints/projects/projects.py b/backend/project/endpoints/projects/projects.py index 8d6ae75f..84412ddf 100644 --- a/backend/project/endpoints/projects/projects.py +++ b/backend/project/endpoints/projects/projects.py @@ -2,7 +2,6 @@ Module that implements the /projects endpoint of the API """ import os -from os import getenv from urllib.parse import urljoin import zipfile From 33880d6c960bf78a0c89021eab07b3a529c31225 Mon Sep 17 00:00:00 2001 From: gerwoud Date: Mon, 11 Mar 2024 18:59:49 +0100 Subject: [PATCH 36/56] removed valid_project function --- backend/project/endpoints/projects/projects.py | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/backend/project/endpoints/projects/projects.py b/backend/project/endpoints/projects/projects.py index 84412ddf..00f8c9aa 100644 --- a/backend/project/endpoints/projects/projects.py +++ b/backend/project/endpoints/projects/projects.py @@ -13,21 +13,8 @@ from project.endpoints.projects.endpoint_parser import parse_project_params -API_URL = getenv('API_HOST') -UPLOAD_FOLDER = getenv('UPLOAD_URL') -ALLOWED_EXTENSIONS = {'zip'} - - - - - -def allowed_file(filename: str): - """ - check if file extension is allowed for upload - """ - return '.' in filename and \ - filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS - +API_URL = os.getenv('API_HOST') +UPLOAD_FOLDER = os.getenv('UPLOAD_URL') class ProjectsEndpoint(Resource): """ From 10b0c1aa560aac5f3bf9a849fd643bf910c2fe2a Mon Sep 17 00:00:00 2001 From: gerwoud Date: Mon, 11 Mar 2024 19:14:50 +0100 Subject: [PATCH 37/56] fix fstring --- backend/project/endpoints/projects/projects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/project/endpoints/projects/projects.py b/backend/project/endpoints/projects/projects.py index 00f8c9aa..261d5861 100644 --- a/backend/project/endpoints/projects/projects.py +++ b/backend/project/endpoints/projects/projects.py @@ -53,7 +53,7 @@ def post(self): new_project = create_model_instance( Project, project_json, - urljoin(API_URL, "/projects"), + urljoin(f"{API_URL}/", "/projects"), required_fields=[ "title", "descriptions", From 0edbd46c83872a656582405c68f9ab41e3db0a0c Mon Sep 17 00:00:00 2001 From: gerwoud Date: Mon, 11 Mar 2024 19:19:30 +0100 Subject: [PATCH 38/56] fix: upload_directory --- backend/project/endpoints/projects/projects.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/project/endpoints/projects/projects.py b/backend/project/endpoints/projects/projects.py index 261d5861..ca1bf08a 100644 --- a/backend/project/endpoints/projects/projects.py +++ b/backend/project/endpoints/projects/projects.py @@ -62,9 +62,9 @@ def post(self): "archieved"] ) - project_upload_directory = f"{UPLOAD_FOLDER}{new_project.project_id}" + project_upload_directory = os.path.join(f"{UPLOAD_FOLDER}", f"{new_project.project_id}") - file_location = "." + os.path.join(project_upload_directory) + file_location = os.path.join(project_upload_directory) if not os.path.exists(project_upload_directory): os.makedirs(file_location, exist_ok=True) From 3438972b1233a7ae14351ee6928528cc7d743312 Mon Sep 17 00:00:00 2001 From: gerwoud Date: Mon, 11 Mar 2024 19:21:56 +0100 Subject: [PATCH 39/56] removed exist_ok=True --- backend/project/endpoints/projects/projects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/project/endpoints/projects/projects.py b/backend/project/endpoints/projects/projects.py index ca1bf08a..a2767c89 100644 --- a/backend/project/endpoints/projects/projects.py +++ b/backend/project/endpoints/projects/projects.py @@ -67,7 +67,7 @@ def post(self): file_location = os.path.join(project_upload_directory) if not os.path.exists(project_upload_directory): - os.makedirs(file_location, exist_ok=True) + os.makedirs(file_location) file.save(file_location + "/" + filename) try: From 8892d9873874f4bab4fc01f5bd47a33a46cc8816 Mon Sep 17 00:00:00 2001 From: gerwoud Date: Mon, 11 Mar 2024 19:28:16 +0100 Subject: [PATCH 40/56] use path.join --- backend/project/endpoints/projects/projects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/project/endpoints/projects/projects.py b/backend/project/endpoints/projects/projects.py index a2767c89..8602f39c 100644 --- a/backend/project/endpoints/projects/projects.py +++ b/backend/project/endpoints/projects/projects.py @@ -69,7 +69,7 @@ def post(self): if not os.path.exists(project_upload_directory): os.makedirs(file_location) - file.save(file_location + "/" + filename) + file.save(os.path.join(file_location, filename)) try: with zipfile.ZipFile(file_location + "/" + filename) as upload_zip: upload_zip.extractall(file_location) From c5e3bc44b94e771ae5ae00f619b598bd08516f32 Mon Sep 17 00:00:00 2001 From: gerwoud Date: Mon, 11 Mar 2024 19:29:25 +0100 Subject: [PATCH 41/56] added url field --- backend/project/endpoints/projects/projects.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/backend/project/endpoints/projects/projects.py b/backend/project/endpoints/projects/projects.py index 8602f39c..5fde6a53 100644 --- a/backend/project/endpoints/projects/projects.py +++ b/backend/project/endpoints/projects/projects.py @@ -74,7 +74,11 @@ def post(self): with zipfile.ZipFile(file_location + "/" + filename) as upload_zip: upload_zip.extractall(file_location) except zipfile.BadZipfile: - return {"message": "Please provide a .zip file for uploading the instructions"}, 400 + return ({ + "message": "Please provide a .zip file for uploading the instructions", + "url": f"{API_URL}/projects" + }, + 400) return { "message": "Project created succesfully", From 5c6a28deb9bd82bde9cf813babef027fd2bdc6ca Mon Sep 17 00:00:00 2001 From: gerwoud Date: Mon, 11 Mar 2024 19:44:08 +0100 Subject: [PATCH 42/56] fixed not checking for tuple type anymore --- backend/project/endpoints/projects/projects.py | 2 +- backend/project/utils/query_agent.py | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/backend/project/endpoints/projects/projects.py b/backend/project/endpoints/projects/projects.py index 5fde6a53..d6091a2e 100644 --- a/backend/project/endpoints/projects/projects.py +++ b/backend/project/endpoints/projects/projects.py @@ -60,7 +60,7 @@ def post(self): "course_id", "visible_for_students", "archieved"] - ) + )[0] project_upload_directory = os.path.join(f"{UPLOAD_FOLDER}", f"{new_project.project_id}") diff --git a/backend/project/utils/query_agent.py b/backend/project/utils/query_agent.py index 41cb9e86..2398d9be 100644 --- a/backend/project/utils/query_agent.py +++ b/backend/project/utils/query_agent.py @@ -72,7 +72,7 @@ def create_model_instance(model: DeclarativeMeta, db.session.add(new_instance) db.session.commit() - return new_instance + return new_instance, 201 def insert_into_model(model: DeclarativeMeta, @@ -95,18 +95,20 @@ def insert_into_model(model: DeclarativeMeta, """ try: new_instance = create_model_instance(model, data, response_url_base, required_fields) + model_instance = new_instance[0] + status_code = new_instance[1] # if its a tuple the model instance couldn't be created so it already # is the right format of error message and we just need to return - if isinstance(new_instance, tuple): - return new_instance + if status_code == 400: + return model_instance, status_code return (jsonify({ - "data": new_instance, + "data": model_instance, "message": "Object created succesfully.", "url": urljoin(response_url_base + "/", - str(getattr(new_instance, url_id_field)))}), - 201) + str(getattr(model_instance, url_id_field)))}), + status_code) except SQLAlchemyError: db.session.rollback() return jsonify({"error": "Something went wrong while inserting into the database.", From 0be828f0aaf1286e732abc50d9113aa8653d3103 Mon Sep 17 00:00:00 2001 From: gerwoud Date: Mon, 11 Mar 2024 19:45:42 +0100 Subject: [PATCH 43/56] fixed env var for tests --- backend/tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/tests.yaml b/backend/tests.yaml index 2807d904..fd6d7a16 100644 --- a/backend/tests.yaml +++ b/backend/tests.yaml @@ -29,7 +29,7 @@ services: POSTGRES_PASSWORD: test_password POSTGRES_DB: test_database API_HOST: http://api_is_here - UPLOAD_URL: /project/endpoints/uploads/ + UPLOAD_URL: /data/assignments volumes: - .:/app command: ["pytest"] From adbb14baa4f0762fae7bfdb5f61bef192c3aabff Mon Sep 17 00:00:00 2001 From: gerwoud Date: Mon, 11 Mar 2024 19:50:18 +0100 Subject: [PATCH 44/56] fixed env var for tests --- backend/tests/endpoints/project_test.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/backend/tests/endpoints/project_test.py b/backend/tests/endpoints/project_test.py index 369dc97b..8e2c49a9 100644 --- a/backend/tests/endpoints/project_test.py +++ b/backend/tests/endpoints/project_test.py @@ -54,9 +54,12 @@ def test_remove_project(db_session, client, course_ad, course_teacher_ad, projec # cant be done with 'with' because it autocloses then # pylint: disable=R1732 - project_json["assignment_file"] = open("testzip.zip", "rb") + # project_json["assignment_file"] = open("testzip.zip", "rb") # post the project - response = client.post("/projects", data=project_json) + # response = client.post("/projects", data=project_json) + with open("testzip.zip", "rb") as zip_file: + project_json["assignment_file"] = open("testzip.zip", "rb") + response = client.post("/projects", data=project_json) # check if the project with the id is present project_id = response.json["data"]["project_id"] From f0be9495970651d44cd5e42c479e1560427f69d9 Mon Sep 17 00:00:00 2001 From: gerwoud Date: Mon, 11 Mar 2024 19:53:41 +0100 Subject: [PATCH 45/56] fixed with statements --- backend/tests/endpoints/project_test.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/backend/tests/endpoints/project_test.py b/backend/tests/endpoints/project_test.py index 8e2c49a9..b833a03d 100644 --- a/backend/tests/endpoints/project_test.py +++ b/backend/tests/endpoints/project_test.py @@ -25,14 +25,15 @@ def test_post_project(db_session, client, course_ad, course_teacher_ad, project_ project_json["course_id"] = course_ad.course_id # cant be done with 'with' because it autocloses then # pylint: disable=R1732 - project_json["assignment_file"] = open("testzip.zip", "rb") + with open("testzip.zip", "rb") as zip_file: + project_json["assignment_file"] = zip_file + # post the project + response = client.post( + "/projects", + data=project_json, + content_type='multipart/form-data' + ) - # post the project - response = client.post( - "/projects", - data=project_json, - content_type='multipart/form-data' - ) assert response.status_code == 201 # check if the project with the id is present @@ -52,13 +53,9 @@ def test_remove_project(db_session, client, course_ad, course_teacher_ad, projec project_json["course_id"] = course_ad.course_id - # cant be done with 'with' because it autocloses then - # pylint: disable=R1732 - # project_json["assignment_file"] = open("testzip.zip", "rb") # post the project - # response = client.post("/projects", data=project_json) with open("testzip.zip", "rb") as zip_file: - project_json["assignment_file"] = open("testzip.zip", "rb") + project_json["assignment_file"] = zip_file response = client.post("/projects", data=project_json) # check if the project with the id is present From 220856fa81f1a5a6499bb4d8f19e645f7dec0886 Mon Sep 17 00:00:00 2001 From: gerwoud Date: Mon, 11 Mar 2024 20:02:34 +0100 Subject: [PATCH 46/56] using os.path.split instead of regular split --- backend/project/endpoints/projects/projects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/project/endpoints/projects/projects.py b/backend/project/endpoints/projects/projects.py index d6091a2e..2a0a2d9a 100644 --- a/backend/project/endpoints/projects/projects.py +++ b/backend/project/endpoints/projects/projects.py @@ -46,7 +46,7 @@ def post(self): file = request.files["assignment_file"] project_json = parse_project_params() - filename = file.filename.split("/")[-1] + filename = os.path.split(file.filename)[1] # save the file that is given with the request From 8c848d68f429f271c0299e1ab1cb9f675ce4fc5a Mon Sep 17 00:00:00 2001 From: gerwoud Date: Mon, 11 Mar 2024 21:20:01 +0100 Subject: [PATCH 47/56] added exist_ok --- backend/project/endpoints/projects/projects.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/project/endpoints/projects/projects.py b/backend/project/endpoints/projects/projects.py index 2a0a2d9a..f7dec135 100644 --- a/backend/project/endpoints/projects/projects.py +++ b/backend/project/endpoints/projects/projects.py @@ -66,8 +66,7 @@ def post(self): file_location = os.path.join(project_upload_directory) - if not os.path.exists(project_upload_directory): - os.makedirs(file_location) + os.makedirs(file_location, exist_ok=True) file.save(os.path.join(file_location, filename)) try: From cb7eac1c8a8cc44938883980b717cec920686e13 Mon Sep 17 00:00:00 2001 From: gerwoud Date: Mon, 11 Mar 2024 21:25:43 +0100 Subject: [PATCH 48/56] i forgot :skull: fix lmao yeet --- backend/project/endpoints/projects/projects.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/backend/project/endpoints/projects/projects.py b/backend/project/endpoints/projects/projects.py index f7dec135..624949ca 100644 --- a/backend/project/endpoints/projects/projects.py +++ b/backend/project/endpoints/projects/projects.py @@ -64,14 +64,12 @@ def post(self): project_upload_directory = os.path.join(f"{UPLOAD_FOLDER}", f"{new_project.project_id}") - file_location = os.path.join(project_upload_directory) + os.makedirs(project_upload_directory, exist_ok=True) - os.makedirs(file_location, exist_ok=True) - - file.save(os.path.join(file_location, filename)) + file.save(os.path.join(project_upload_directory, filename)) try: - with zipfile.ZipFile(file_location + "/" + filename) as upload_zip: - upload_zip.extractall(file_location) + with zipfile.ZipFile(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", From 6db6fada770ec71e37b30314a25da9a33046883f Mon Sep 17 00:00:00 2001 From: gerwoud Date: Mon, 11 Mar 2024 21:27:14 +0100 Subject: [PATCH 49/56] i forgot :skull: fix lmao yeet --- backend/project/utils/query_agent.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/backend/project/utils/query_agent.py b/backend/project/utils/query_agent.py index 2398d9be..d530f8a6 100644 --- a/backend/project/utils/query_agent.py +++ b/backend/project/utils/query_agent.py @@ -94,9 +94,8 @@ def insert_into_model(model: DeclarativeMeta, a message indicating that something went wrong while inserting into the database. """ try: - new_instance = create_model_instance(model, data, response_url_base, required_fields) - model_instance = new_instance[0] - status_code = new_instance[1] + model_instance, status_code = create_model_instance(model, data, response_url_base, required_fields) + # if its a tuple the model instance couldn't be created so it already # is the right format of error message and we just need to return if status_code == 400: From 058f53e5f925f4cfa3873223bcf4a0f787662e06 Mon Sep 17 00:00:00 2001 From: gerwoud Date: Mon, 11 Mar 2024 21:27:57 +0100 Subject: [PATCH 50/56] goofy augh fstring --- backend/project/utils/query_agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/project/utils/query_agent.py b/backend/project/utils/query_agent.py index d530f8a6..ab6e8973 100644 --- a/backend/project/utils/query_agent.py +++ b/backend/project/utils/query_agent.py @@ -105,7 +105,7 @@ def insert_into_model(model: DeclarativeMeta, "data": model_instance, "message": "Object created succesfully.", "url": - urljoin(response_url_base + "/", + urljoin(f"{response_url_base}/", str(getattr(model_instance, url_id_field)))}), status_code) except SQLAlchemyError: From 2314ff6652a66fd65777637baa826ee42c096c4f Mon Sep 17 00:00:00 2001 From: gerwoud Date: Mon, 11 Mar 2024 21:29:22 +0100 Subject: [PATCH 51/56] another small fix --- backend/project/endpoints/projects/projects.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/project/endpoints/projects/projects.py b/backend/project/endpoints/projects/projects.py index 624949ca..6369b4fc 100644 --- a/backend/project/endpoints/projects/projects.py +++ b/backend/project/endpoints/projects/projects.py @@ -50,7 +50,7 @@ def post(self): # save the file that is given with the request - new_project = create_model_instance( + new_project, _ = create_model_instance( Project, project_json, urljoin(f"{API_URL}/", "/projects"), @@ -60,7 +60,7 @@ def post(self): "course_id", "visible_for_students", "archieved"] - )[0] + ) project_upload_directory = os.path.join(f"{UPLOAD_FOLDER}", f"{new_project.project_id}") From 251ff2905d7d5b81a1dea60a2f6c3b6d3df79971 Mon Sep 17 00:00:00 2001 From: gerwoud Date: Mon, 11 Mar 2024 21:31:37 +0100 Subject: [PATCH 52/56] fixed the 'not fixed eh' problem --- backend/project/endpoints/projects/projects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/project/endpoints/projects/projects.py b/backend/project/endpoints/projects/projects.py index 6369b4fc..39815429 100644 --- a/backend/project/endpoints/projects/projects.py +++ b/backend/project/endpoints/projects/projects.py @@ -68,7 +68,7 @@ def post(self): file.save(os.path.join(project_upload_directory, filename)) try: - with zipfile.ZipFile(project_upload_directory + "/" + filename) as upload_zip: + with zipfile.ZipFile(os.path.join(project_upload_directory, filename)) as upload_zip: upload_zip.extractall(project_upload_directory) except zipfile.BadZipfile: return ({ From f01fed999aa07437503bb54755891245f11d3f63 Mon Sep 17 00:00:00 2001 From: gerwoud Date: Mon, 11 Mar 2024 21:34:35 +0100 Subject: [PATCH 53/56] linting --- backend/project/utils/query_agent.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/backend/project/utils/query_agent.py b/backend/project/utils/query_agent.py index ab6e8973..bcdf1ea0 100644 --- a/backend/project/utils/query_agent.py +++ b/backend/project/utils/query_agent.py @@ -94,7 +94,11 @@ def insert_into_model(model: DeclarativeMeta, a message indicating that something went wrong while inserting into the database. """ try: - model_instance, status_code = create_model_instance(model, data, response_url_base, required_fields) + model_instance, status_code = create_model_instance( + model, + data, + response_url_base, + required_fields) # if its a tuple the model instance couldn't be created so it already # is the right format of error message and we just need to return From d53eef1061720364d33e7a0c381a41a7b43a7408 Mon Sep 17 00:00:00 2001 From: gerwoud Date: Mon, 11 Mar 2024 21:39:34 +0100 Subject: [PATCH 54/56] fix handling fail --- backend/project/endpoints/projects/projects.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/project/endpoints/projects/projects.py b/backend/project/endpoints/projects/projects.py index 39815429..bfa65a0e 100644 --- a/backend/project/endpoints/projects/projects.py +++ b/backend/project/endpoints/projects/projects.py @@ -50,7 +50,7 @@ def post(self): # save the file that is given with the request - new_project, _ = create_model_instance( + new_project, status_code = create_model_instance( Project, project_json, urljoin(f"{API_URL}/", "/projects"), @@ -62,6 +62,9 @@ def post(self): "archieved"] ) + if status_code == 400: + 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) From dad70154761b641e52f1f0fc52cdc5b7b881eaf4 Mon Sep 17 00:00:00 2001 From: gerwoud Date: Tue, 12 Mar 2024 12:42:57 +0100 Subject: [PATCH 55/56] added try block --- .../project/endpoints/projects/projects.py | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/backend/project/endpoints/projects/projects.py b/backend/project/endpoints/projects/projects.py index bfa65a0e..b86bd021 100644 --- a/backend/project/endpoints/projects/projects.py +++ b/backend/project/endpoints/projects/projects.py @@ -49,18 +49,22 @@ def post(self): filename = os.path.split(file.filename)[1] # save the file that is given with the request - - new_project, status_code = create_model_instance( - Project, - project_json, - urljoin(f"{API_URL}/", "/projects"), - required_fields=[ - "title", - "descriptions", - "course_id", - "visible_for_students", - "archieved"] - ) + try: + new_project, status_code = create_model_instance( + Project, + project_json, + urljoin(f"{API_URL}/", "/projects"), + required_fields=[ + "title", + "descriptions", + "course_id", + "visible_for_students", + "archieved"] + ) + except SQLAlchemyError: + db.session.rollback() + return jsonify({"error": "Something went wrong while inserting into the database.", + "url": f"{API_URL}/projects"}), 500 if status_code == 400: return new_project, status_code From f4fe9fa83979bfcb0c60c14bd38fa4cd18de63a5 Mon Sep 17 00:00:00 2001 From: gerwoud Date: Tue, 12 Mar 2024 13:09:57 +0100 Subject: [PATCH 56/56] linter --- backend/project/endpoints/projects/projects.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/project/endpoints/projects/projects.py b/backend/project/endpoints/projects/projects.py index b86bd021..ed26f568 100644 --- a/backend/project/endpoints/projects/projects.py +++ b/backend/project/endpoints/projects/projects.py @@ -4,10 +4,12 @@ import os from urllib.parse import urljoin import zipfile +from sqlalchemy.exc import SQLAlchemyError -from flask import request +from flask import request, jsonify from flask_restful import Resource + from project.models.project import Project from project.utils.query_agent import query_selected_from_model, create_model_instance @@ -62,7 +64,6 @@ def post(self): "archieved"] ) except SQLAlchemyError: - db.session.rollback() return jsonify({"error": "Something went wrong while inserting into the database.", "url": f"{API_URL}/projects"}), 500