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

Enhancement/projects file upload #62

Merged
merged 58 commits into from
Mar 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
12830d5
added query agent containing functions that can be used by multiple e…
AronBuzogany Mar 6, 2024
81e3d3c
loading env variables is only necessary in __main__
AronBuzogany Mar 6, 2024
658dfd3
removed unneeded load_dotenv
AronBuzogany Mar 6, 2024
7dd5682
completed functions that are ought to be used by multiple endpoints o…
AronBuzogany Mar 6, 2024
e42c57d
simplified endpoint functions by using query_agent functions
AronBuzogany Mar 6, 2024
3467bb2
fixed linting
AronBuzogany Mar 6, 2024
69df26e
fixed urljoin incorrectly joining url
AronBuzogany Mar 6, 2024
e836f50
lint: removed trailing whitepsace
AronBuzogany Mar 6, 2024
e26c015
completely replaced functionality with query_agent functions
AronBuzogany Mar 6, 2024
fd941b2
added functionality for patching an entry in the database
AronBuzogany Mar 6, 2024
073d6c5
fixed linting
AronBuzogany Mar 6, 2024
fd2ae83
filtered queries and forms to only contain entries that are valid in …
AronBuzogany Mar 6, 2024
d675fe6
created function that filters dict keys that are not in table
AronBuzogany Mar 6, 2024
2298d8c
made class serializable
AronBuzogany Mar 6, 2024
d0e9a10
url query is not a valid authentication method, filtered out option
AronBuzogany Mar 6, 2024
6b3e733
using query_agent functions to prevent code duplication
AronBuzogany Mar 6, 2024
a00cb99
split courses into multiple files to keep it organized
AronBuzogany Mar 6, 2024
f4aff02
fixed linting
AronBuzogany Mar 6, 2024
4e0945f
added new courses blueprint
AronBuzogany Mar 6, 2024
05038c9
removed trailing space
AronBuzogany Mar 6, 2024
a142a88
changed test to stay consistent with course admin relation also
AronBuzogany Mar 7, 2024
1f11956
added query agent functions to prevent code duplication
AronBuzogany Mar 7, 2024
7fad876
first version of file uploads
Gerwoud Mar 7, 2024
ca42936
formatting json for posting in the db
Gerwoud Mar 7, 2024
ed94408
Merge branch 'enhancement/endpoints-cleanup' into enhancement/project…
Gerwoud Mar 7, 2024
8d75ae6
working file upload system, reused parser
Gerwoud Mar 7, 2024
11377d1
fixed extracting of zip and uploading in project upload directory
Gerwoud Mar 11, 2024
bc6e4a9
fix
Gerwoud Mar 11, 2024
62632be
fixed tests
Gerwoud Mar 11, 2024
6f323ec
added test zip file
Gerwoud Mar 11, 2024
b5f8ef9
linter fixes
Gerwoud Mar 11, 2024
72a9b7d
removed some test files
Gerwoud Mar 11, 2024
e1f79c7
Merge branch 'development' into enhancement/projects-file-upload
Gerwoud Mar 11, 2024
12d0aa5
linter and test fixes
Gerwoud Mar 11, 2024
e1624b1
import depedency fix
Gerwoud Mar 11, 2024
123992c
fix import order
Gerwoud Mar 11, 2024
2c3a719
removed import getenv
Gerwoud Mar 11, 2024
33880d6
removed valid_project function
Gerwoud Mar 11, 2024
10b0c1a
fix fstring
Gerwoud Mar 11, 2024
0edbd46
fix: upload_directory
Gerwoud Mar 11, 2024
3438972
removed exist_ok=True
Gerwoud Mar 11, 2024
8892d98
use path.join
Gerwoud Mar 11, 2024
c5e3bc4
added url field
Gerwoud Mar 11, 2024
5c6a28d
fixed not checking for tuple type anymore
Gerwoud Mar 11, 2024
0be828f
fixed env var for tests
Gerwoud Mar 11, 2024
adbb14b
fixed env var for tests
Gerwoud Mar 11, 2024
f0be949
fixed with statements
Gerwoud Mar 11, 2024
220856f
using os.path.split instead of regular split
Gerwoud Mar 11, 2024
8c848d6
added exist_ok
Gerwoud Mar 11, 2024
cb7eac1
i forgot :skull: fix lmao yeet
Gerwoud Mar 11, 2024
6db6fad
i forgot :skull: fix lmao yeet
Gerwoud Mar 11, 2024
058f53e
goofy augh fstring
Gerwoud Mar 11, 2024
2314ff6
another small fix
Gerwoud Mar 11, 2024
251ff29
fixed the 'not fixed eh' problem
Gerwoud Mar 11, 2024
f01fed9
linting
Gerwoud Mar 11, 2024
d53eef1
fix handling fail
Gerwoud Mar 11, 2024
dad7015
added try block
Gerwoud Mar 12, 2024
f4fe9fa
linter
Gerwoud Mar 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.