From 0d22489ff2880f8ad22bd3fb7fb7b06f8085fa35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carl=20Sch=C3=B6nfelder?= <carsc272@student.liu.se> Date: Thu, 1 Apr 2021 12:15:48 +0000 Subject: [PATCH] Resolve "Improve competition-api" --- server/app/apis/__init__.py | 6 ++ server/app/apis/competitions.py | 81 ++------------------------- server/app/apis/misc.py | 43 +++++++++++++++ server/app/apis/slides.py | 89 ++++++++++++++++++++++++++++-- server/app/apis/teams.py | 51 +++++++++++++++++ server/app/apis/users.py | 9 +-- server/app/core/controller/edit.py | 32 +++++++++++ server/app/core/controller/get.py | 16 ++++-- server/app/core/dto.py | 34 ++++++++++-- server/app/core/parsers.py | 22 +++++--- server/app/core/schemas.py | 23 -------- server/tests/test_app.py | 3 + 12 files changed, 286 insertions(+), 123 deletions(-) create mode 100644 server/app/apis/misc.py create mode 100644 server/app/apis/teams.py delete mode 100644 server/app/core/schemas.py diff --git a/server/app/apis/__init__.py b/server/app/apis/__init__.py index 3c0e7020..c9ff2f51 100644 --- a/server/app/apis/__init__.py +++ b/server/app/apis/__init__.py @@ -40,9 +40,15 @@ from flask_restx import Api from .auth import api as auth_ns from .competitions import api as comp_ns +from .misc import api as misc_ns +from .slides import api as slide_ns +from .teams import api as team_ns from .users import api as user_ns flask_api = Api() +flask_api.add_namespace(misc_ns, path="/api/misc") flask_api.add_namespace(user_ns, path="/api/users") flask_api.add_namespace(auth_ns, path="/api/auth") 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") diff --git a/server/app/apis/competitions.py b/server/app/apis/competitions.py index a60a49d5..2fed07aa 100644 --- a/server/app/apis/competitions.py +++ b/server/app/apis/competitions.py @@ -9,8 +9,7 @@ from flask_restx import Resource, reqparse api = CompetitionDTO.api competition_model = CompetitionDTO.model -slide_model = CompetitionDTO.slide_model -team_model = CompetitionDTO.team_model +competition_list_model = CompetitionDTO.user_list_model def get_comp(CID): @@ -69,83 +68,15 @@ class Competitions(Resource): @api.route("/search") class CompetitionSearch(Resource): @jwt_required - @api.marshal_with(competition_model) + @api.marshal_with(competition_list_model) def get(self): args = competition_search_parser.parse_args(strict=True) name = args.get("name") year = args.get("year") city_id = args.get("city_id") style_id = args.get("style_id") - page = args.get("page") - page_size = args.get("page_size") - return dbc.get.search_competitions(name, year, city_id, style_id, page, page_size) - - -@api.route("/<CID>/slides") -@api.param("CID") -class SlidesList(Resource): - @jwt_required - @api.marshal_with(slide_model) - def get(self, CID): - item_comp = get_comp(CID) - return item_comp.slides - - @jwt_required - @api.marshal_with(slide_model) - def post(self, CID): - dbc.add.slide(CID) - item_comp = get_comp(CID) - return item_comp.slides - - -@api.route("/<CID>/slides/<SID>") -@api.param("CID,SID") -class Slides(Resource): - @jwt_required - @api.marshal_with(slide_model) - def get(self, CID, SID): - item_slide = dbc.get.slide(CID, SID) - return item_slide - - @jwt_required - def delete(self, CID, SID): - item_slide = dbc.get.slide(CID, SID) - dbc.delete.slide(item_slide) - return "deleted" + page = args.get("page", 0) + page_size = args.get("page_size", 15) + result, total = dbc.get.search_competitions(name, year, city_id, style_id, page, page_size) - -@api.route("/<CID>/teams") -@api.param("CID") -class TeamsList(Resource): - @jwt_required - @api.marshal_with(team_model) - def get(self, CID): - item_comp = get_comp(CID) - return item_comp.teams - - @jwt_required - @api.marshal_with(team_model) - def post(self, CID): - parser = reqparse.RequestParser() - parser.add_argument("name", type=str, location="json") - args = competition_parser.parse_args(strict=True) - - dbc.add.default(Team(args["name"], CID)) - item_comp = get_comp(CID) - return item_comp.teams - - -@api.route("/<CID>/teams/<TID>") -@api.param("CID,TID") -class Teams(Resource): - @jwt_required - @api.marshal_with(team_model) - def get(self, CID, TID): - item_team = dbc.get.team(CID, TID) - return item_team - - @jwt_required - def delete(self, CID, TID): - item_team = dbc.get.team(CID, TID) - dbc.delete.team(item_team) - return "deleted" + return {"competitions": result, "count": len(result), "total": total} diff --git a/server/app/apis/misc.py b/server/app/apis/misc.py new file mode 100644 index 00000000..d88c6ae3 --- /dev/null +++ b/server/app/apis/misc.py @@ -0,0 +1,43 @@ +from app.core.dto import MiscDTO +from app.core.models import City, MediaType, QuestionType, Role +from flask_jwt_extended import jwt_required +from flask_restx import Resource + +api = MiscDTO.api + +question_type_model = MiscDTO.question_type_model +media_type_model = MiscDTO.media_type_model +role_model = MiscDTO.role_model +city_model = MiscDTO.city_model + + +@api.route("/media_types") +class MediaTypeList(Resource): + @jwt_required + @api.marshal_with(media_type_model) + def get(self): + return MediaType.query.all() + + +@api.route("/question_types") +class QuestionTypeList(Resource): + @jwt_required + @api.marshal_with(question_type_model) + def get(self): + return QuestionType.query.all() + + +@api.route("/roles") +class RoleList(Resource): + @jwt_required + @api.marshal_with(role_model) + def get(self): + return Role.query.all() + + +@api.route("/cities") +class CityList(Resource): + @jwt_required + @api.marshal_with(city_model) + def get(self): + return City.query.all() diff --git a/server/app/apis/slides.py b/server/app/apis/slides.py index 6b0ee735..833b0651 100644 --- a/server/app/apis/slides.py +++ b/server/app/apis/slides.py @@ -1,8 +1,89 @@ import app.core.controller as dbc import app.core.utils.http_codes as codes from app.apis import admin_required -from app.core.models import Competition, Slide, User -from app.core.parsers import competition_parser, competition_search_parser -from app.core.schemas import competition_schema +from app.core.dto import SlideDTO +from app.core.models import Competition, Slide +from app.core.parsers import slide_parser from flask_jwt_extended import get_jwt_identity, jwt_required -from flask_restx import Namespace, Resource +from flask_restx import Namespace, Resource, reqparse + +api = SlideDTO.api +model = SlideDTO.model + + +def get_comp(CID): + return Competition.query.filter(Competition.id == CID).first() + + +@api.route("/") +@api.param("CID") +class SlidesList(Resource): + @jwt_required + @api.marshal_with(model) + def get(self, CID): + item_comp = get_comp(CID) + return item_comp.slides + + @jwt_required + @api.marshal_with(model) + def post(self, CID): + dbc.add.slide(CID) + item_comp = get_comp(CID) + return item_comp.slides + + +@api.route("/<SID>") +@api.param("CID,SID") +class Slides(Resource): + @jwt_required + @api.marshal_with(model) + def get(self, CID, SID): + item_slide = dbc.get.slide(CID, SID) + return item_slide + + @jwt_required + @api.marshal_with(model) + def put(self, CID, SID): + args = slide_parser.parse_args(strict=True) + title = args.get("title") + timer = args.get("timer") + + item_slide = dbc.get.slide(CID, SID) + + return dbc.edit.slide(item_slide, title, timer) + + @jwt_required + def delete(self, CID, SID): + item_slide = dbc.get.slide(CID, SID) + dbc.delete.slide(item_slide) + return "deleted" + + +@api.route("/<SID>/order") +@api.param("CID,SID") +class SlidesOrder(Resource): + @jwt_required + @api.marshal_with(model) + def put(self, CID, SID): + args = slide_parser.parse_args(strict=True) + order = args.get("order") + + item_slide = dbc.get.slide(CID, SID) + + if order == item_slide.order: + api.abort(codes.BAD_REQUEST) + + # clamp order between 0 and max + order_count = Slide.query.filter(Slide.competition_id == item_slide.competition_id).count() + if order < 0: + order = 0 + elif order >= order_count - 1: + order = order_count - 1 + + # get slide at the requested order + item_slide_order = dbc.get.slide_by_order(CID, order) + + # switch place between them + item_slide = dbc.edit.switch_order(item_slide, item_slide_order) + + return item_slide diff --git a/server/app/apis/teams.py b/server/app/apis/teams.py new file mode 100644 index 00000000..0b155f20 --- /dev/null +++ b/server/app/apis/teams.py @@ -0,0 +1,51 @@ +import app.core.controller as dbc +import app.core.utils.http_codes as codes +from app.apis import admin_required +from app.core.dto import TeamDTO +from app.core.models import Competition, Team +from flask_jwt_extended import get_jwt_identity, jwt_required +from flask_restx import Namespace, Resource, reqparse + +api = TeamDTO.api +model = TeamDTO.model + + +def get_comp(CID): + return Competition.query.filter(Competition.id == CID).first() + + +@api.route("/") +@api.param("CID") +class TeamsList(Resource): + @jwt_required + @api.marshal_with(model) + def get(self, CID): + item_comp = get_comp(CID) + return item_comp.teams + + @jwt_required + @api.marshal_with(model) + def post(self, CID): + parser = reqparse.RequestParser() + parser.add_argument("name", type=str, location="json") + args = parser.parse_args(strict=True) + + dbc.add.default(Team(args["name"], CID)) + item_comp = get_comp(CID) + return item_comp.teams + + +@api.route("/<TID>") +@api.param("CID,TID") +class Teams(Resource): + @jwt_required + @api.marshal_with(model) + def get(self, CID, TID): + item_team = dbc.get.team(CID, TID) + return item_team + + @jwt_required + def delete(self, CID, TID): + item_team = dbc.get.team(CID, TID) + dbc.delete.team(item_team) + return "deleted" diff --git a/server/app/apis/users.py b/server/app/apis/users.py index 9b7d266e..4ea7dee8 100644 --- a/server/app/apis/users.py +++ b/server/app/apis/users.py @@ -4,12 +4,12 @@ from app.apis import admin_required from app.core.dto import UserDTO from app.core.models import User from app.core.parsers import user_parser, user_search_parser -from app.core.schemas import user_schema from flask_jwt_extended import get_jwt_identity, jwt_required from flask_restx import Namespace, Resource api = UserDTO.api user_model = UserDTO.model +user_list_model = UserDTO.user_list_model def edit_user(item_user, args): @@ -59,14 +59,15 @@ class Users(Resource): @api.route("/search") class UserSearch(Resource): @jwt_required - @api.marshal_list_with(user_model) + @api.marshal_list_with(user_list_model) def get(self): args = user_search_parser.parse_args(strict=True) name = args.get("name") email = args.get("email") role_id = args.get("role_id") city_id = args.get("city_id") - page = args.get("page", 1) + page = args.get("page", 0) page_size = args.get("page_size", 15) - return dbc.get.search_user(email, name, city_id, role_id, page, page_size) + result, total = dbc.get.search_user(email, name, city_id, role_id, page, page_size) + return {"users": result, "count": len(result), "total": total} diff --git a/server/app/core/controller/edit.py b/server/app/core/controller/edit.py index a9277904..bd6255a7 100644 --- a/server/app/core/controller/edit.py +++ b/server/app/core/controller/edit.py @@ -1,7 +1,39 @@ +import math + from app.core import db from app.core.models import Blacklist, City, Competition, Role, Slide, User +def switch_order(item1, item2): + old_order = item1.order + new_order = item2.order + + item2.order = -1 + db.session.commit() + db.session.refresh(item2) + + item1.order = new_order + db.session.commit() + db.session.refresh(item1) + + item2.order = old_order + db.session.commit() + db.session.refresh(item2) + + return item1 + + +def slide(item, title=None, timer=None): + if title: + item.title = title + if timer: + item.timer = timer + + db.session.commit() + db.session.refresh(item) + return item + + def competition(item, name=None, year=None, city_id=None, style_id=None): if name: item.name = name diff --git a/server/app/core/controller/get.py b/server/app/core/controller/get.py index b1a19e04..ef86ef6b 100644 --- a/server/app/core/controller/get.py +++ b/server/app/core/controller/get.py @@ -1,6 +1,10 @@ from app.core.models import Competition, 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() @@ -9,7 +13,7 @@ def team(CID, TID): return Team.query.filter((Team.competition_id == CID) & (Team.id == TID)).first() -def search_user(email=None, name=None, city_id=None, role_id=None, page=1, page_size=15): +def search_user(email=None, name=None, city_id=None, role_id=None, page=0, page_size=15): query = User.query if name: query = query.filter(User.name.like(f"%{name}%")) @@ -20,12 +24,14 @@ def search_user(email=None, name=None, city_id=None, role_id=None, page=1, page_ if role_id: query = query.filter(User.role_id == role_id) + total = query.count() query = query.limit(page_size).offset(page * page_size) + result = query.all() - return query.all() + return result, total -def search_competitions(name=None, year=None, city_id=None, style_id=None, page=1, page_size=15): +def search_competitions(name=None, year=None, city_id=None, style_id=None, page=0, page_size=15): query = Competition.query if name: query = query.filter(Competition.name.like(f"%{name}%")) @@ -36,6 +42,8 @@ def search_competitions(name=None, year=None, city_id=None, style_id=None, page= if style_id: query = query.filter(Competition.style_id == style_id) + total = query.count() query = query.limit(page_size).offset(page * page_size) + result = query.all() - return query.all() + return result, total diff --git a/server/app/core/dto.py b/server/app/core/dto.py index 1ef2f51b..6c47877c 100644 --- a/server/app/core/dto.py +++ b/server/app/core/dto.py @@ -1,4 +1,3 @@ -from app.apis import slides from flask_restx import Namespace, fields @@ -19,6 +18,10 @@ class UserDTO: "city_id": fields.Integer(), }, ) + user_list_model = api.model( + "UserList", + {"users": fields.List(fields.Nested(model)), "count": fields.Integer(), "total": fields.Integer()}, + ) class CompetitionDTO: @@ -33,15 +36,34 @@ class CompetitionDTO: "city_id": fields.Integer(), }, ) - slide_model = api.model( + user_list_model = api.model( + "CompetitionList", + {"competitions": fields.List(fields.Nested(model)), "count": fields.Integer(), "total": fields.Integer()}, + ) + + +class SlideDTO: + api = Namespace("slides") + model = api.model( "Slide", { "id": fields.Integer(), - "competition_id": fields.Integer(), "order": fields.Integer(), + "title": fields.String(), + "timer": fields.Integer(), + "competition_id": fields.Integer(), }, ) - team_model = api.model( - "Team", {"id": fields.Integer(), "name": fields.String(), "competition_id": fields.Integer()} - ) + +class TeamDTO: + api = Namespace("teams") + model = api.model("Team", {"id": fields.Integer(), "name": fields.String(), "competition_id": fields.Integer()}) + + +class MiscDTO: + api = Namespace("misc") + role_model = api.model("Role", {"id": fields.Integer(), "name": fields.String()}) + question_type_model = api.model("QuestionType", {"id": fields.Integer(), "name": fields.String()}) + media_type_model = api.model("MediaType", {"id": fields.Integer(), "name": fields.String()}) + city_model = api.model("City", {"id": fields.Integer(), "name": fields.String()}) diff --git a/server/app/core/parsers.py b/server/app/core/parsers.py index 9106c5d8..a71525d9 100644 --- a/server/app/core/parsers.py +++ b/server/app/core/parsers.py @@ -2,8 +2,8 @@ from flask_restx import inputs, reqparse ###SEARCH#### search_parser = reqparse.RequestParser() -search_parser.add_argument("page", type=int, default=0, location="args") -search_parser.add_argument("page_size", type=int, default=15, location="args") +search_parser.add_argument("page", type=int, default=0) +search_parser.add_argument("page_size", type=int, default=15) ###LOGIN#### login_parser = reqparse.RequestParser() @@ -33,15 +33,23 @@ user_search_parser.add_argument("role_id", type=int, default=None, location="arg ###COMPETIION#### competition_parser = reqparse.RequestParser() -competition_parser.add_argument("name", type=str, location="json") -competition_parser.add_argument("year", type=int, location="json") -competition_parser.add_argument("city_id", type=int, location="json") -competition_parser.add_argument("style_id", type=int, location="json") +competition_parser.add_argument("name", type=str) +competition_parser.add_argument("year", type=int) +competition_parser.add_argument("city_id", type=int) +competition_parser.add_argument("style_id", type=int) ###SEARCH_COMPETITOIN#### -competition_search_parser = search_parser.copy() +competition_search_parser = reqparse.RequestParser() +# competition_search_parser.add_argument(competition_parser, search_parser) competition_search_parser.add_argument("name", type=str, default=None, location="args") competition_search_parser.add_argument("year", type=str, default=None, location="args") competition_search_parser.add_argument("city_id", type=int, default=None, location="args") competition_search_parser.add_argument("style_id", type=int, default=None, location="args") + + +###SEARCH_COMPETITOIN#### +slide_parser = reqparse.RequestParser() +slide_parser.add_argument("order", type=int, default=None) +slide_parser.add_argument("title", type=str, default=None) +slide_parser.add_argument("timer", type=int, default=None) diff --git a/server/app/core/schemas.py b/server/app/core/schemas.py deleted file mode 100644 index afc1911b..00000000 --- a/server/app/core/schemas.py +++ /dev/null @@ -1,23 +0,0 @@ -from flask_restx import Namespace, Resource, abort, fields, inputs, model, reqparse - -user_schema = ( - "User", - { - "id": fields.Integer(), - "name": fields.String(), - "email": fields.String(), - "role_id": fields.Integer(), - "city_id": fields.Integer(), - }, -) - -competition_schema = ( - "Competition", - { - "id": fields.Integer(), - "name": fields.String(), - "year": fields.Integer(), - "style_id": fields.Integer(), - "city_id": fields.Integer(), - }, -) diff --git a/server/tests/test_app.py b/server/tests/test_app.py index bba8b481..ce0255c3 100644 --- a/server/tests/test_app.py +++ b/server/tests/test_app.py @@ -31,6 +31,9 @@ def test_competition(client): assert response.status_code == 200 assert len(body) == 2 + response, body = put(client, "/api/competitions/1/slides/1/order", {"order": 1}, headers=headers) + assert response.status_code == 200 + response, body = post(client, "/api/competitions/1/teams", {"name": "t1"}, headers=headers) assert response.status_code == 200 -- GitLab