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

Commit

Permalink
Merge pull request #62 from SELab-2/enhancement/projects-file-upload
Browse files Browse the repository at this point in the history
Enhancement/projects file upload
  • Loading branch information
Vucis authored Mar 12, 2024
2 parents 815fd2a + f4fe9fa commit 33c4b59
Show file tree
Hide file tree
Showing 9 changed files with 138 additions and 44 deletions.
36 changes: 26 additions & 10 deletions backend/project/endpoints/projects/endpoint_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
1 change: 1 addition & 0 deletions backend/project/endpoints/projects/project_detail.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
patch_by_id_from_model



API_URL = getenv('API_HOST')
RESPONSE_URL = urljoin(API_URL, "projects")

Expand Down
2 changes: 0 additions & 2 deletions backend/project/endpoints/projects/project_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
62 changes: 54 additions & 8 deletions backend/project/endpoints/projects/projects.py
Original file line number Diff line number Diff line change
@@ -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):
"""
Expand Down Expand Up @@ -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
61 changes: 43 additions & 18 deletions backend/project/utils/query_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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

Expand Down
1 change: 1 addition & 0 deletions backend/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
1 change: 0 additions & 1 deletion backend/tests/endpoints/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
18 changes: 13 additions & 5 deletions backend/tests/endpoints/project_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"""

Expand All @@ -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"]
Expand All @@ -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
Expand Down
Binary file added backend/testzip.zip
Binary file not shown.

0 comments on commit 33c4b59

Please sign in to comment.