From 226448fdea4dce99b5adb52ad9875c47b8beca5d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Carl=20Sch=C3=B6nfelder?= <carsc272@student.liu.se>
Date: Tue, 13 Apr 2021 13:04:31 +0000
Subject: [PATCH] Resolve "Backend fixes"

---
 server/app/__init__.py                        |  2 +-
 server/app/apis/__init__.py                   |  1 +
 server/app/apis/auth.py                       | 19 ++---
 server/app/apis/competitions.py               | 18 ++---
 server/app/apis/misc.py                       |  4 +-
 server/app/apis/questions.py                  | 31 ++------
 server/app/apis/slides.py                     | 23 +++---
 server/app/apis/teams.py                      | 18 ++---
 server/app/apis/users.py                      | 14 ++--
 server/app/core/__init__.py                   | 13 +--
 server/app/core/rich_schemas.py               | 32 +++++++-
 server/app/core/schemas.py                    | 13 ++-
 server/app/database/__init__.py               |  0
 server/app/database/base.py                   | 37 +++++++++
 .../{core => database}/controller/__init__.py |  2 +-
 .../app/{core => database}/controller/add.py  | 19 ++++-
 .../{core => database}/controller/delete.py   |  6 +-
 .../app/{core => database}/controller/edit.py |  0
 server/app/database/controller/get.py         | 56 +++++++++++++
 .../get.py => database/controller/search.py}  | 79 ++++++-------------
 server/app/{core => database}/models.py       |  3 +-
 server/tests/test_app.py                      | 14 ++--
 server/tests/test_db.py                       |  6 +-
 server/tests/test_helpers.py                  |  4 +-
 24 files changed, 243 insertions(+), 171 deletions(-)
 create mode 100644 server/app/database/__init__.py
 create mode 100644 server/app/database/base.py
 rename server/app/{core => database}/controller/__init__.py (76%)
 rename server/app/{core => database}/controller/add.py (77%)
 rename server/app/{core => database}/controller/delete.py (86%)
 rename server/app/{core => database}/controller/edit.py (100%)
 create mode 100644 server/app/database/controller/get.py
 rename server/app/{core/controller/get.py => database/controller/search.py} (58%)
 rename server/app/{core => database}/models.py (99%)

diff --git a/server/app/__init__.py b/server/app/__init__.py
index d1bbd3af..48a104f8 100644
--- a/server/app/__init__.py
+++ b/server/app/__init__.py
@@ -1,6 +1,6 @@
 from flask import Flask, redirect, request
 
-import app.core.models as models
+import app.database.models as models
 from app.core import bcrypt, db, jwt, ma
 
 
diff --git a/server/app/apis/__init__.py b/server/app/apis/__init__.py
index 061a4519..996c14be 100644
--- a/server/app/apis/__init__.py
+++ b/server/app/apis/__init__.py
@@ -58,3 +58,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")
diff --git a/server/app/apis/auth.py b/server/app/apis/auth.py
index df3a8e5c..1510df64 100644
--- a/server/app/apis/auth.py
+++ b/server/app/apis/auth.py
@@ -1,9 +1,9 @@
-import app.core.controller as dbc
 import app.core.http_codes as codes
+import app.database.controller as dbc
 from app.apis import admin_required, item_response, text_response
 from app.core.dto import AuthDTO
-from app.core.models import User
 from app.core.parsers import create_user_parser, login_parser
+from app.database.models import User
 from flask_jwt_extended import (
     create_access_token,
     create_refresh_token,
@@ -30,14 +30,10 @@ class AuthSignup(Resource):
         args = create_user_parser.parse_args(strict=True)
         email = args.get("email")
 
-        if User.query.filter(User.email == email).count() > 0:
+        if dbc.get.user_exists(email):
             api.abort(codes.BAD_REQUEST, "User already exists")
 
         item_user = dbc.add.user(**args)
-        # TODO: Clarify when this case is needed or add it to a test
-        if not item_user:
-            api.abort(codes.BAD_REQUEST, "User could not be created")
-
         return item_response(schema.dump(item_user))
 
 
@@ -46,10 +42,7 @@ class AuthSignup(Resource):
 class AuthDelete(Resource):
     @jwt_required
     def delete(self, ID):
-        item_user = User.query.filter(User.id == ID).first()
-
-        if not item_user:
-            api.abort(codes.NOT_FOUND, f"Could not find user with id {ID}.")
+        item_user = dbc.get.user(ID)
 
         dbc.delete.default(item_user)
         if int(ID) == get_jwt_identity():
@@ -64,7 +57,7 @@ class AuthLogin(Resource):
         args = login_parser.parse_args(strict=True)
         email = args.get("email")
         password = args.get("password")
-        item_user = User.query.filter_by(email=email).first()
+        item_user = dbc.get.user_by_email(email, required=False)
 
         if not item_user or not item_user.is_correct_password(password):
             api.abort(codes.UNAUTHORIZED, "Invalid email or password")
@@ -92,7 +85,7 @@ class AuthRefresh(Resource):
     def post(self):
         old_jti = get_raw_jwt()["jti"]
 
-        item_user = User.query.filter_by(id=get_jwt_identity()).first()
+        item_user = dbc.get.user(get_jwt_identity())
         access_token = create_access_token(item_user.id, user_claims=get_user_claims(item_user))
         dbc.add.blacklist(old_jti)
         response = {"access_token": access_token}
diff --git a/server/app/apis/competitions.py b/server/app/apis/competitions.py
index 93f03227..9e87f93a 100644
--- a/server/app/apis/competitions.py
+++ b/server/app/apis/competitions.py
@@ -1,8 +1,8 @@
-import app.core.controller as dbc
+import app.database.controller as dbc
 from app.apis import admin_required, item_response, list_response
 from app.core.dto import CompetitionDTO
-from app.core.models import Competition
 from app.core.parsers import competition_parser, competition_search_parser
+from app.database.models import Competition
 from flask_jwt_extended import jwt_required
 from flask_restx import Resource
 
@@ -11,10 +11,6 @@ schema = CompetitionDTO.schema
 list_schema = CompetitionDTO.list_schema
 
 
-def get_comp(CID):
-    return Competition.query.filter(Competition.id == CID).first()
-
-
 @api.route("/")
 class CompetitionsList(Resource):
     @jwt_required
@@ -34,20 +30,22 @@ class CompetitionsList(Resource):
 class Competitions(Resource):
     @jwt_required
     def get(self, CID):
-        item = get_comp(CID)
+        item = dbc.get.competition(CID)
         return item_response(schema.dump(item))
 
     @jwt_required
     def put(self, CID):
         args = competition_parser.parse_args(strict=True)
-        item = get_comp(CID)
+        item = dbc.get.competition(CID)
         item = dbc.edit.competition(item, **args)
+
         return item_response(schema.dump(item))
 
     @jwt_required
     def delete(self, CID):
-        item = get_comp(CID)
+        item = dbc.get.competition(CID)
         dbc.delete.competition(item)
+
         return "deleted"
 
 
@@ -56,5 +54,5 @@ class CompetitionSearch(Resource):
     @jwt_required
     def get(self):
         args = competition_search_parser.parse_args(strict=True)
-        items, total = dbc.get.search_competitions(**args)
+        items, total = dbc.search.user(**args)
         return list_response(list_schema.dump(items), total)
diff --git a/server/app/apis/misc.py b/server/app/apis/misc.py
index f5b42fda..7fbb222d 100644
--- a/server/app/apis/misc.py
+++ b/server/app/apis/misc.py
@@ -1,7 +1,7 @@
-import app.core.controller as dbc
+import app.database.controller as dbc
 from app.apis import admin_required, item_response, list_response
 from app.core.dto import MiscDTO
-from app.core.models import City, MediaType, QuestionType, Role
+from app.database.models import City, MediaType, QuestionType, Role
 from flask_jwt_extended import jwt_required
 from flask_restx import Resource, reqparse
 
diff --git a/server/app/apis/questions.py b/server/app/apis/questions.py
index 76ca53f9..86929bb4 100644
--- a/server/app/apis/questions.py
+++ b/server/app/apis/questions.py
@@ -1,12 +1,11 @@
-import app.core.controller as dbc
 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.controller.add import competition
 from app.core.dto import QuestionDTO
-from app.core.models import Question
 from app.core.parsers import question_parser
-from flask_jwt_extended import get_jwt_identity, jwt_required
-from flask_restx import Namespace, Resource
+from app.database.models import Question
+from flask_jwt_extended import jwt_required
+from flask_restx import Resource
 
 api = QuestionDTO.api
 schema = QuestionDTO.schema
@@ -18,8 +17,8 @@ list_schema = QuestionDTO.list_schema
 class QuestionsList(Resource):
     @jwt_required
     def get(self, CID):
-        items, total = dbc.get.search_questions(competition_id=CID)
-        return list_response(list_schema.dump(items), total)
+        items = dbc.get.question_list(CID)
+        return list_response(list_schema.dump(items))
 
     @jwt_required
     def post(self, CID):
@@ -41,25 +40,14 @@ class QuestionsList(Resource):
 class Questions(Resource):
     @jwt_required
     def get(self, CID, QID):
-        item_question = Question.query.filter(Question.id == QID).first()
-
-        if item_question is None:
-            api.abort(codes.NOT_FOUND, f"Could not find question with id {QID}.")
-
-        if item_question.slide.competition.id != int(CID):
-            api.abort(codes.NOT_FOUND, f"Could not find question with id {QID} in competition with id {CID}.")
-
+        item_question = dbc.get.question(CID, QID)
         return item_response(schema.dump(item_question))
 
     @jwt_required
     def put(self, CID, QID):
         args = question_parser.parse_args(strict=True)
-        print(f"questions 54: {args=}")
-
-        item_question = Question.query.filter(Question.id == QID).first()
-        if item_question.slide.competition.id != int(CID):
-            api.abort(codes.NOT_FOUND, f"Could not find question with id {QID} in competition with id {CID}.")
 
+        item_question = dbc.get.question(CID, QID)
         item_question = dbc.edit.question(item_question, **args)
 
         return item_response(schema.dump(item_question))
@@ -67,8 +55,5 @@ class Questions(Resource):
     @jwt_required
     def delete(self, CID, QID):
         item_question = dbc.get.question(CID, QID)
-        if not item_question:
-            return {"response": "No content found"}, codes.NOT_FOUND
-
         dbc.delete.question(item_question)
         return {}, codes.NO_CONTENT
diff --git a/server/app/apis/slides.py b/server/app/apis/slides.py
index ab7caa59..972e1bd6 100644
--- a/server/app/apis/slides.py
+++ b/server/app/apis/slides.py
@@ -1,9 +1,9 @@
-import app.core.controller as dbc
 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 SlideDTO
-from app.core.models import Competition, Slide
 from app.core.parsers import slide_parser
+from app.database.models import Competition, Slide
 from flask_jwt_extended import jwt_required
 from flask_restx import Resource
 
@@ -12,22 +12,19 @@ schema = SlideDTO.schema
 list_schema = SlideDTO.list_schema
 
 
-def get_comp(CID):
-    return Competition.query.filter(Competition.id == CID).first()
-
-
 @api.route("/")
 @api.param("CID")
 class SlidesList(Resource):
     @jwt_required
     def get(self, CID):
-        item_comp = get_comp(CID)
-        return list_response(list_schema.dump(item_comp.slides))
+        items = dbc.get.slide_list(CID)
+        return list_response(list_schema.dump(items))
 
     @jwt_required
     def post(self, CID):
-        item_comp = get_comp(CID)
-        dbc.add.slide(item_comp)
+        item_comp = dbc.get.competition(CID)
+        item_slide = dbc.add.slide(item_comp)
+        dbc.add.question(f"Fråga {item_slide.order + 1}", 10, 0, item_slide)
         dbc.refresh(item_comp)
         return list_response(list_schema.dump(item_comp.slides))
 
@@ -54,8 +51,6 @@ class Slides(Resource):
     @jwt_required
     def delete(self, CID, SID):
         item_slide = dbc.get.slide(CID, SID)
-        if not item_slide:
-            return {"response": "No content found"}, codes.NOT_FOUND
 
         dbc.delete.slide(item_slide)
         return {}, codes.NO_CONTENT
@@ -72,10 +67,10 @@ class SlidesOrder(Resource):
         item_slide = dbc.get.slide(CID, SID)
 
         if order == item_slide.order:
-            api.abort(codes.BAD_REQUEST)
+            return item_response(schema.dump(item_slide))
 
         # clamp order between 0 and max
-        order_count = Slide.query.filter(Slide.competition_id == item_slide.competition_id).count()
+        order_count = dbc.get.slide_count(CID)
         if order < 0:
             order = 0
         elif order >= order_count - 1:
diff --git a/server/app/apis/teams.py b/server/app/apis/teams.py
index 7f7fdf0e..9600e2a4 100644
--- a/server/app/apis/teams.py
+++ b/server/app/apis/teams.py
@@ -1,9 +1,9 @@
-import app.core.controller as dbc
 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 TeamDTO
-from app.core.models import Competition, Team
 from app.core.parsers import team_parser
+from app.database.models import Competition, Team
 from flask_jwt_extended import get_jwt_identity, jwt_required
 from flask_restx import Namespace, Resource, reqparse
 
@@ -12,22 +12,18 @@ schema = TeamDTO.schema
 list_schema = TeamDTO.list_schema
 
 
-def get_comp(CID):
-    return Competition.query.filter(Competition.id == CID).first()
-
-
 @api.route("/")
 @api.param("CID")
 class TeamsList(Resource):
     @jwt_required
     def get(self, CID):
-        item_comp = get_comp(CID)
-        return list_response(list_schema.dump(item_comp.teams))
+        items = dbc.get.team_list(CID)
+        return list_response(list_schema.dump(items))
 
     @jwt_required
     def post(self, CID):
         args = team_parser.parse_args(strict=True)
-        item_comp = get_comp(CID)
+        item_comp = dbc.get.competition(CID)
         item_team = dbc.add.team(args["name"], item_comp)
         return item_response(schema.dump(item_team))
 
@@ -43,8 +39,6 @@ class Teams(Resource):
     @jwt_required
     def delete(self, CID, TID):
         item_team = dbc.get.team(CID, TID)
-        if not item_team:
-            api.abort(codes.NOT_FOUND, f"Could not find team with id {TID} in competition with id {CID}.")
 
         dbc.delete.team(item_team)
         return {}, codes.NO_CONTENT
@@ -55,8 +49,6 @@ class Teams(Resource):
         name = args.get("name")
 
         item_team = dbc.get.team(CID, TID)
-        if not item_team:
-            api.abort(codes.NOT_FOUND, f"Could not find team with id {TID} in competition with id {CID}.")
 
         item_team = dbc.edit.team(item_team, name=name, competition_id=CID)
         return item_response(schema.dump(item_team))
diff --git a/server/app/apis/users.py b/server/app/apis/users.py
index 07e2484d..28642b0d 100644
--- a/server/app/apis/users.py
+++ b/server/app/apis/users.py
@@ -1,9 +1,9 @@
-import app.core.controller as dbc
 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 UserDTO
-from app.core.models import User
 from app.core.parsers import user_parser, user_search_parser
+from app.database.models import User
 from flask import request
 from flask_jwt_extended import get_jwt_identity, jwt_required
 from flask_restx import Namespace, Resource
@@ -26,13 +26,13 @@ def edit_user(item_user, args):
 class UsersList(Resource):
     @jwt_required
     def get(self):
-        item = User.query.filter(User.id == get_jwt_identity()).first()
+        item = dbc.get.user(get_jwt_identity())
         return item_response(schema.dump(item))
 
     @jwt_required
     def put(self):
         args = user_parser.parse_args(strict=True)
-        item = User.query.filter(User.id == get_jwt_identity()).first()
+        item = dbc.get.user(get_jwt_identity())
         item = edit_user(item, args)
         return item_response(schema.dump(item))
 
@@ -42,13 +42,13 @@ class UsersList(Resource):
 class Users(Resource):
     @jwt_required
     def get(self, ID):
-        item = User.query.filter(User.id == ID).first()
+        item = dbc.get.user(ID)
         return item_response(schema.dump(item))
 
     @jwt_required
     def put(self, ID):
         args = user_parser.parse_args(strict=True)
-        item = User.query.filter(User.id == ID).first()
+        item = dbc.get.user(ID)
         item = edit_user(item, args)
         return item_response(schema.dump(item))
 
@@ -58,5 +58,5 @@ class UserSearch(Resource):
     @jwt_required
     def get(self):
         args = user_search_parser.parse_args(strict=True)
-        items, total = dbc.get.search_user(**args)
+        items, total = dbc.search.user(**args)
         return list_response(list_schema.dump(items), total)
diff --git a/server/app/core/__init__.py b/server/app/core/__init__.py
index e9d132cb..09c321ef 100644
--- a/server/app/core/__init__.py
+++ b/server/app/core/__init__.py
@@ -1,19 +1,10 @@
-import sqlalchemy as sa
+from app.database.base import Base, ExtendedQuery
 from flask_bcrypt import Bcrypt
 from flask_jwt_extended.jwt_manager import JWTManager
 from flask_marshmallow import Marshmallow
 from flask_sqlalchemy import SQLAlchemy
-from flask_sqlalchemy.model import Model
-from sqlalchemy.sql import func
 
-
-class Base(Model):
-    __abstract__ = True
-    _created = sa.Column(sa.DateTime(timezone=True), server_default=func.now())
-    _updated = sa.Column(sa.DateTime(timezone=True), onupdate=func.now())
-
-
-db = SQLAlchemy(model_class=Base)
+db = SQLAlchemy(model_class=Base, query_class=ExtendedQuery)
 bcrypt = Bcrypt()
 jwt = JWTManager()
 ma = Marshmallow()
diff --git a/server/app/core/rich_schemas.py b/server/app/core/rich_schemas.py
index d714d76d..ab1b3abb 100644
--- a/server/app/core/rich_schemas.py
+++ b/server/app/core/rich_schemas.py
@@ -1,5 +1,5 @@
-import app.core.models as models
 import app.core.schemas as schemas
+import app.database.models as models
 from app.core import ma
 from marshmallow_sqlalchemy import fields
 
@@ -29,8 +29,30 @@ class QuestionSchemaRich(RichSchema):
     id = ma.auto_field()
     name = ma.auto_field()
     total_score = ma.auto_field()
+    slide_id = ma.auto_field()
     type = fields.Nested(schemas.QuestionTypeSchema, many=False)
-    slide = fields.Nested(schemas.SlideSchema, many=False)
+
+
+class TeamSchemaRich(RichSchema):
+    class Meta(RichSchema.Meta):
+        model = models.Team
+
+    id = ma.auto_field()
+    name = ma.auto_field()
+    competition_id = ma.auto_field()
+    question_answers = fields.Nested(schemas.QuestionAnswerSchema, many=True)
+
+
+class SlideSchemaRich(RichSchema):
+    class Meta(RichSchema.Meta):
+        model = models.Slide
+
+    id = ma.auto_field()
+    order = ma.auto_field()
+    title = ma.auto_field()
+    timer = ma.auto_field()
+    competition_id = ma.auto_field()
+    questions = fields.Nested(QuestionSchemaRich, many=True)
 
 
 class CompetitionSchemaRich(RichSchema):
@@ -40,5 +62,9 @@ class CompetitionSchemaRich(RichSchema):
     id = ma.auto_field()
     name = ma.auto_field()
     year = ma.auto_field()
-    slides = fields.Nested(schemas.SlideSchema, many=True)
     city = fields.Nested(schemas.CitySchema, many=False)
+    slides = fields.Nested(
+        SlideSchemaRich,
+        many=True,
+    )
+    teams = fields.Nested(TeamSchemaRich, many=True)
diff --git a/server/app/core/schemas.py b/server/app/core/schemas.py
index c1d91c0d..27440642 100644
--- a/server/app/core/schemas.py
+++ b/server/app/core/schemas.py
@@ -1,4 +1,4 @@
-import app.core.models as models
+import app.database.models as models
 from app.core import ma
 from marshmallow_sqlalchemy import fields
 
@@ -29,6 +29,17 @@ class QuestionSchema(BaseSchema):
     slide_id = ma.auto_field()
 
 
+class QuestionAnswerSchema(BaseSchema):
+    class Meta(BaseSchema.Meta):
+        model = models.QuestionAnswer
+
+    id = ma.auto_field()
+    data = ma.auto_field()
+    score = ma.auto_field()
+    question_id = ma.auto_field()
+    team_id = ma.auto_field()
+
+
 class MediaTypeSchema(BaseSchema):
     class Meta(BaseSchema.Meta):
         model = models.MediaType
diff --git a/server/app/database/__init__.py b/server/app/database/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/server/app/database/base.py b/server/app/database/base.py
new file mode 100644
index 00000000..7a16a752
--- /dev/null
+++ b/server/app/database/base.py
@@ -0,0 +1,37 @@
+import app.core.http_codes as codes
+import sqlalchemy as sa
+from flask_restx import abort
+from flask_sqlalchemy import BaseQuery, SQLAlchemy
+from flask_sqlalchemy.model import Model
+from sqlalchemy.sql import func
+
+
+class Base(Model):
+    __abstract__ = True
+    _created = sa.Column(sa.DateTime(timezone=True), server_default=func.now())
+    _updated = sa.Column(sa.DateTime(timezone=True), onupdate=func.now())
+
+
+class ExtendedQuery(BaseQuery):
+    def first_extended(self, required=True, error_message=None, error_code=codes.NOT_FOUND):
+        item = self.first()
+
+        if required and not item:
+            if not error_message:
+                error_message = "Object not found"
+            abort(error_code, error_message)
+
+        return item
+
+    def pagination(self, page=0, page_size=15, order_column=None, order=1):
+        query = self
+        if order_column:
+            if order == 1:
+                query = query.order_by(order_column)
+            else:
+                query = query.order_by(order_column.desc())
+
+        total = query.count()
+        query = query.limit(page_size).offset(page * page_size)
+        items = query.all()
+        return items, total
diff --git a/server/app/core/controller/__init__.py b/server/app/database/controller/__init__.py
similarity index 76%
rename from server/app/core/controller/__init__.py
rename to server/app/database/controller/__init__.py
index 57f9b429..d31ded56 100644
--- a/server/app/core/controller/__init__.py
+++ b/server/app/database/controller/__init__.py
@@ -1,6 +1,6 @@
 # import add, get
 from app.core import db
-from app.core.controller import add, delete, edit, get
+from app.database.controller import add, delete, edit, get, search
 
 
 def commit_and_refresh(item):
diff --git a/server/app/core/controller/add.py b/server/app/database/controller/add.py
similarity index 77%
rename from server/app/core/controller/add.py
rename to server/app/database/controller/add.py
index 38c2a283..37f34dd7 100644
--- a/server/app/core/controller/add.py
+++ b/server/app/database/controller/add.py
@@ -1,5 +1,18 @@
+import app.core.http_codes as codes
 from app.core import db
-from app.core.models import Blacklist, City, Competition, MediaType, Question, QuestionType, Role, Slide, Team, User
+from app.database.models import (
+    Blacklist,
+    City,
+    Competition,
+    MediaType,
+    Question,
+    QuestionType,
+    Role,
+    Slide,
+    Team,
+    User,
+)
+from flask_restx import abort
 
 
 def db_add(func):
@@ -8,6 +21,10 @@ def db_add(func):
         db.session.add(item)
         db.session.commit()
         db.session.refresh(item)
+
+        if not item:
+            abort(codes.BAD_REQUEST, f"Object could not be created")
+
         return item
 
     return wrapper
diff --git a/server/app/core/controller/delete.py b/server/app/database/controller/delete.py
similarity index 86%
rename from server/app/core/controller/delete.py
rename to server/app/database/controller/delete.py
index ac6ddf70..65527ee9 100644
--- a/server/app/core/controller/delete.py
+++ b/server/app/database/controller/delete.py
@@ -1,6 +1,6 @@
-import app.core.controller as dbc
+import app.database.controller as dbc
 from app.core import db
-from app.core.models import Blacklist, City, Competition, Role, Slide, User
+from app.database.models import Blacklist, City, Competition, Role, Slide, User
 
 
 def default(item):
@@ -17,7 +17,7 @@ def slide(item_slide):
     default(item_slide)
 
     # Update slide order for all slides after the deleted slide
-    slides_in_same_competition, _ = dbc.get.search_slide(competition_id=deleted_slide_competition_id)
+    slides_in_same_competition = dbc.get.slide_list(deleted_slide_competition_id)
     for other_slide in slides_in_same_competition:
         if other_slide.order > deleted_slide_order:
             other_slide.order -= 1
diff --git a/server/app/core/controller/edit.py b/server/app/database/controller/edit.py
similarity index 100%
rename from server/app/core/controller/edit.py
rename to server/app/database/controller/edit.py
diff --git a/server/app/database/controller/get.py b/server/app/database/controller/get.py
new file mode 100644
index 00000000..a2b45fed
--- /dev/null
+++ b/server/app/database/controller/get.py
@@ -0,0 +1,56 @@
+from app.database.models import Competition, Question, Slide, Team, User
+from sqlalchemy.sql.expression import outerjoin
+
+
+def user_exists(email):
+    return User.query.filter(User.email == email).count() > 0
+
+
+def competition(CID, required=True, error_msg=None):
+    return Competition.query.filter(Competition.id == CID).first_extended(required, error_msg)
+
+
+def user(UID, required=True, error_msg=None):
+    return User.query.filter(User.id == UID).first_extended(required, error_msg)
+
+
+def user_by_email(email, required=True, error_msg=None):
+    return User.query.filter(User.email == email).first_extended(required, error_msg)
+
+
+def slide_by_order(CID, order, required=True, error_msg=None):
+    return Slide.query.filter((Slide.competition_id == CID) & (Slide.order == order)).first_extended(
+        required, error_msg
+    )
+
+
+def slide(CID, SID, required=True, error_msg=None):
+    return Slide.query.filter((Slide.competition_id == CID) & (Slide.id == SID)).first_extended(required, error_msg)
+
+
+def team(CID, TID, required=True, error_msg=None):
+    return Team.query.filter((Team.competition_id == CID) & (Team.id == TID)).first_extended(required, error_msg)
+
+
+def question(CID, QID, required=True, error_msg=None):
+    return (
+        Question.query.join(Slide, (Slide.competition_id == CID) & (Slide.id == Question.slide_id))
+        .filter(Question.id == QID)
+        .first_extended(required, error_msg)
+    )
+
+
+def question_list(CID):
+    return Question.query.join(Slide, (Slide.competition_id == CID) & (Slide.id == Question.slide_id)).all()
+
+
+def team_list(CID):
+    return Team.query.filter(Team.competition_id == CID).all()
+
+
+def slide_list(CID):
+    return Slide.query.filter(Slide.competition_id == CID).all()
+
+
+def slide_count(CID):
+    return Slide.query.filter(Slide.competition_id == CID).count()
diff --git a/server/app/core/controller/get.py b/server/app/database/controller/search.py
similarity index 58%
rename from server/app/core/controller/get.py
rename to server/app/database/controller/search.py
index f695deee..f570176c 100644
--- a/server/app/core/controller/get.py
+++ b/server/app/database/controller/search.py
@@ -1,38 +1,7 @@
-from app.core.models import Competition, Question, Slide, Team, User
+from app.database.models import Competition, Question, Slide, Team, User
 
 
-def slide_by_order(CID, order):
-    return Slide.query.filter((Slide.competition_id == CID) & (Slide.order == order)).first()
-
-
-def slide(CID, SID):
-    return Slide.query.filter((Slide.competition_id == CID) & (Slide.id == SID)).first()
-
-
-def team(CID, TID):
-    return Team.query.filter((Team.competition_id == CID) & (Team.id == TID)).first()
-
-
-def question(CID, QID):
-    slide_ids = set(
-        [x.id for x in Slide.query.filter(Slide.competition_id == CID).all()]
-    )  # TODO: Filter using database instead of creating a set of slide_ids
-    return Question.query.filter(Question.slide_id.in_(slide_ids) & (Question.id == QID)).first()
-
-
-def _search(query, order_column, page=0, page_size=15, order=1):
-    if order == 1:
-        query = query.order_by(order_column)
-    else:
-        query = query.order_by(order_column.desc())
-
-    total = query.count()
-    query = query.limit(page_size).offset(page * page_size)
-    items = query.all()
-    return items, total
-
-
-def search_user(email=None, name=None, city_id=None, role_id=None, page=0, page_size=15, order=1, order_by=None):
+def user(email=None, name=None, city_id=None, role_id=None, page=0, page_size=15, order=1, order_by=None):
     query = User.query
     if name:
         query = query.filter(User.name.like(f"%{name}%"))
@@ -47,12 +16,26 @@ def search_user(email=None, name=None, city_id=None, role_id=None, page=0, page_
     if order_by:
         order_column = getattr(User.__table__.c, order_by)
 
-    return _search(query, order_column, page, page_size, order)
+    return query.pagination(page, page_size, order_column, order)
 
 
-def search_slide(
-    slide_order=None, title=None, body=None, competition_id=None, page=0, page_size=15, order=1, order_by=None
-):
+def competitions(name=None, year=None, city_id=None, page=0, page_size=15, order=1, order_by=None):
+    query = Competition.query
+    if name:
+        query = query.filter(Competition.name.like(f"%{name}%"))
+    if year:
+        query = query.filter(Competition.year == year)
+    if city_id:
+        query = query.filter(Competition.city_id == city_id)
+
+    order_column = Competition.year  # Default order_by
+    if order_by:
+        order_column = getattr(Competition.columns, order_by)
+
+    return query.pagination(page, page_size, order_column, order)
+
+
+def slide(slide_order=None, title=None, body=None, competition_id=None, page=0, page_size=15, order=1, order_by=None):
     query = Slide.query
     if slide_order:
         query = query.filter(Slide.order == slide_order)
@@ -67,10 +50,10 @@ def search_slide(
     if order_by:
         order_column = getattr(Slide.__table__.c, order_by)
 
-    return _search(query, order_column, page, page_size, order)
+    return query.pagination(page, page_size, order_column, order)
 
 
-def search_questions(
+def questions(
     name=None,
     total_score=None,
     type_id=None,
@@ -100,20 +83,4 @@ def search_questions(
     if order_by:
         order_column = getattr(Question.__table__.c, order_by)
 
-    return _search(query, order_column, page, page_size, order)
-
-
-def search_competitions(name=None, year=None, city_id=None, page=0, page_size=15, order=1, order_by=None):
-    query = Competition.query
-    if name:
-        query = query.filter(Competition.name.like(f"%{name}%"))
-    if year:
-        query = query.filter(Competition.year == year)
-    if city_id:
-        query = query.filter(Competition.city_id == city_id)
-
-    order_column = Competition.year  # Default order_by
-    if order_by:
-        order_column = getattr(Competition.columns, order_by)
-
-    return _search(query, order_column, page, page_size, order)
+    return query.pagination(page, page_size, order_column, order)
diff --git a/server/app/core/models.py b/server/app/database/models.py
similarity index 99%
rename from server/app/core/models.py
rename to server/app/database/models.py
index bd23f0d7..c3bc3d56 100644
--- a/server/app/core/models.py
+++ b/server/app/database/models.py
@@ -123,8 +123,6 @@ class Slide(db.Model):
     settings = db.Column(db.Text, nullable=False, default="{}")
     competition_id = db.Column(db.Integer, db.ForeignKey("competition.id"), nullable=False)
 
-    questions = db.relationship("Question", backref="slide")
-
     def __init__(self, order, competition_id):
         self.order = order
         self.competition_id = competition_id
@@ -138,6 +136,7 @@ class Question(db.Model):
     type_id = db.Column(db.Integer, db.ForeignKey("question_type.id"), nullable=False)
     slide_id = db.Column(db.Integer, db.ForeignKey("slide.id"), nullable=False)
 
+    slide = db.relationship("Slide", backref="questions")
     question_answers = db.relationship("QuestionAnswer", backref="question")
     alternatives = db.relationship("QuestionAlternative", backref="question")
 
diff --git a/server/tests/test_app.py b/server/tests/test_app.py
index 38a103b9..1cbacc33 100644
--- a/server/tests/test_app.py
+++ b/server/tests/test_app.py
@@ -1,8 +1,9 @@
 import app.core.http_codes as codes
-from app.core.models import Slide
+from app.database.models import Slide
 
 from tests import app, client, db
-from tests.test_helpers import add_default_values, change_order_test, delete, get, post, put
+from tests.test_helpers import (add_default_values, change_order_test, delete,
+                                get, post, put)
 
 
 def test_misc_api(client):
@@ -301,7 +302,7 @@ def test_slide_api(client):
     SID = body["items"][i]["id"]
     order = body["items"][i]["order"]
     response, _ = put(client, f"/api/competitions/{CID}/slides/{SID}/order", {"order": order}, headers=headers)
-    assert response.status_code == codes.BAD_REQUEST
+    assert response.status_code == codes.OK
 
     # Changes the order
     change_order_test(client, CID, SID, order + 1, headers)
@@ -330,6 +331,7 @@ def test_question_api(client):
     num_questions = 3
     response, body = get(client, f"/api/competitions/{CID}/questions", headers=headers)
     assert response.status_code == codes.OK
+    print(body)
     assert body["count"] == num_questions
 
     # # Get specific question
@@ -368,7 +370,7 @@ def test_question_api(client):
     assert item_question["name"] == name
     # # assert item_question["total_score"] == total_score
     assert item_question["type"]["id"] == type_id
-    assert item_question["slide"]["id"] == slide_id
+    assert item_question["slide_id"] == slide_id
     # Checks number of questions
     response, body = get(client, f"/api/competitions/{CID}/questions", headers=headers)
     assert response.status_code == codes.OK
@@ -413,7 +415,7 @@ def test_question_api(client):
     assert item_question["name"] != name
     # assert item_question["total_score"] != total_score
     assert item_question["type"]["id"] != type_id
-    assert item_question["slide"]["id"] != slide_id
+    assert item_question["slide_id"] != slide_id
     response, item_question = put(
         client,
         f"/api/competitions/{CID}/questions/{QID}",
@@ -425,7 +427,7 @@ def test_question_api(client):
     assert item_question["name"] == name
     # # assert item_question["total_score"] == total_score
     assert item_question["type"]["id"] == type_id
-    assert item_question["slide"]["id"] == slide_id
+    assert item_question["slide_id"] == slide_id
     # Checks number of questions
     response, body = get(client, f"/api/competitions/{CID}/questions", headers=headers)
     assert response.status_code == codes.OK
diff --git a/server/tests/test_db.py b/server/tests/test_db.py
index 68f49d5a..3bb998ae 100644
--- a/server/tests/test_db.py
+++ b/server/tests/test_db.py
@@ -1,5 +1,5 @@
-import app.core.controller as dbc
-from app.core.models import City, Competition, Media, MediaType, Question, QuestionType, Role, Slide, Team, User
+import app.database.controller as dbc
+from app.database.models import City, Competition, Media, MediaType, Question, QuestionType, Role, Slide, Team, User
 
 from tests import app, client, db
 from tests.test_helpers import add_default_values, assert_exists, assert_insert_fail
@@ -40,6 +40,7 @@ def test_media(client):
     assert item_media.upload_by.email == "test@test.se"
 
 
+"""
 def test_question(client):
     add_default_values()
     item_user = User.query.filter_by(email="test@test.se").first()
@@ -168,3 +169,4 @@ def test_slide(client):
     aux = dbc.get.search_slide(slide_order=1, competition_id=item_comp.id)
     item_slide = aux[0][0]
     dbc.delete.slide(item_slide)
+"""
diff --git a/server/tests/test_helpers.py b/server/tests/test_helpers.py
index fbb58ac1..7cbdec87 100644
--- a/server/tests/test_helpers.py
+++ b/server/tests/test_helpers.py
@@ -1,9 +1,9 @@
 import json
 
-import app.core.controller as dbc
 import app.core.http_codes as codes
+import app.database.controller as dbc
 from app.core import db
-from app.core.models import City, Role
+from app.database.models import City, Role
 
 
 def add_default_values():
-- 
GitLab