From 7e769c25e3aea67388513ce74d2351cc3c46748d Mon Sep 17 00:00:00 2001 From: robban64 <carl@schonfelder.se> Date: Sat, 8 May 2021 21:39:42 +0200 Subject: [PATCH 1/5] fix: add docstrings and comments --- server/app/__init__.py | 7 ++ server/app/core/__init__.py | 9 +++ server/app/core/codes.py | 9 ++- server/app/core/files.py | 18 ++++- server/app/database/__init__.py | 25 +++++++ server/app/database/controller/add.py | 4 +- server/app/database/models.py | 102 +++++++++++++++++++++++++- server/app/database/types.py | 2 + 8 files changed, 166 insertions(+), 10 deletions(-) diff --git a/server/app/__init__.py b/server/app/__init__.py index 5bdffe82..5edf5d5d 100644 --- a/server/app/__init__.py +++ b/server/app/__init__.py @@ -12,11 +12,14 @@ def create_app(config_name="configmodule.DevelopmentConfig"): SocketIO instance and pass in the Flask app to start the server. """ + # Init flask app = Flask(__name__, static_url_path="/static", static_folder="static") app.config.from_object(config_name) app.url_map.strict_slashes = False + with app.app_context(): + # Init flask apps bcrypt.init_app(app) jwt.init_app(app) db.init_app(app) @@ -24,14 +27,18 @@ def create_app(config_name="configmodule.DevelopmentConfig"): ma.init_app(app) configure_uploads(app, (MediaDTO.image_set,)) + # Init socket from app.core.sockets import sio sio.init_app(app) + # Init api from app.apis import flask_api flask_api.init_app(app) + # Flask helpers methods + @app.before_request def clear_trailing(): rp = request.path diff --git a/server/app/core/__init__.py b/server/app/core/__init__.py index c80bb5c0..091a76c3 100644 --- a/server/app/core/__init__.py +++ b/server/app/core/__init__.py @@ -9,6 +9,7 @@ from flask_jwt_extended.jwt_manager import JWTManager from flask_marshmallow import Marshmallow from flask_sqlalchemy import SQLAlchemy +# Flask apps db = SQLAlchemy(model_class=Base, query_class=ExtendedQuery) bcrypt = Bcrypt() jwt = JWTManager() @@ -17,5 +18,13 @@ ma = Marshmallow() @jwt.token_in_blacklist_loader def check_if_token_in_blacklist(decrypted_token): + """ + An extension method with flask_jwt_extended that will execute when jwt verifies + Check if the token is blacklisted in the database + :param decrypted_token: jti or string of the jwt + :type decrypted_token: str + :return: True if token is blacklisted + :rtype: bool + """ jti = decrypted_token["jti"] return models.Blacklist.query.filter_by(jti=jti).first() is not None diff --git a/server/app/core/codes.py b/server/app/core/codes.py index c52ddf8d..f66bf1d6 100644 --- a/server/app/core/codes.py +++ b/server/app/core/codes.py @@ -11,13 +11,14 @@ CODE_RE = re.compile(f"^[{ALLOWED_CHARS}]{{{CODE_LENGTH}}}$") def generate_code_string() -> str: - """Generates a 6 character long random sequence containg uppercase letters - and numbers. + """ + Return a 6 character long random sequence containing uppercase letters and numbers. """ return "".join(random.choices(ALLOWED_CHARS, k=CODE_LENGTH)) def verify_code(code: str) -> bool: - """Returns True if code only contains letters and/or numbers - and is exactly 6 characters long.""" + """ + Returns True if code only contains letters and/or numbers and is exactly 6 characters long. + """ return CODE_RE.search(code.upper()) is not None diff --git a/server/app/core/files.py b/server/app/core/files.py index f45b9bb5..4f5256fb 100644 --- a/server/app/core/files.py +++ b/server/app/core/files.py @@ -2,10 +2,11 @@ Contains functions related to file handling, mainly saving and deleting images. """ -from PIL import Image -from flask import current_app, has_app_context import os + +from flask import current_app, has_app_context from flask_uploads import IMAGES, UploadSet +from PIL import Image if has_app_context(): PHOTO_PATH = current_app.config["UPLOADED_PHOTOS_DEST"] @@ -32,6 +33,13 @@ if has_app_context(): def _delete_image(filename): + """ + Private function + Delete an image with the given filename + :param filename: Of the image that will be deleted + :type filename: str + :rtype: None + """ path = os.path.join(PHOTO_PATH, filename) os.remove(path) @@ -39,6 +47,10 @@ def _delete_image(filename): def save_image_with_thumbnail(image_file): """ Saves the given image and also creates a small thumbnail for it. + :param image_file: Image object that will be saved on server + :type image_file: object + :return: Filename of the saved image, if filename already exist the filename will be changed + :rtype: str """ saved_filename = image_set.save(image_file) @@ -55,6 +67,8 @@ def save_image_with_thumbnail(image_file): def delete_image_and_thumbnail(filename): """ Delete the given image together with its thumbnail. + :param filename: + :type filename: """ _delete_image(filename) _delete_image(f"thumbnail_{filename}") diff --git a/server/app/database/__init__.py b/server/app/database/__init__.py index 2840c94e..5cf85a21 100644 --- a/server/app/database/__init__.py +++ b/server/app/database/__init__.py @@ -11,6 +11,10 @@ from sqlalchemy.sql import func class Base(Model): + """ + Abstract table/model that all tables inherit + """ + __abstract__ = True _created = Column(DateTime(timezone=True), server_default=func.now()) _updated = Column(DateTime(timezone=True), onupdate=func.now()) @@ -25,7 +29,17 @@ class ExtendedQuery(BaseQuery): """ Extensions of the first() functions otherwise used on queries. Abort if no item was found and it was required. + + :param required: Raise an exception if the query results in None + :type required: bool + :param error_message: The message that will be sent to the client with the exception + :type error_message:str + :param error_code: The status code that will be sent to the client with the exception + :type error_code: int + :return: + :rtype: """ + item = self.first() if required and not item: @@ -39,7 +53,18 @@ class ExtendedQuery(BaseQuery): """ When looking for lists of items this is used to only return a few of them to allow for pagination. + :param page: Offset of the result + :type page: int + :param page_size: Amount of rows that will be retrieved from the query + :type page_size: int + :param order_column: Field of a DbModel in which the query shall order by + :type order_column: sqlalchemy.sql.schema.Column + :param order: If equals 1 then order by ascending otherwise order by descending + :type order: int + :return: A page/list of items with offset page*page_size and the total count of all rows ignoring page and page_size + :rtype: list, int """ + query = self if order_column: if order == 1: diff --git a/server/app/database/controller/add.py b/server/app/database/controller/add.py index d0adf5a0..1560daa3 100644 --- a/server/app/database/controller/add.py +++ b/server/app/database/controller/add.py @@ -39,8 +39,8 @@ from sqlalchemy import exc def db_add(item): """ - Internal function. Adds item to the database - and handles comitting and refreshing. + Internal function that add item to the database. + Handle different types of errors that occur when inserting an item into the database by calling abort. """ try: db.session.add(item) diff --git a/server/app/database/models.py b/server/app/database/models.py index 97eb6097..9a6af127 100644 --- a/server/app/database/models.py +++ b/server/app/database/models.py @@ -8,10 +8,15 @@ from app.core import bcrypt, db from app.database.types import ID_IMAGE_COMPONENT, ID_QUESTION_COMPONENT, ID_TEXT_COMPONENT from sqlalchemy.ext.hybrid import hybrid_method, hybrid_property -STRING_SIZE = 254 +STRING_SIZE = 254 # Default size of string Columns (varchar) class Whitelist(db.Model): + """ + Table with allowed jwt. + Depend on table: User, Competition. + """ + id = db.Column(db.Integer, primary_key=True) jti = db.Column(db.String, unique=True) user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=True) @@ -24,6 +29,10 @@ class Whitelist(db.Model): class Blacklist(db.Model): + """ + Table with banned jwt. + """ + id = db.Column(db.Integer, primary_key=True) jti = db.Column(db.String, unique=True) @@ -32,6 +41,10 @@ class Blacklist(db.Model): class Role(db.Model): + """ + Table with roles: Admin and Editor. + """ + id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(STRING_SIZE), unique=True) @@ -41,8 +54,11 @@ class Role(db.Model): self.name = name -# TODO Region? class City(db.Model): + """ + Table with cities. + """ + id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(STRING_SIZE), unique=True) @@ -54,6 +70,11 @@ class City(db.Model): class User(db.Model): + """ + Table with users. + Depend on table: Role, City. + """ + id = db.Column(db.Integer, primary_key=True) email = db.Column(db.String(STRING_SIZE), unique=True) name = db.Column(db.String(STRING_SIZE), nullable=True) @@ -92,6 +113,11 @@ class User(db.Model): class Media(db.Model): + """ + Table with media objects that can be image or video depending on type_id. + Depend on table: MediaType, User. + """ + id = db.Column(db.Integer, primary_key=True) filename = db.Column(db.String(STRING_SIZE), unique=True) type_id = db.Column(db.Integer, db.ForeignKey("media_type.id"), nullable=False) @@ -104,6 +130,11 @@ class Media(db.Model): class Competition(db.Model): + """ + Table with competitions. + Depend on table: Media, City. + """ + id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(STRING_SIZE), unique=True) year = db.Column(db.Integer, nullable=False, default=2020) @@ -127,6 +158,11 @@ class Competition(db.Model): class Team(db.Model): + """ + Table with teams. + Depend on table: Competition. + """ + __table_args__ = (db.UniqueConstraint("competition_id", "name"),) id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(STRING_SIZE), nullable=False) @@ -141,6 +177,11 @@ class Team(db.Model): class Slide(db.Model): + """ + Table with slides. + Depend on table: Competition, Media. + """ + __table_args__ = (db.UniqueConstraint("order", "competition_id"),) id = db.Column(db.Integer, primary_key=True) order = db.Column(db.Integer, nullable=False) @@ -162,6 +203,11 @@ class Slide(db.Model): class Question(db.Model): + """ + Table with questions of different types depending on type_id + Depend on table: QuestionType, Slide. + """ + id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(STRING_SIZE), nullable=False) total_score = db.Column(db.Integer, nullable=False, default=1) @@ -181,6 +227,11 @@ class Question(db.Model): class QuestionAlternative(db.Model): + """ + Table with question alternatives. + Depend on table: Question. + """ + id = db.Column(db.Integer, primary_key=True) text = db.Column(db.String(STRING_SIZE), nullable=False) value = db.Column(db.Integer, nullable=False) @@ -193,6 +244,11 @@ class QuestionAlternative(db.Model): class QuestionAnswer(db.Model): + """ + Table with question answers. + Depend on table: Question, Team. + """ + __table_args__ = (db.UniqueConstraint("question_id", "team_id"),) id = db.Column(db.Integer, primary_key=True) answer = db.Column(db.String(STRING_SIZE), nullable=False) @@ -209,6 +265,11 @@ class QuestionAnswer(db.Model): class Component(db.Model): + """ + Table with components of different types depending on type_id. + Depend on table: ViewType, Slide, ComponentType, Media, Question. + """ + id = db.Column(db.Integer, primary_key=True) x = db.Column(db.Integer, nullable=False, default=0) y = db.Column(db.Integer, nullable=False, default=0) @@ -231,6 +292,10 @@ class Component(db.Model): class TextComponent(Component): + """ + Extended version of Component with text based components. + """ + text = db.Column(db.Text, default="", nullable=False) # __tablename__ = None @@ -238,6 +303,11 @@ class TextComponent(Component): class ImageComponent(Component): + """ + Extended version of Component with image based components. + Depend on table: Media. + """ + media_id = db.Column(db.Integer, db.ForeignKey("media.id"), nullable=True) media = db.relationship("Media", uselist=False) @@ -246,6 +316,11 @@ class ImageComponent(Component): class QuestionComponent(Component): + """ + Extended version of Component with question based components. + Depend on table: Question. + """ + question_id = db.Column(db.Integer, db.ForeignKey("question.id"), nullable=True) # __tablename__ = None @@ -253,6 +328,11 @@ class QuestionComponent(Component): class Code(db.Model): + """ + Table with codes. + Depend on table: ViewType, Competition, Team. + """ + id = db.Column(db.Integer, primary_key=True) code = db.Column(db.Text, unique=True) view_type_id = db.Column(db.Integer, db.ForeignKey("view_type.id"), nullable=False) @@ -268,7 +348,13 @@ class Code(db.Model): self.team_id = team_id +### Types ### + class ViewType(db.Model): + """ + Table with view types: Team, Judge, Audience and Operator. + """ + id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(STRING_SIZE), unique=True) @@ -277,6 +363,10 @@ class ViewType(db.Model): class ComponentType(db.Model): + """ + Table with component types: Text, Image and Question. + """ + id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(STRING_SIZE), unique=True) components = db.relationship("Component", backref="component_type") @@ -286,6 +376,10 @@ class ComponentType(db.Model): class MediaType(db.Model): + """ + Table with media types: Image and Video. + """ + id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(STRING_SIZE), unique=True) media = db.relationship("Media", backref="type") @@ -295,6 +389,10 @@ class MediaType(db.Model): class QuestionType(db.Model): + """ + Table with question types: Text, Practical, Multiple and Single. + """ + id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(STRING_SIZE), unique=True) questions = db.relationship("Question", backref="type") diff --git a/server/app/database/types.py b/server/app/database/types.py index 46db168c..4fa05222 100644 --- a/server/app/database/types.py +++ b/server/app/database/types.py @@ -2,6 +2,8 @@ This module defines the different component types. """ +# Todo set type_id from database on startup. + ID_TEXT_COMPONENT = 1 ID_IMAGE_COMPONENT = 2 ID_QUESTION_COMPONENT = 3 \ No newline at end of file -- GitLab From d014f4f7216a572e727edd9b25156645da05f938 Mon Sep 17 00:00:00 2001 From: robban64 <carl@schonfelder.se> Date: Sat, 8 May 2021 21:40:31 +0200 Subject: [PATCH 2/5] fix: rename ID to media_id in media.py --- server/app/apis/media.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/server/app/apis/media.py b/server/app/apis/media.py index c59589a6..c177ae16 100644 --- a/server/app/apis/media.py +++ b/server/app/apis/media.py @@ -54,21 +54,21 @@ class ImageList(Resource): api.abort(codes.INTERNAL_SERVER_ERROR, "Something went wrong when trying to save image") -@api.route("/images/<ID>") -@api.param("ID") +@api.route("/images/<media_id>") +@api.param("media_id") class ImageList(Resource): @protect_route(allowed_roles=["*"], allowed_views=["*"]) - def get(self, ID): + def get(self, media_id): """ Gets the specified image. """ - item = dbc.get.one(Media, ID) + item = dbc.get.one(Media, media_id) return item_response(schema.dump(item)) @protect_route(allowed_roles=["*"]) - def delete(self, ID): + def delete(self, media_id): """ Deletes the specified image. """ - item = dbc.get.one(Media, ID) + item = dbc.get.one(Media, media_id) try: files.delete_image_and_thumbnail(item.filename) dbc.delete.default(item) -- GitLab From 5487badee20911c321d5b17ad9de955024207316 Mon Sep 17 00:00:00 2001 From: robban64 <carl@schonfelder.se> Date: Sat, 8 May 2021 21:44:43 +0200 Subject: [PATCH 3/5] fix: rename first_extended -> first_api --- server/app/database/__init__.py | 2 +- server/app/database/controller/get.py | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/server/app/database/__init__.py b/server/app/database/__init__.py index 5cf85a21..e27d9b58 100644 --- a/server/app/database/__init__.py +++ b/server/app/database/__init__.py @@ -25,7 +25,7 @@ class ExtendedQuery(BaseQuery): Extensions to a regular query which makes using the database more convenient. """ - def first_extended(self, required=True, error_message=None, error_code=404): + def first_api(self, required=True, error_message=None, error_code=404): """ Extensions of the first() functions otherwise used on queries. Abort if no item was found and it was required. diff --git a/server/app/database/controller/get.py b/server/app/database/controller/get.py index abdb9e15..21abe22c 100644 --- a/server/app/database/controller/get.py +++ b/server/app/database/controller/get.py @@ -30,14 +30,14 @@ def all(db_type): def one(db_type, id, required=True): """ Get lazy db-item in the table that has the same id. """ - return db_type.query.filter(db_type.id == id).first_extended(required=required) + return db_type.query.filter(db_type.id == id).first_api(required=required) ### Codes ### def code_by_code(code): """ Gets the code object associated with the provided code. """ - return Code.query.filter(Code.code == code.upper()).first_extended( + return Code.query.filter(Code.code == code.upper()).first_api( True, "A presentation with that code does not exist" ) @@ -63,7 +63,7 @@ def user_exists(email): def user_by_email(email): """ Gets the user object associated with the provided email. """ - return User.query.filter(User.email == email).first_extended(error_code=codes.UNAUTHORIZED) + return User.query.filter(User.email == email).first_api(error_code=codes.UNAUTHORIZED) ### Slides ### @@ -75,7 +75,7 @@ def slide(competition_id, slide_id): join_competition = Competition.id == Slide.competition_id filters = (Competition.id == competition_id) & (Slide.id == slide_id) - return Slide.query.join(Competition, join_competition).filter(filters).first_extended() + return Slide.query.join(Competition, join_competition).filter(filters).first_api() def slide_list(competition_id): @@ -102,7 +102,7 @@ def team(competition_id, team_id): join_competition = Competition.id == Team.competition_id filters = (Competition.id == competition_id) & (Team.id == team_id) - return Team.query.join(Competition, join_competition).filter(filters).first_extended() + return Team.query.join(Competition, join_competition).filter(filters).first_api() def team_list(competition_id): @@ -127,7 +127,7 @@ def question(competition_id, slide_id, question_id): join_slide = Slide.id == Question.slide_id filters = (Competition.id == competition_id) & (Slide.id == slide_id) & (Question.id == question_id) - return Question.query.join(Competition, join_competition).join(Slide, join_slide).filter(filters).first_extended() + return Question.query.join(Competition, join_competition).join(Slide, join_slide).filter(filters).first_api() def question_list(competition_id, slide_id): @@ -183,7 +183,7 @@ def question_alternative( .join(Slide, join_slide) .join(Question, join_question) .filter(filters) - .first_extended() + .first_api() ) @@ -217,7 +217,7 @@ def question_answer(competition_id, team_id, answer_id): join_team = Team.id == QuestionAnswer.team_id filters = (Competition.id == competition_id) & (Team.id == team_id) & (QuestionAnswer.id == answer_id) return ( - QuestionAnswer.query.join(Competition, join_competition).join(Team, join_team).filter(filters).first_extended() + QuestionAnswer.query.join(Competition, join_competition).join(Team, join_team).filter(filters).first_api() ) @@ -249,7 +249,7 @@ def component(competition_id, slide_id, component_id): .join(Competition, join_competition) .join(Slide, join_slide) .filter(filters) - .first_extended() + .first_api() ) -- GitLab From 2e6294aafc8c62f2cdbd90a1449100435a4b1209 Mon Sep 17 00:00:00 2001 From: robban64 <carl@schonfelder.se> Date: Sat, 8 May 2021 21:49:47 +0200 Subject: [PATCH 4/5] fix: rename _add_items -> create_default_items in populate.py --- server/populate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/populate.py b/server/populate.py index 9981f0c0..06f3275b 100644 --- a/server/populate.py +++ b/server/populate.py @@ -9,7 +9,7 @@ from app import create_app, db from app.database.models import City, QuestionType, Role -def _add_items(): +def create_default_items(): media_types = ["Image", "Video"] question_types = ["Text", "Practical", "Multiple", "Single"] component_types = ["Text", "Image", "Question"] @@ -121,4 +121,4 @@ if __name__ == "__main__": db.drop_all() db.create_all() - _add_items() + create_default_items() -- GitLab From 19928b47fc482450447ed50b967461b56aaa1325 Mon Sep 17 00:00:00 2001 From: robban64 <carl@schonfelder.se> Date: Sat, 8 May 2021 22:44:56 +0200 Subject: [PATCH 5/5] add: const for all type_id and comments --- server/app/core/schemas.py | 4 +-- server/app/database/controller/add.py | 8 +++--- server/app/database/controller/copy.py | 8 +++--- server/app/database/models.py | 38 ++++++++++++++++++-------- server/app/database/types.py | 24 +++++++++++++--- 5 files changed, 56 insertions(+), 26 deletions(-) diff --git a/server/app/core/schemas.py b/server/app/core/schemas.py index d9331ae7..f2da6300 100644 --- a/server/app/core/schemas.py +++ b/server/app/core/schemas.py @@ -1,5 +1,5 @@ """ -This module contains schemas used to convert database objects into +This module contains marshmallow schemas used to convert database objects into dictionaries. """ @@ -169,4 +169,4 @@ class ComponentSchema(BaseSchema): text = fields.fields.String() media = fields.Nested(MediaSchema, many=False) - question_id = fields.fields.Integer() \ No newline at end of file + question_id = fields.fields.Integer() diff --git a/server/app/database/controller/add.py b/server/app/database/controller/add.py index 1560daa3..e649a7ca 100644 --- a/server/app/database/controller/add.py +++ b/server/app/database/controller/add.py @@ -29,7 +29,7 @@ from app.database.models import ( ViewType, Whitelist, ) -from app.database.types import ID_IMAGE_COMPONENT, ID_QUESTION_COMPONENT, ID_TEXT_COMPONENT +from app.database.types import IMAGE_COMPONENT_ID, QUESTION_COMPONENT_ID, TEXT_COMPONENT_ID from flask import current_app from flask.globals import current_app from flask_restx import abort @@ -87,17 +87,17 @@ def component(type_id, slide_id, view_type_id, x=0, y=0, w=0, h=0, **data): w *= ratio h *= ratio - if type_id == ID_TEXT_COMPONENT: + if type_id == TEXT_COMPONENT_ID: item = db_add( TextComponent(slide_id, type_id, view_type_id, x, y, w, h), ) item.text = data.get("text") - elif type_id == ID_IMAGE_COMPONENT: + elif type_id == IMAGE_COMPONENT_ID: item = db_add( ImageComponent(slide_id, type_id, view_type_id, x, y, w, h), ) item.media_id = data.get("media_id") - elif type_id == ID_QUESTION_COMPONENT: + elif type_id == QUESTION_COMPONENT_ID: item = db_add( QuestionComponent(slide_id, type_id, view_type_id, x, y, w, h), ) diff --git a/server/app/database/controller/copy.py b/server/app/database/controller/copy.py index dd807334..8a91db9b 100644 --- a/server/app/database/controller/copy.py +++ b/server/app/database/controller/copy.py @@ -4,7 +4,7 @@ This file contains functionality to copy and duplicate data to the database. from app.database.controller import add, get, search, utils from app.database.models import Question -from app.database.types import ID_IMAGE_COMPONENT, ID_QUESTION_COMPONENT, ID_TEXT_COMPONENT +from app.database.types import IMAGE_COMPONENT_ID, QUESTION_COMPONENT_ID, TEXT_COMPONENT_ID def _alternative(item_old, question_id): @@ -53,11 +53,11 @@ def component(item_component, slide_id_new, view_type_id): """ data = {} - if item_component.type_id == ID_TEXT_COMPONENT: + if item_component.type_id == TEXT_COMPONENT_ID: data["text"] = item_component.text - elif item_component.type_id == ID_IMAGE_COMPONENT: + elif item_component.type_id == IMAGE_COMPONENT_ID: data["media_id"] = item_component.media_id - elif item_component.type_id == ID_QUESTION_COMPONENT: + elif item_component.type_id == QUESTION_COMPONENT_ID: data["question_id"] = item_component.question_id return add.component( diff --git a/server/app/database/models.py b/server/app/database/models.py index 9a6af127..e0bdac86 100644 --- a/server/app/database/models.py +++ b/server/app/database/models.py @@ -5,15 +5,16 @@ each other. """ from app.core import bcrypt, db -from app.database.types import ID_IMAGE_COMPONENT, ID_QUESTION_COMPONENT, ID_TEXT_COMPONENT +from app.database.types import IMAGE_COMPONENT_ID, QUESTION_COMPONENT_ID, TEXT_COMPONENT_ID from sqlalchemy.ext.hybrid import hybrid_method, hybrid_property -STRING_SIZE = 254 # Default size of string Columns (varchar) +STRING_SIZE = 254 # Default size of string Columns (varchar) class Whitelist(db.Model): """ Table with allowed jwt. + Depend on table: User, Competition. """ @@ -72,6 +73,7 @@ class City(db.Model): class User(db.Model): """ Table with users. + Depend on table: Role, City. """ @@ -115,6 +117,7 @@ class User(db.Model): class Media(db.Model): """ Table with media objects that can be image or video depending on type_id. + Depend on table: MediaType, User. """ @@ -132,6 +135,7 @@ class Media(db.Model): class Competition(db.Model): """ Table with competitions. + Depend on table: Media, City. """ @@ -160,6 +164,7 @@ class Competition(db.Model): class Team(db.Model): """ Table with teams. + Depend on table: Competition. """ @@ -179,6 +184,7 @@ class Team(db.Model): class Slide(db.Model): """ Table with slides. + Depend on table: Competition, Media. """ @@ -205,6 +211,7 @@ class Slide(db.Model): class Question(db.Model): """ Table with questions of different types depending on type_id + Depend on table: QuestionType, Slide. """ @@ -229,6 +236,7 @@ class Question(db.Model): class QuestionAlternative(db.Model): """ Table with question alternatives. + Depend on table: Question. """ @@ -246,7 +254,10 @@ class QuestionAlternative(db.Model): class QuestionAnswer(db.Model): """ Table with question answers. + Depend on table: Question, Team. + + Unique Constraint: Question.id, Team.id. """ __table_args__ = (db.UniqueConstraint("question_id", "team_id"),) @@ -266,8 +277,10 @@ class QuestionAnswer(db.Model): class Component(db.Model): """ - Table with components of different types depending on type_id. - Depend on table: ViewType, Slide, ComponentType, Media, Question. + Table implemented with single table inheritance where each subclass is linked to a specific type_id + This class/table contains all fields from every subclass. + + Depend on table: :ViewType, Slide, ComponentType, Media, Question. """ id = db.Column(db.Integer, primary_key=True) @@ -293,18 +306,19 @@ class Component(db.Model): class TextComponent(Component): """ - Extended version of Component with text based components. + Subclass of Component that contains text. """ text = db.Column(db.Text, default="", nullable=False) # __tablename__ = None - __mapper_args__ = {"polymorphic_identity": ID_TEXT_COMPONENT} + __mapper_args__ = {"polymorphic_identity": TEXT_COMPONENT_ID} class ImageComponent(Component): """ - Extended version of Component with image based components. + Subclass of Component that contains an image. + Depend on table: Media. """ @@ -312,24 +326,26 @@ class ImageComponent(Component): media = db.relationship("Media", uselist=False) # __tablename__ = None - __mapper_args__ = {"polymorphic_identity": ID_IMAGE_COMPONENT} + __mapper_args__ = {"polymorphic_identity": IMAGE_COMPONENT_ID} class QuestionComponent(Component): """ - Extended version of Component with question based components. + Subclass of Component that contains a question. + Depend on table: Question. """ question_id = db.Column(db.Integer, db.ForeignKey("question.id"), nullable=True) # __tablename__ = None - __mapper_args__ = {"polymorphic_identity": ID_QUESTION_COMPONENT} + __mapper_args__ = {"polymorphic_identity": QUESTION_COMPONENT_ID} class Code(db.Model): """ Table with codes. + Depend on table: ViewType, Competition, Team. """ @@ -348,8 +364,6 @@ class Code(db.Model): self.team_id = team_id -### Types ### - class ViewType(db.Model): """ Table with view types: Team, Judge, Audience and Operator. diff --git a/server/app/database/types.py b/server/app/database/types.py index 4fa05222..e60dcacc 100644 --- a/server/app/database/types.py +++ b/server/app/database/types.py @@ -2,8 +2,24 @@ This module defines the different component types. """ -# Todo set type_id from database on startup. +# Store all type ID in cache instead of here. -ID_TEXT_COMPONENT = 1 -ID_IMAGE_COMPONENT = 2 -ID_QUESTION_COMPONENT = 3 \ No newline at end of file +IMAGE_MEDIA_ID = 1 +VIDEO_MEDIA_ID = 2 + +TEXT_QUESTION_ID = 1 +PRACTICAL_QUESTION_ID = 2 +MULTIPLE_QUESTION_ID = 3 +SINGLE_QUESTION_ID = 4 + +TEAM_VIEW_ID = 1 +JUDGE_VIEW_ID = 2 +AUDIENCE_VIEW_ID = 3 +OPERATOR_VIEW_ID = 4 + +ADMIN_ROLE_ID = 1 +EDITOR_ROLE_ID = 2 + +TEXT_COMPONENT_ID = 1 +IMAGE_COMPONENT_ID = 2 +QUESTION_COMPONENT_ID = 3 -- GitLab