From fd07018b0345e3dbadc15074d3aeb74bf51a11ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carl=20Sch=C3=B6nfelder?= <carsc272@student.liu.se> Date: Wed, 14 Apr 2021 10:15:28 +0000 Subject: [PATCH] Resolve "upload images" --- .gitignore | 3 +- server/app/__init__.py | 3 ++ server/app/apis/__init__.py | 4 +- server/app/apis/admin.py | 8 ---- server/app/apis/components.py | 0 server/app/apis/media.py | 47 +++++++++++++++++++++++ server/app/core/dto.py | 8 ++++ server/app/core/parsers.py | 4 ++ server/app/database/controller/add.py | 6 +++ server/app/database/controller/search.py | 15 +++++--- server/app/database/models.py | 7 ++++ server/configmodule.py | 8 +++- server/requirements.txt | Bin 2170 -> 2206 bytes 13 files changed, 96 insertions(+), 17 deletions(-) delete mode 100644 server/app/apis/admin.py create mode 100644 server/app/apis/components.py create mode 100644 server/app/apis/media.py diff --git a/.gitignore b/.gitignore index d68e7dcf..a6906867 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ __pycache__ htmlcov .pytest_cache /.idea -.vs/ \ No newline at end of file +.vs/ +*/static \ No newline at end of file diff --git a/server/app/__init__.py b/server/app/__init__.py index 48a104f8..8aa7a08a 100644 --- a/server/app/__init__.py +++ b/server/app/__init__.py @@ -1,7 +1,9 @@ from flask import Flask, redirect, request +from flask_uploads import IMAGES, UploadSet, configure_uploads import app.database.models as models from app.core import bcrypt, db, jwt, ma +from app.core.dto import MediaDTO def create_app(config_name="configmodule.DevelopmentConfig"): @@ -14,6 +16,7 @@ def create_app(config_name="configmodule.DevelopmentConfig"): jwt.init_app(app) db.init_app(app) ma.init_app(app) + configure_uploads(app, (MediaDTO.image_set,)) from app.apis import flask_api diff --git a/server/app/apis/__init__.py b/server/app/apis/__init__.py index 996c14be..3b2ed213 100644 --- a/server/app/apis/__init__.py +++ b/server/app/apis/__init__.py @@ -44,6 +44,7 @@ from flask_restx import Api from .auth import api as auth_ns from .competitions import api as comp_ns +from .media import api as media_ns from .misc import api as misc_ns from .questions import api as question_ns from .slides import api as slide_ns @@ -51,6 +52,7 @@ from .teams import api as team_ns from .users import api as user_ns flask_api = Api() +flask_api.add_namespace(media_ns, path="/api/media") flask_api.add_namespace(misc_ns, path="/api/misc") flask_api.add_namespace(user_ns, path="/api/users") flask_api.add_namespace(auth_ns, path="/api/auth") @@ -58,4 +60,4 @@ flask_api.add_namespace(comp_ns, path="/api/competitions") flask_api.add_namespace(slide_ns, path="/api/competitions/<CID>/slides") flask_api.add_namespace(team_ns, path="/api/competitions/<CID>/teams") flask_api.add_namespace(question_ns, path="/api/competitions/<CID>/questions") -#flask_api.add_namespace(question_ns, path="/api/competitions/<CID>/slides/<SID>/question") +# flask_api.add_namespace(question_ns, path="/api/competitions/<CID>/slides/<SID>/question") diff --git a/server/app/apis/admin.py b/server/app/apis/admin.py deleted file mode 100644 index 92d3922c..00000000 --- a/server/app/apis/admin.py +++ /dev/null @@ -1,8 +0,0 @@ -### -# Admin stuff placed here for later use -# No need to implement this before the application is somewhat done -### - -from flask import Blueprint - -admin_blueprint = Blueprint("admin", __name__) diff --git a/server/app/apis/components.py b/server/app/apis/components.py new file mode 100644 index 00000000..e69de29b diff --git a/server/app/apis/media.py b/server/app/apis/media.py new file mode 100644 index 00000000..a7c2d5d1 --- /dev/null +++ b/server/app/apis/media.py @@ -0,0 +1,47 @@ +import app.core.http_codes as codes +import app.database.controller as dbc +from app.apis import admin_required, item_response, list_response +from app.core.dto import MediaDTO +from app.core.parsers import media_parser_search +from app.database.models import City, Media, MediaType, QuestionType, Role +from flask import request +from flask_jwt_extended import get_jwt_identity, jwt_required +from flask_restx import Resource, reqparse +from flask_uploads import UploadNotAllowed +from PIL import Image + +api = MediaDTO.api +image_set = MediaDTO.image_set +schema = MediaDTO.schema +list_schema = MediaDTO.list_schema + + +def generate_thumbnail(filename): + with Image.open(f"./static/images/{filename}") as im: + im.thumbnail((120, 120)) + im.save(f"./static/images/thumbnail_{filename}") + + +@api.route("/images") +class ImageList(Resource): + @jwt_required + def get(self): + args = media_parser_search.parse_args(strict=True) + items, total = dbc.search.image(**args) + return list_response(list_schema.dump(items), total) + + @jwt_required + def post(self): + if "image" not in request.files: + api.abort(codes.BAD_REQUEST, "Missing image in request.files") + + try: + filename = image_set.save(request.files["image"]) + generate_thumbnail(filename) + print(filename) + item = dbc.add.image(filename, get_jwt_identity()) + return item_response(schema.dump(item)) + except UploadNotAllowed: + api.abort(codes.BAD_REQUEST, "Could not save the image") + except: + api.abort(codes.INTERNAL_SERVER_ERROR, "Something went wrong when trying to save image") diff --git a/server/app/core/dto.py b/server/app/core/dto.py index e2f36bbb..99d467ba 100644 --- a/server/app/core/dto.py +++ b/server/app/core/dto.py @@ -2,6 +2,14 @@ import app.core.rich_schemas as rich_schemas import app.core.schemas as schemas import marshmallow as ma from flask_restx import Namespace, fields +from flask_uploads import IMAGES, UploadSet + + +class MediaDTO: + api = Namespace("media") + image_set = UploadSet("photos", IMAGES) + schema = schemas.MediaSchema(many=False) + list_schema = schemas.MediaSchema(many=True) class AuthDTO: diff --git a/server/app/core/parsers.py b/server/app/core/parsers.py index f05adc10..7a1c6089 100644 --- a/server/app/core/parsers.py +++ b/server/app/core/parsers.py @@ -64,3 +64,7 @@ question_parser.add_argument("type_id", type=int, default=None, location="json") ###TEAM#### team_parser = reqparse.RequestParser() team_parser.add_argument("name", type=str, location="json") + +###SEARCH_COMPETITION#### +media_parser_search = search_parser.copy() +media_parser_search.add_argument("filename", type=str, default=None, location="args") diff --git a/server/app/database/controller/add.py b/server/app/database/controller/add.py index 37f34dd7..f612cdd6 100644 --- a/server/app/database/controller/add.py +++ b/server/app/database/controller/add.py @@ -4,6 +4,7 @@ from app.database.models import ( Blacklist, City, Competition, + Media, MediaType, Question, QuestionType, @@ -35,6 +36,11 @@ def blacklist(jti): return Blacklist(jti) +@db_add +def image(filename, user_id): + return Media(filename, 1, user_id) + + @db_add def slide(item_competition): order = Slide.query.filter(Slide.competition_id == item_competition.id).count() # first element has index 0 diff --git a/server/app/database/controller/search.py b/server/app/database/controller/search.py index a2ca6b84..466efd01 100644 --- a/server/app/database/controller/search.py +++ b/server/app/database/controller/search.py @@ -1,4 +1,12 @@ -from app.database.models import Competition, Question, Slide, Team, User +from app.database.models import Competition, Media, Question, Slide, Team, User + + +def image(filename, page=0, page_size=15, order=1, order_by=None): + query = Media.query.filter(Media.type_id == 1) + if filename: + query = query.filter(Media.filename.like(f"%{filename}%")) + + return query.pagination(page, page_size, None, None) def user(email=None, name=None, city_id=None, role_id=None, page=0, page_size=15, order=1, order_by=None): @@ -74,10 +82,7 @@ def questions( if slide_id: query = query.filter(Question.slide_id == slide_id) if competition_id: - slide_ids = set( - [x.id for x in Slide.query.filter(Slide.competition_id == competition_id).all()] - ) # TODO: Filter using database instead of creating a set of slide_ids - query = query.filter(Question.slide_id.in_(slide_ids)) + query = query.join(Slide, (Slide.competition_id == competition_id) & (Slide.id == Question.slide_id)) order_column = Question.id # Default order_by if order_by: diff --git a/server/app/database/models.py b/server/app/database/models.py index c3bc3d56..5a17fb0d 100644 --- a/server/app/database/models.py +++ b/server/app/database/models.py @@ -1,5 +1,6 @@ from app.core import bcrypt, db from sqlalchemy.ext.hybrid import hybrid_method, hybrid_property +from sqlalchemy.orm import backref STRING_SIZE = 254 @@ -90,10 +91,13 @@ class Competition(db.Model): year = db.Column(db.Integer, nullable=False, default=2020) city_id = db.Column(db.Integer, db.ForeignKey("city.id"), nullable=False) + background_image_id = db.Column(db.Integer, db.ForeignKey("media.id"), nullable=True) slides = db.relationship("Slide", backref="competition") teams = db.relationship("Team", backref="competition") + background_image = db.relationship("Media", uselist=False) + def __init__(self, name, year, city_id): self.name = name self.year = year @@ -123,6 +127,9 @@ class Slide(db.Model): settings = db.Column(db.Text, nullable=False, default="{}") competition_id = db.Column(db.Integer, db.ForeignKey("competition.id"), nullable=False) + background_image_id = db.Column(db.Integer, db.ForeignKey("media.id"), nullable=True) + background_image = db.relationship("Media", uselist=False) + def __init__(self, order, competition_id): self.order = order self.competition_id = competition_id diff --git a/server/configmodule.py b/server/configmodule.py index d042d594..fcf23cbf 100644 --- a/server/configmodule.py +++ b/server/configmodule.py @@ -1,22 +1,26 @@ +import os from datetime import timedelta class Config: DEBUG = False TESTING = False + BUNDLE_ERRORS = True SQLALCHEMY_DATABASE_URI = "sqlite:///database.db" + SQLALCHEMY_TRACK_MODIFICATIONS = False JWT_SECRET_KEY = "super-secret" JWT_BLACKLIST_ENABLED = True JWT_BLACKLIST_TOKEN_CHECKS = ["access", "refresh"] - BUNDLE_ERRORS = True JWT_ACCESS_TOKEN_EXPIRES = timedelta(days=2) JWT_REFRESH_TOKEN_EXPIRES = timedelta(days=30) - SQLALCHEMY_TRACK_MODIFICATIONS = False + UPLOADED_PHOTOS_DEST = "static/images" # os.getcwd() + SECRET_KEY = os.urandom(24) class DevelopmentConfig(Config): DEBUG = True SQLALCHEMY_DATABASE_URI = "sqlite:///database.db" + SQLALCHEMY_ECHO = True class TestingConfig(Config): diff --git a/server/requirements.txt b/server/requirements.txt index 172f152510c3ccd6245f19e5cb53ac702a6b4370..cbda8a7040167ce59a2209747e8d7d4204f7fe2d 100644 GIT binary patch delta 77 zcmew*Fi&v96(-prhE#@9h608hhJ1!Zh7=$#g~1jG4H@(p3?}bq%G?~nEXO1nz>o=4 V0#Z~CmbL&&8-bK;&Sm9g1_0fO5QhK& delta 61 zcmbOy_)B2J6{g8-%pvM-3^@#m48;uD47v=V3<W@5K9HXR6tM+D0|q??BOo^19LlWB Kw0RmU2QvWGHVr=j -- GitLab