diff --git a/backend/project/endpoints/projects/endpoint_parser.py b/backend/project/endpoints/projects/endpoint_parser.py index 87f61e69..3c64f9e5 100644 --- a/backend/project/endpoints/projects/endpoint_parser.py +++ b/backend/project/endpoints/projects/endpoint_parser.py @@ -3,18 +3,34 @@ """ from flask_restful import reqparse +from werkzeug.datastructures import FileStorage 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("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('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=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("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/project_detail.py b/backend/project/endpoints/projects/project_detail.py index df4e99d7..85d7b99c 100644 --- a/backend/project/endpoints/projects/project_detail.py +++ b/backend/project/endpoints/projects/project_detail.py @@ -14,6 +14,7 @@ patch_by_id_from_model + API_URL = getenv('API_HOST') RESPONSE_URL = urljoin(API_URL, "projects") diff --git a/backend/project/endpoints/projects/project_endpoint.py b/backend/project/endpoints/projects/project_endpoint.py index c996a514..09938878 100644 --- a/backend/project/endpoints/projects/project_endpoint.py +++ b/backend/project/endpoints/projects/project_endpoint.py @@ -4,13 +4,11 @@ """ from flask import Blueprint -from flask_restful import Api from project.endpoints.projects.projects import ProjectsEndpoint 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 275e21eb..ed26f568 100644 --- a/backend/project/endpoints/projects/projects.py +++ b/backend/project/endpoints/projects/projects.py @@ -1,16 +1,22 @@ """ Module that implements the /projects endpoint of the API """ -from os import getenv +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, insert_into_model +from project.utils.query_agent import query_selected_from_model, create_model_instance + +from project.endpoints.projects.endpoint_parser import parse_project_params -API_URL = getenv('API_HOST') +API_URL = os.getenv('API_HOST') +UPLOAD_FOLDER = os.getenv('UPLOAD_URL') class ProjectsEndpoint(Resource): """ @@ -40,7 +46,47 @@ def post(self): using flask_restfull parse lib """ - return insert_into_model( - Project,request.json, - urljoin(API_URL, "/projects"), - "project_id") + file = request.files["assignment_file"] + project_json = parse_project_params() + filename = os.path.split(file.filename)[1] + + # save the file that is given with the request + 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: + 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 + + project_upload_directory = os.path.join(f"{UPLOAD_FOLDER}", f"{new_project.project_id}") + + os.makedirs(project_upload_directory, exist_ok=True) + + file.save(os.path.join(project_upload_directory, filename)) + try: + with zipfile.ZipFile(os.path.join(project_upload_directory, filename)) as upload_zip: + upload_zip.extractall(project_upload_directory) + except zipfile.BadZipfile: + return ({ + "message": "Please provide a .zip file for uploading the instructions", + "url": f"{API_URL}/projects" + }, + 400) + + return { + "message": "Project created succesfully", + "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 8a688163..bcdf1ea0 100644 --- a/backend/project/utils/query_agent.py +++ b/backend/project/utils/query_agent.py @@ -50,6 +50,31 @@ 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): + """ + Create an instance of a model + """ + 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, 201 + + def insert_into_model(model: DeclarativeMeta, data: Dict[str, Union[str, int]], response_url_base: str, @@ -69,26 +94,26 @@ 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() - return jsonify({ - "data": new_instance, + 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: + return model_instance, status_code + + return (jsonify({ + "data": model_instance, "message": "Object created succesfully.", - "url": urljoin( - f"{response_url_base}/", - str(getattr(new_instance, url_id_field)))}), 201 + "url": + urljoin(f"{response_url_base}/", + 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.", "url": response_url_base}), 500 diff --git a/backend/tests.yaml b/backend/tests.yaml index 7b799a2e..fd6d7a16 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: /data/assignments volumes: - .:/app command: ["pytest"] diff --git a/backend/tests/endpoints/conftest.py b/backend/tests/endpoints/conftest.py index 6110ac93..1861ec85 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 0ddc96fa..b833a03d 100644 --- a/backend/tests/endpoints/project_test.py +++ b/backend/tests/endpoints/project_test.py @@ -23,9 +23,17 @@ 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 + 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", json=project_json) assert response.status_code == 201 # check if the project with the id is present @@ -34,7 +42,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""" @@ -47,7 +54,9 @@ def test_remove_project(db_session, client, course_ad, course_teacher_ad, projec project_json["course_id"] = course_ad.course_id # post the project - response = client.post("/projects", json=project_json) + with open("testzip.zip", "rb") as zip_file: + project_json["assignment_file"] = zip_file + response = client.post("/projects", data=project_json) # check if the project with the id is present project_id = response.json["data"]["project_id"] @@ -59,7 +68,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 diff --git a/backend/testzip.zip b/backend/testzip.zip new file mode 100644 index 00000000..f99b6a05 Binary files /dev/null and b/backend/testzip.zip differ