diff --git a/client/src/interfaces/ApiModels.ts b/client/src/interfaces/ApiModels.ts index 85933175e45980d46acf8f62dc0af55ca7e3ee23..73fe6db65f1b733abd4d0c7c7e81f1e74f907e19 100644 --- a/client/src/interfaces/ApiModels.ts +++ b/client/src/interfaces/ApiModels.ts @@ -83,17 +83,13 @@ export interface Component { } export interface ImageComponent extends Component { - data: { - media_id: number - filename: string - } + media_id: number + filename: string } export interface TextComponent extends Component { - data: { - text: string - font: string - } + text: string + font: string } export interface QuestionAlternativeComponent extends Component { diff --git a/client/src/pages/presentationEditor/components/ImageComponentDisplay.tsx b/client/src/pages/presentationEditor/components/ImageComponentDisplay.tsx index 7886a9b15899dfe4b790be9d3673591b4d1940d7..5e409d57f0e6f8a7503745760dd9078440fa732c 100644 --- a/client/src/pages/presentationEditor/components/ImageComponentDisplay.tsx +++ b/client/src/pages/presentationEditor/components/ImageComponentDisplay.tsx @@ -10,7 +10,7 @@ type ImageComponentProps = { const ImageComponentDisplay = ({ component, width, height }: ImageComponentProps) => { return ( <img - src={`http://localhost:5000/static/images/${component.data.filename}`} + src={`http://localhost:5000/static/images/${component.filename}`} height={height} width={width} draggable={false} diff --git a/client/src/pages/presentationEditor/components/Images.tsx b/client/src/pages/presentationEditor/components/Images.tsx index 4f942374a604dbee5b8796c645b19f1f545e6c56..b715ba2c4482d6d074f53409fbb1f4abab996409 100644 --- a/client/src/pages/presentationEditor/components/Images.tsx +++ b/client/src/pages/presentationEditor/components/Images.tsx @@ -46,10 +46,7 @@ const Images = ({ activeSlide, competitionId }: ImagesProps) => { const imageData = { x: 0, y: 0, - data: { - media_id: media.id, - filename: media.filename, - }, + media_id: media.id, type_id: 2, } await axios @@ -78,7 +75,7 @@ const Images = ({ activeSlide, competitionId }: ImagesProps) => { const handleCloseimageClick = async (image: ImageComponent) => { // Removes selected image component and deletes its file from the server. await axios - .delete(`/api/media/images/${image.data.media_id}`) + .delete(`/api/media/images/${image.media_id}`) .then(() => { dispatch(getEditorCompetition(competitionId)) }) @@ -112,9 +109,9 @@ const Images = ({ activeSlide, competitionId }: ImagesProps) => { images.map((image) => ( <div key={image.id}> <ListItem divider button> - <ImportedImage src={`http://localhost:5000/static/images/thumbnail_${image.data.filename}`} /> + <ImportedImage src={`http://localhost:5000/static/images/thumbnail_${image.filename}`} /> <Center> - <ListItemText primary={image.data.filename} /> + <ListItemText primary={image.filename} /> </Center> <CloseIcon onClick={() => handleCloseimageClick(image)} /> </ListItem> diff --git a/client/src/pages/presentationEditor/components/RndComponent.tsx b/client/src/pages/presentationEditor/components/RndComponent.tsx index effd3362df2cbb5ae8301c7e731934f6dee7abb6..c7b5933abb8144722d4348a81de59f1fd669002e 100644 --- a/client/src/pages/presentationEditor/components/RndComponent.tsx +++ b/client/src/pages/presentationEditor/components/RndComponent.tsx @@ -72,9 +72,7 @@ const RndComponent = ({ component, width, height }: ImageComponentProps) => { <HoverContainer hover={hover} dangerouslySetInnerHTML={{ - __html: `<div style="font-size: ${Math.round(24 * scale)}px;">${ - (component as TextComponent).data.text - }</div>`, + __html: `<div style="font-size: ${Math.round(24 * scale)}px;">${(component as TextComponent).text}</div>`, }} /> ) @@ -83,7 +81,7 @@ const RndComponent = ({ component, width, height }: ImageComponentProps) => { <HoverContainer hover={hover}> <img key={component.id} - src={`/static/images/${(component as ImageComponent).data.filename}`} + src={`/static/images/${(component as ImageComponent).filename}`} height={currentSize.h * scale} width={currentSize.w * scale} draggable={false} @@ -95,7 +93,7 @@ const RndComponent = ({ component, width, height }: ImageComponentProps) => { <HoverContainer hover={hover}> <img key={component.id} - src={`/static/images/${(component as ImageComponent).data.filename}`} + src={`/static/images/${(component as ImageComponent).filename}`} height={currentSize.h * scale} width={currentSize.w * scale} draggable={false} diff --git a/client/src/pages/presentationEditor/components/TextComponentEdit.tsx b/client/src/pages/presentationEditor/components/TextComponentEdit.tsx index 0a5c8522447c3111629ac179ce27532048d55c7a..f50e547a8d1e7f5eadadca1ad1d531a46cc0410e 100644 --- a/client/src/pages/presentationEditor/components/TextComponentEdit.tsx +++ b/client/src/pages/presentationEditor/components/TextComponentEdit.tsx @@ -24,7 +24,7 @@ const TextComponentEdit = ({ component }: ImageComponentProps) => { const dispatch = useAppDispatch() useEffect(() => { - setContent(component.data.text) + setContent(component.text) }, []) const handleSaveText = async (a: string) => { @@ -38,7 +38,7 @@ const TextComponentEdit = ({ component }: ImageComponentProps) => { window.setTimeout(async () => { console.log('Content was updated on server. id: ', component.id) await axios.put(`/api/competitions/${competitionId}/slides/${activeSlideId}/components/${component.id}`, { - data: { ...component.data, text: a }, + data: { ...component, text: a }, }) dispatch(getEditorCompetition(id)) }, 250) diff --git a/client/src/pages/presentationEditor/components/Texts.tsx b/client/src/pages/presentationEditor/components/Texts.tsx index 22cde214c92bb64788b4f69d9993977c85fe981d..31ecf57cd29325972328647290fc74de7c9827bd 100644 --- a/client/src/pages/presentationEditor/components/Texts.tsx +++ b/client/src/pages/presentationEditor/components/Texts.tsx @@ -27,7 +27,7 @@ const Texts = ({ activeSlide, competitionId }: TextsProps) => { if (activeSlide) { await axios.post(`/api/competitions/${competitionId}/slides/${activeSlide?.id}/components`, { type_id: 1, - data: { text: 'Ny text' }, + text: 'Ny text', w: 315, h: 50, }) 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/parsers.py b/server/app/core/parsers.py index 71f8fdee352c3fe88831124b0c8907fcbc51f505..1e9813e99d9d131d42dd221ef893ff6eb99b81f6 100644 --- a/server/app/core/parsers.py +++ b/server/app/core/parsers.py @@ -97,11 +97,13 @@ component_parser.add_argument("x", type=str, default=None, location="json") component_parser.add_argument("y", type=int, default=None, location="json") component_parser.add_argument("w", type=int, default=None, location="json") component_parser.add_argument("h", type=int, default=None, location="json") -component_parser.add_argument("data", type=dict, default=None, location="json") +# component_parser.add_argument("data", type=dict, default=None, location="json") component_create_parser = component_parser.copy() -component_create_parser.replace_argument("data", type=dict, required=True, location="json") +# component_create_parser.replace_argument("data", type=dict, required=True, location="json") component_create_parser.add_argument("type_id", type=int, required=True, location="json") +component_create_parser.add_argument("text", type=str, required=False, location="json") +component_create_parser.add_argument("media_id", type=str, required=False, location="json") login_code_parser = reqparse.RequestParser() login_code_parser.add_argument("code", type=str, location="json") 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 378f99af09167fc7ea6d7b40798d355cff5e8514..4e440fc4b5e282201f19602a428a506e763f670a 100644 --- a/server/app/core/schemas.py +++ b/server/app/core/schemas.py @@ -1,3 +1,4 @@ +from marshmallow.decorators import pre_load from marshmallow.decorators import pre_dump import app.database.models as models from app.core import ma @@ -115,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): @@ -145,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): @@ -156,6 +161,9 @@ class ComponentSchema(BaseSchema): y = ma.auto_field() w = ma.auto_field() h = ma.auto_field() - data = ma.Function(lambda obj: obj.data) slide_id = ma.auto_field() type_id = ma.auto_field() + + text = fields.fields.String() + media_id = fields.fields.Integer() + filename = ma.Function(lambda x: x.media.filename if hasattr(x, "media_id") else "") diff --git a/server/app/database/__init__.py b/server/app/database/__init__.py index 784c006b8fab59ca1dcc9e6dbac8ebffe91cb013..e0c78ad3af3f3376fdecdae4eae219aa1c62be24 100644 --- a/server/app/database/__init__.py +++ b/server/app/database/__init__.py @@ -6,6 +6,7 @@ from flask_sqlalchemy.model import Model from sqlalchemy import Column, DateTime, Text from sqlalchemy.sql import func from sqlalchemy.types import TypeDecorator +from sqlalchemy import event class Base(Model): diff --git a/server/app/database/controller/add.py b/server/app/database/controller/add.py index 1f57f7d06c74d9a0fd9999c99a5108b1d59ad25f..dceccfe85851a9dffe24032c0df5cc199f056ab0 100644 --- a/server/app/database/controller/add.py +++ b/server/app/database/controller/add.py @@ -14,6 +14,7 @@ from app.database.models import ( Competition, Component, ComponentType, + ImageComponent, Media, MediaType, Question, @@ -23,6 +24,7 @@ from app.database.models import ( Role, Slide, Team, + TextComponent, User, ViewType, ) @@ -30,8 +32,10 @@ from flask.globals import current_app from flask_restx import abort from PIL import Image 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): @@ -55,59 +59,16 @@ def db_add(item): return item -def blacklist(jti): - """ Adds a blacklist to the database. """ - - return db_add(Blacklist(jti)) - - -def mediaType(name): - """ Adds a media type to the database. """ - - return db_add(MediaType(name)) - - -def questionType(name): - """ Adds a question type to the database. """ - - return db_add(QuestionType(name)) - - -def componentType(name): - """ Adds a component type to the database. """ - - return db_add(ComponentType(name)) - - -def viewType(name): - """ Adds a view type to the database. """ - - return db_add(ViewType(name)) - - -def role(name): - """ Adds a role to the database. """ - - return db_add(Role(name)) - - -def city(name): - """ Adds a city to the database. """ - - return db_add(City(name)) - - -def component(type_id, slide_id, data, x=0, y=0, w=0, h=0): +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 @@ -118,37 +79,17 @@ def component(type_id, slide_id, data, x=0, y=0, w=0, h=0): w *= ratio h *= ratio - return db_add(Component(slide_id, type_id, data, x, y, w, h)) - - -def image(filename, user_id): - """ - Adds an image to the database and keeps track of who called the function. - """ - - return db_add(Media(filename, 1, user_id)) - - -def user(email, password, role_id, city_id, name=None): - """ Adds a user to the database using the provided arguments. """ - - return db_add(User(email, password, role_id, city_id, name)) - - -def question(name, total_score, type_id, slide_id): - """ - Adds a question to the specified slide using the provided arguments. - """ - - return db_add(Question(name, total_score, type_id, slide_id)) - - -def question_alternative(text, value, question_id): - return db_add(QuestionAlternative(text, value, question_id)) + if type_id == 1: + item = db_add(TextComponent(slide_id, type_id, x, y, w, h)) + item.text = data.get("text") + elif type_id == 2: + item = db_add(ImageComponent(slide_id, type_id, x, y, w, h)) + item.media_id = data.get("media_id") + else: + abort(codes.BAD_REQUEST, f"Invalid type_id{type_id}") - -def question_answer(data, score, question_id, team_id): - return db_add(QuestionAnswer(data, score, question_id, team_id)) + item = utils.commit_and_refresh(item) + return item def code(view_type_id, competition_id=None, team_id=None): @@ -235,3 +176,75 @@ def _competition_no_slides(name, year, city_id, font=None): item_competition = utils.refresh(item_competition) return item_competition + + +def blacklist(jti): + """ Adds a blacklist to the database. """ + + return db_add(Blacklist(jti)) + + +def mediaType(name): + """ Adds a media type to the database. """ + + return db_add(MediaType(name)) + + +def questionType(name): + """ Adds a question type to the database. """ + + return db_add(QuestionType(name)) + + +def componentType(name): + """ Adds a component type to the database. """ + + return db_add(ComponentType(name)) + + +def viewType(name): + """ Adds a view type to the database. """ + + return db_add(ViewType(name)) + + +def role(name): + """ Adds a role to the database. """ + + return db_add(Role(name)) + + +def city(name): + """ Adds a city to the database. """ + + return db_add(City(name)) + + +def image(filename, user_id): + """ + Adds an image to the database and keeps track of who called the function. + """ + + return db_add(Media(filename, 1, user_id)) + + +def user(email, password, role_id, city_id, name=None): + """ Adds a user to the database using the provided arguments. """ + + return db_add(User(email, password, role_id, city_id, name)) + + +def question(name, total_score, type_id, slide_id): + """ + Adds a question to the specified slide using the provided arguments. + """ + + return db_add(Question(name, total_score, type_id, slide_id)) + + +def question_alternative(text, value, question_id): + return db_add(QuestionAlternative(text, value, question_id)) + + +def question_answer(data, score, question_id, team_id): + return db_add(QuestionAnswer(data, score, question_id, team_id)) diff --git a/server/app/database/controller/copy.py b/server/app/database/controller/copy.py index 79a4df0f4a7f308b7bc40b5b9ab20e61423ffb14..87e3f831542aac9478566e916d12118f66a7c120 100644 --- a/server/app/database/controller/copy.py +++ b/server/app/database/controller/copy.py @@ -37,15 +37,19 @@ def _component(item_component, item_slide_new): Internal function. Makes a copy of the provided component item to the specified slide. """ - + data = {} + if item_component.type_id == 1: + data["text"] = item_component.text + elif item_component.type_id == 2: + data["media_id"] = item_component.media_id add.component( item_component.type_id, item_slide_new.id, - item_component.data, item_component.x, item_component.y, item_component.w, item_component.h, + **data, ) diff --git a/server/app/database/controller/get.py b/server/app/database/controller/get.py index 3e5e7f872c2866c70131dd1b4f1205c3bd131798..ba975d35432437746de480667bb8774267cc5437 100644 --- a/server/app/database/controller/get.py +++ b/server/app/database/controller/get.py @@ -2,17 +2,20 @@ This file contains functionality to get data from the database. """ +from sqlalchemy.orm.util import with_polymorphic from app.core import db from app.core import http_codes as codes from app.database.models import ( Code, Competition, Component, + ImageComponent, Question, QuestionAlternative, QuestionAnswer, Slide, Team, + TextComponent, User, ) from sqlalchemy.orm import joinedload, subqueryload @@ -216,7 +219,15 @@ def component(competition_id, slide_id, component_id): join_competition = Competition.id == Slide.competition_id join_slide = Slide.id == Component.slide_id filters = (Competition.id == competition_id) & (Slide.id == slide_id) & (Component.id == component_id) - return Component.query.join(Competition, join_competition).join(Slide, join_slide).filter(filters).first_extended() + + poly = with_polymorphic(Component, [TextComponent, ImageComponent]) + return ( + db.session.query(poly) + .join(Competition, join_competition) + .join(Slide, join_slide) + .filter(filters) + .first_extended() + ) def component_list(competition_id, slide_id): diff --git a/server/app/database/models.py b/server/app/database/models.py index 7174080b799eeea7d5956ca641c0c8fdca8d5ee2..1ed99d911700c64349816e937230f2cd0e90e437 100644 --- a/server/app/database/models.py +++ b/server/app/database/models.py @@ -2,6 +2,8 @@ from app.core import bcrypt, db from app.database import Dictionary from sqlalchemy.ext.hybrid import hybrid_method, hybrid_property +from app.database.types import ID_IMAGE_COMPONENT, ID_TEXT_COMPONENT + STRING_SIZE = 254 @@ -91,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") @@ -190,20 +194,36 @@ class Component(db.Model): y = db.Column(db.Integer, nullable=False, default=0) w = db.Column(db.Integer, nullable=False, default=1) h = db.Column(db.Integer, nullable=False, default=1) - data = db.Column(Dictionary()) + view_type_id = db.Column(db.Integer, db.ForeignKey("view_type.id"), nullable=True) slide_id = db.Column(db.Integer, db.ForeignKey("slide.id"), nullable=False) type_id = db.Column(db.Integer, db.ForeignKey("component_type.id"), nullable=False) - def __init__(self, slide_id, type_id, data, x=0, y=0, w=1, h=1): + __mapper_args__ = {"polymorphic_on": type_id} + + def __init__(self, slide_id, type_id, x=0, y=0, w=1, h=1): self.x = x self.y = y self.w = w self.h = h - self.data = data self.slide_id = slide_id self.type_id = type_id +class TextComponent(Component): + text = db.Column(db.Text, default="", nullable=False) + + # __tablename__ = None + __mapper_args__ = {"polymorphic_identity": ID_TEXT_COMPONENT} + + +class ImageComponent(Component): + media_id = db.Column(db.Integer, db.ForeignKey("media.id"), nullable=True) + media = db.relationship("Media", uselist=False) + + # __tablename__ = None + __mapper_args__ = {"polymorphic_identity": ID_IMAGE_COMPONENT} + + class Code(db.Model): id = db.Column(db.Integer, primary_key=True) code = db.Column(db.Text, unique=True) diff --git a/server/app/database/types.py b/server/app/database/types.py new file mode 100644 index 0000000000000000000000000000000000000000..236e50f5278d03684f2cbdcc05c2e7637e21a057 --- /dev/null +++ b/server/app/database/types.py @@ -0,0 +1,2 @@ +ID_TEXT_COMPONENT = 1 +ID_IMAGE_COMPONENT = 2 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 diff --git a/server/populate.py b/server/populate.py index cc19aa4cdd43190ac52c302f3029ade08b2a6c7a..6a4be4ef42cbe14d938466282c6db2addb92dc57 100644 --- a/server/populate.py +++ b/server/populate.py @@ -86,7 +86,7 @@ def _add_items(): y = random.randrange(1, 500) w = random.randrange(150, 400) h = random.randrange(150, 400) - dbc.add.component(1, item_slide.id, {"text": f"hej{k}"}, x, y, w, h) + dbc.add.component(1, item_slide.id, x, y, w, h, text=f"hej{k}") # item_slide = dbc.add.slide(item_comp) # item_slide.title = f"Slide {len(item_comp.slides)}" diff --git a/server/tests/test_db.py b/server/tests/test_db.py index cf10329ce6a320fdceb89ae9eba82da9e6b17092..dd13427c1c62689a88edefc96a6c16b8aa9db682 100644 --- a/server/tests/test_db.py +++ b/server/tests/test_db.py @@ -94,11 +94,13 @@ def check_slides_copy(item_slide_original, item_slide_copy, num_slides, order): assert c1.y == c2.y assert c1.w == c2.w assert c1.h == c2.h - assert c1.data == c2.data assert c1.slide_id == item_slide_original.id assert c2.slide_id == item_slide_copy.id assert c1.type_id == c2.type_id - + if c1.type_id == 1: + assert c1.text == c2.text + elif c1.type_id == 2: + assert c1.image_id == c2.image_id # Checks that all questions were correctly copied questions = item_slide_original.questions questions_copy = item_slide_copy.questions diff --git a/server/tests/test_helpers.py b/server/tests/test_helpers.py index cc630626822aa3358da79b7d9a9a132705982bdf..b5f1e54e136b759d9924a534acf2f37e6f7b08cd 100644 --- a/server/tests/test_helpers.py +++ b/server/tests/test_helpers.py @@ -59,7 +59,7 @@ def add_default_values(): # dbc.add.question(name=f"Q{i+1}", total_score=i + 1, type_id=1, slide_id=item_slide.id) # Add text component - dbc.add.component(1, item_slide.id, {"text": "Text"}, i, 2 * i, 3 * i, 4 * i) + dbc.add.component(1, item_slide.id, i, 2 * i, 3 * i, 4 * i, text="Text") def get_body(response):