diff --git a/server/app/apis/media.py b/server/app/apis/media.py index 830b16de0a6125dde7c95406c8b7bc928c2bc105..8f0b28f7b332184f043f3f79562c02b6a001f7ca 100644 --- a/server/app/apis/media.py +++ b/server/app/apis/media.py @@ -1,41 +1,21 @@ -import os - import app.core.http_codes as codes import app.database.controller as dbc from app.apis import check_jwt, 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 current_app, request +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 from sqlalchemy import exc +import app.core.files as files api = MediaDTO.api image_set = MediaDTO.image_set schema = MediaDTO.schema list_schema = MediaDTO.list_schema -PHOTO_PATH = current_app.config["UPLOADED_PHOTOS_DEST"] - - -def generate_thumbnail(filename): - thumbnail_size = current_app.config["THUMBNAIL_SIZE"] - path = os.path.join(PHOTO_PATH, filename) - thumb_path = os.path.join(PHOTO_PATH, f"thumbnail_{filename}") - with Image.open(path) as im: - im.thumbnail(thumbnail_size) - im.save(thumb_path) - - -def delete_image(filename): - path = os.path.join(PHOTO_PATH, filename) - thumb_path = os.path.join(PHOTO_PATH, f"thumbnail_{filename}") - os.remove(path) - os.remove(thumb_path) - @api.route("/images") class ImageList(Resource): @@ -50,16 +30,16 @@ class ImageList(Resource): 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()) + filename = files.save_image_with_thumbnail(request.files["image"]) + item = Media.query.filter(Media.filename == filename).first() + if not item: + 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") - finally: - return item_response(schema.dump(item)) @api.route("/images/<ID>") @@ -74,11 +54,10 @@ class ImageList(Resource): def delete(self, ID): item = dbc.get.one(Media, ID) try: - delete_image(item.filename) + files.delete_image_and_thumbnail(item.filename) dbc.delete.default(item) + return {}, codes.NO_CONTENT except OSError: api.abort(codes.BAD_REQUEST, "Could not delete the file image") except exc.SQLAlchemyError: api.abort(codes.INTERNAL_SERVER_ERROR, "Something went wrong when trying to delete image") - finally: - return {}, codes.NO_CONTENT diff --git a/server/app/core/files.py b/server/app/core/files.py new file mode 100644 index 0000000000000000000000000000000000000000..01a03166811b02f06e2b21f21430dd25837b2067 --- /dev/null +++ b/server/app/core/files.py @@ -0,0 +1,88 @@ +from PIL import Image, ImageChops +from flask import current_app +import os +import datetime +from flask_uploads import IMAGES, UploadSet + +PHOTO_PATH = current_app.config["UPLOADED_PHOTOS_DEST"] +THUMBNAIL_SIZE = current_app.config["THUMBNAIL_SIZE"] +image_set = UploadSet("photos", IMAGES) + + +def compare_images(input_image, output_image): + # compare image dimensions (assumption 1) + if input_image.size != output_image.size: + return False + + rows, cols = input_image.size + + # compare image pixels (assumption 2 and 3) + for row in range(rows): + for col in range(cols): + input_pixel = input_image.getpixel((row, col)) + output_pixel = output_image.getpixel((row, col)) + if input_pixel != output_pixel: + return False + + return True + + +def _delete_image(filename): + path = os.path.join(PHOTO_PATH, filename) + os.remove(path) + + +def save_image_with_thumbnail(image_file): + saved_filename = image_set.save(image_file) + saved_path = os.path.join(PHOTO_PATH, saved_filename) + with Image.open(saved_path) as im: + im_thumbnail = im.copy() + im_thumbnail.thumbnail(THUMBNAIL_SIZE) + thumb_path = os.path.join(PHOTO_PATH, f"thumbnail_{saved_filename}") + im_thumbnail.save(thumb_path) + im.close() + return saved_filename + + +def delete_image_and_thumbnail(filename): + _delete_image(filename) + _delete_image(f"thumbnail_{filename}") + + +""" +def _resolve_name_conflict(filename): + split = os.path.splitext(filename) + suffix = split[0] + preffix = split[1] + now = datetime.datetime.now() + time_stamp = now.strftime("%Y%m%d%H%M%S") + return f"{suffix}-{time_stamp}{preffix}" +""" +""" +def save_image_with_thumbnail(image_file): + filename = image_file.filename + path = os.path.join(PHOTO_PATH, filename) + + saved_filename = image_set.save(image_file) + saved_path = os.path.join(PHOTO_PATH, saved_filename) + im = Image.open(saved_path) + + # Check if image already exists + if path != saved_path: + im_existing = Image.open(path) + # If both images are identical, then return None + if compare_images(im, im_existing): + im.close() + im_existing.close() + _delete_image(saved_filename) + return filename + + path = os.path.join(PHOTO_PATH, saved_filename) + im_thumbnail = im.copy() + im_thumbnail.thumbnail(THUMBNAIL_SIZE) + + thumb_path = os.path.join(PHOTO_PATH, f"thumbnail_{saved_filename}") + im_thumbnail.save(thumb_path) + im.close() + return saved_filename +""" diff --git a/server/app/core/rich_schemas.py b/server/app/core/rich_schemas.py index 8852c392c4a060a065f618a66393e55b8a2627a8..da5f58d653ea5efb6c00648db56c1bc81a24d44b 100644 --- a/server/app/core/rich_schemas.py +++ b/server/app/core/rich_schemas.py @@ -42,6 +42,8 @@ class SlideSchemaRich(RichSchema): title = ma.auto_field() timer = ma.auto_field() competition_id = ma.auto_field() + background_image_id = ma.auto_field() + background_image = ma.Function(lambda x: x.background_image.filename if x.background_image is not None else "") questions = fields.Nested(QuestionSchemaRich, many=True) components = fields.Nested(schemas.ComponentSchema, many=True) @@ -54,6 +56,9 @@ class CompetitionSchemaRich(RichSchema): name = ma.auto_field() year = ma.auto_field() city_id = ma.auto_field() + background_image_id = ma.auto_field() + background_image = ma.Function(lambda x: x.background_image.filename if x.background_image is not None else "") + slides = fields.Nested( SlideSchemaRich, many=True, diff --git a/server/app/core/schemas.py b/server/app/core/schemas.py index 94a73829c870a8ee17083d555aa62d5aacfb025d..df0ad8d93152398bb18fdc61c0f9669d002290b5 100644 --- a/server/app/core/schemas.py +++ b/server/app/core/schemas.py @@ -116,6 +116,8 @@ class SlideSchema(BaseSchema): title = ma.auto_field() timer = ma.auto_field() competition_id = ma.auto_field() + background_image_id = ma.auto_field() + background_image = ma.Function(lambda x: x.background_image.filename if x.background_image is not None else "") class TeamSchema(BaseSchema): @@ -146,6 +148,8 @@ class CompetitionSchema(BaseSchema): name = ma.auto_field() year = ma.auto_field() city_id = ma.auto_field() + background_image_id = ma.auto_field() + background_image = ma.Function(lambda x: x.background_image.filename if x.background_image is not None else "") class ComponentSchema(BaseSchema): diff --git a/server/app/database/controller/add.py b/server/app/database/controller/add.py index e398b587f534c80611796825b85ab1bcddd820ef..280608785f6ea6a777f8cd968241ae25f0d3cf9b 100644 --- a/server/app/database/controller/add.py +++ b/server/app/database/controller/add.py @@ -35,6 +35,7 @@ from sqlalchemy import exc from sqlalchemy.orm import with_polymorphic from sqlalchemy.orm import relation from sqlalchemy.orm.session import sessionmaker +from flask import current_app def db_add(item): @@ -63,12 +64,11 @@ def component(type_id, slide_id, x=0, y=0, w=0, h=0, **data): Adds a component to the slide at the specified coordinates with the provided size and data . """ - from app.apis.media import PHOTO_PATH if type_id == 2: # 2 is image item_image = get.one(Media, data["media_id"]) filename = item_image.filename - path = os.path.join(PHOTO_PATH, filename) + path = os.path.join(current_app.config["UPLOADED_PHOTOS_DEST"], filename) with Image.open(path) as im: h = im.height w = im.width diff --git a/server/app/database/models.py b/server/app/database/models.py index 17c9b45993c1a534dc6c2b21ab1218ba90b30107..a8edc6cbefaa86409a8178be65024edfdaca7e46 100644 --- a/server/app/database/models.py +++ b/server/app/database/models.py @@ -93,7 +93,9 @@ class Competition(db.Model): year = db.Column(db.Integer, nullable=False, default=2020) font = db.Column(db.String(STRING_SIZE), nullable=False) 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) + background_image = db.relationship("Media", uselist=False) slides = db.relationship("Slide", backref="competition") teams = db.relationship("Team", backref="competition") diff --git a/server/configmodule.py b/server/configmodule.py index e202abd3fd4f76899598d77df66de8bf148925a2..8c07211ed58d7fa2550893fc241a9a2170527a2f 100644 --- a/server/configmodule.py +++ b/server/configmodule.py @@ -13,7 +13,7 @@ class Config: JWT_BLACKLIST_TOKEN_CHECKS = ["access", "refresh"] JWT_ACCESS_TOKEN_EXPIRES = timedelta(days=2) JWT_REFRESH_TOKEN_EXPIRES = timedelta(days=30) - UPLOADED_PHOTOS_DEST = os.path.join(os.getcwd(), "app/static/images") + UPLOADED_PHOTOS_DEST = os.path.join(os.getcwd(), "app", "static", "images") THUMBNAIL_SIZE = (120, 120) SECRET_KEY = os.urandom(24) SQLALCHEMY_ECHO = False