From 7618ad9cdd6eb73cf369aa42fb57028e5dbfc8c3 Mon Sep 17 00:00:00 2001 From: Josef Olsson <josol381@student.liu.se> Date: Mon, 12 Apr 2021 15:48:18 +0000 Subject: [PATCH] Resolve "Add more api calls" --- server/app/apis/__init__.py | 10 +- server/app/apis/auth.py | 13 +- server/app/apis/competitions.py | 40 +-- server/app/apis/questions.py | 74 +++++ server/app/apis/slides.py | 5 +- server/app/apis/teams.py | 28 +- server/app/apis/users.py | 18 +- server/app/core/controller/add.py | 8 +- server/app/core/controller/delete.py | 51 +++- server/app/core/controller/edit.py | 31 +++ server/app/core/controller/get.py | 62 ++++- server/app/core/dto.py | 6 + server/app/core/http_codes.py | 1 + server/app/core/parsers.py | 34 ++- server/app/core/rich_schemas.py | 13 +- server/app/core/schemas.py | 11 + server/app/core/sockets.py | 36 +++ server/tests/test_app.py | 399 ++++++++++++++++++++++++--- server/tests/test_db.py | 63 ++++- server/tests/test_helpers.py | 82 +++++- 20 files changed, 845 insertions(+), 140 deletions(-) create mode 100644 server/app/apis/questions.py create mode 100644 server/app/core/sockets.py diff --git a/server/app/apis/__init__.py b/server/app/apis/__init__.py index 493a1a94..061a4519 100644 --- a/server/app/apis/__init__.py +++ b/server/app/apis/__init__.py @@ -22,11 +22,11 @@ def admin_required(): return wrapper -def text_response(message, code=200): - return {"message": message}, 200 +def text_response(message, code=codes.OK): + return {"message": message}, codes.OK -def list_response(items, total=None, code=200): +def list_response(items, total=None, code=codes.OK): if type(items) is not list: abort(codes.INTERNAL_SERVER_ERROR) if not total: @@ -34,7 +34,7 @@ def list_response(items, total=None, code=200): return {"items": items, "count": len(items), "total_count": total}, code -def item_response(item, code=200): +def item_response(item, code=codes.OK): if isinstance(item, list): abort(codes.INTERNAL_SERVER_ERROR) return item, code @@ -45,6 +45,7 @@ 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 .questions import api as question_ns from .slides import api as slide_ns from .teams import api as team_ns from .users import api as user_ns @@ -56,3 +57,4 @@ 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") +flask_api.add_namespace(question_ns, path="/api/competitions/<CID>/questions") diff --git a/server/app/apis/auth.py b/server/app/apis/auth.py index 4fa5624a..df3a8e5c 100644 --- a/server/app/apis/auth.py +++ b/server/app/apis/auth.py @@ -29,15 +29,12 @@ class AuthSignup(Resource): def post(self): args = create_user_parser.parse_args(strict=True) email = args.get("email") - password = args.get("password") - role_id = args.get("role_id") - city_id = args.get("city_id") - name = args.get("name") if User.query.filter(User.email == email).count() > 0: api.abort(codes.BAD_REQUEST, "User already exists") - item_user = dbc.add.user(email, password, role_id, city_id, name) + 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") @@ -50,8 +47,12 @@ 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}.") + dbc.delete.default(item_user) - if ID == get_jwt_identity(): + if int(ID) == get_jwt_identity(): jti = get_raw_jwt()["jti"] dbc.add.blacklist(jti) return text_response(f"User {ID} deleted") diff --git a/server/app/apis/competitions.py b/server/app/apis/competitions.py index 9f17a932..93f03227 100644 --- a/server/app/apis/competitions.py +++ b/server/app/apis/competitions.py @@ -21,42 +21,32 @@ class CompetitionsList(Resource): def post(self): args = competition_parser.parse_args(strict=True) - name = args.get("name") - city_id = args.get("city_id") - year = args.get("year") - # Add competition - item = dbc.add.competition(name, year, city_id) + item = dbc.add.competition(**args) # Add default slide dbc.add.slide(item) - - dbc.refresh(item) return item_response(schema.dump(item)) -@api.route("/<ID>") -@api.param("ID") +@api.route("/<CID>") +@api.param("CID") class Competitions(Resource): @jwt_required - def get(self, ID): - item = get_comp(ID) + def get(self, CID): + item = get_comp(CID) return item_response(schema.dump(item)) @jwt_required - def put(self, ID): + def put(self, CID): args = competition_parser.parse_args(strict=True) - - item = get_comp(ID) - name = args.get("name") - year = args.get("year") - city_id = args.get("city_id") - item = dbc.edit.competition(item, name, year, city_id) + item = get_comp(CID) + item = dbc.edit.competition(item, **args) return item_response(schema.dump(item)) @jwt_required - def delete(self, ID): - item = get_comp(ID) + def delete(self, CID): + item = get_comp(CID) dbc.delete.competition(item) return "deleted" @@ -66,13 +56,5 @@ class CompetitionSearch(Resource): @jwt_required 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") - page = args.get("page", 0) - page_size = args.get("page_size", 15) - order = args.get("order", 1) - order_by = args.get("order_by") - - items, total = dbc.get.search_competitions(name, year, city_id, page, page_size, order, order_by) + items, total = dbc.get.search_competitions(**args) return list_response(list_schema.dump(items), total) diff --git a/server/app/apis/questions.py b/server/app/apis/questions.py new file mode 100644 index 00000000..76ca53f9 --- /dev/null +++ b/server/app/apis/questions.py @@ -0,0 +1,74 @@ +import app.core.controller as dbc +import app.core.http_codes as codes +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 + +api = QuestionDTO.api +schema = QuestionDTO.schema +list_schema = QuestionDTO.list_schema + + +@api.route("/") +@api.param("CID") +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) + + @jwt_required + def post(self, CID): + args = question_parser.parse_args(strict=True) + + name = args.get("name") + total_score = args.get("total_score") + type_id = args.get("type_id") + slide_id = args.get("slide_id") + + item_slide = dbc.get.slide(CID, slide_id) + item = dbc.add.question(name, total_score, type_id, item_slide) + + return item_response(schema.dump(item)) + + +@api.route("/<QID>") +@api.param("CID,QID") +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}.") + + 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.edit.question(item_question, **args) + + return item_response(schema.dump(item_question)) + + @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 6d9465e2..ab7caa59 100644 --- a/server/app/apis/slides.py +++ b/server/app/apis/slides.py @@ -54,8 +54,11 @@ 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 "deleted" + return {}, codes.NO_CONTENT @api.route("/<SID>/order") diff --git a/server/app/apis/teams.py b/server/app/apis/teams.py index bec6cb6a..7f7fdf0e 100644 --- a/server/app/apis/teams.py +++ b/server/app/apis/teams.py @@ -3,6 +3,7 @@ import app.core.http_codes as codes 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 flask_jwt_extended import get_jwt_identity, jwt_required from flask_restx import Namespace, Resource, reqparse @@ -25,14 +26,10 @@ class TeamsList(Resource): @jwt_required def post(self, CID): - parser = reqparse.RequestParser() - parser.add_argument("name", type=str, location="json") - args = parser.parse_args(strict=True) - + args = team_parser.parse_args(strict=True) item_comp = get_comp(CID) - dbc.add.team(args["name"], item_comp) - dbc.refresh(item_comp) - return list_response(list_schema.dump(item_comp.teams)) + item_team = dbc.add.team(args["name"], item_comp) + return item_response(schema.dump(item_team)) @api.route("/<TID>") @@ -46,5 +43,20 @@ 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 "deleted" + return {}, codes.NO_CONTENT + + @jwt_required + def put(self, CID, TID): + args = team_parser.parse_args(strict=True) + 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 4aaf372b..07e2484d 100644 --- a/server/app/apis/users.py +++ b/server/app/apis/users.py @@ -4,6 +4,7 @@ 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 flask import request from flask_jwt_extended import get_jwt_identity, jwt_required from flask_restx import Namespace, Resource @@ -14,15 +15,11 @@ list_schema = UserDTO.list_schema def edit_user(item_user, args): email = args.get("email") - name = args.get("name") - city_id = args.get("city_id") - role_id = args.get("role_id") - if email: if User.query.filter(User.email == args["email"]).count() > 0: api.abort(codes.BAD_REQUEST, "Email is already in use") - return dbc.edit.user(item_user, name, email, city_id, role_id) + return dbc.edit.user(item_user, **args) @api.route("/") @@ -61,14 +58,5 @@ class UserSearch(Resource): @jwt_required 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", 0) - page_size = args.get("page_size", 15) - order = args.get("order", 1) - order_by = args.get("order_by") - - items, total = dbc.get.search_user(email, name, city_id, role_id, page, page_size, order, order_by) + items, total = dbc.get.search_user(**args) return list_response(list_schema.dump(items), total) diff --git a/server/app/core/controller/add.py b/server/app/core/controller/add.py index 2352ec0d..38c2a283 100644 --- a/server/app/core/controller/add.py +++ b/server/app/core/controller/add.py @@ -25,13 +25,13 @@ def slide(item_competition): @db_add -def user(email, plaintext_password, role_id, city_id, name=None): - return User(email, plaintext_password, role_id, city_id, name) +def user(email, password, role_id, city_id, name=None): + return User(email, password, role_id, city_id, name) @db_add -def question(name, order, type_id, item_slide): - return Question(name, order, type_id, item_slide.id) +def question(name, total_score, type_id, item_slide): + return Question(name, total_score, type_id, item_slide.id) @db_add diff --git a/server/app/core/controller/delete.py b/server/app/core/controller/delete.py index 037373e4..ac6ddf70 100644 --- a/server/app/core/controller/delete.py +++ b/server/app/core/controller/delete.py @@ -1,3 +1,4 @@ +import app.core.controller as dbc from app.core import db from app.core.models import Blacklist, City, Competition, Role, Slide, User @@ -7,20 +8,48 @@ def default(item): db.session.commit() -def slide(item): - default(item) +def slide(item_slide): + for item_question in item_slide.questions: + question(item_question) + deleted_slide_competition_id = item_slide.competition_id + deleted_slide_order = item_slide.order + default(item_slide) -def team(item): - default(item) + # Update slide order for all slides after the deleted slide + slides_in_same_competition, _ = dbc.get.search_slide(competition_id=deleted_slide_competition_id) + for other_slide in slides_in_same_competition: + if other_slide.order > deleted_slide_order: + other_slide.order -= 1 + + db.session.commit() + + +def team(item_team): + for item_question_answer in item_team.question_answers: + question_answers(item_question_answer) + default(item_team) + + +def question(item_question): + for item_question_answer in item_question.question_answers: + question_answers(item_question_answer) + for item_alternative in item_question.alternatives: + alternatives(item_alternative) + default(item_question) -def competition(item): - # Remove all slides from competition - for item_slide in item.slides: +def alternatives(item_alternatives): + default(item_alternatives) + + +def question_answers(item_question_answers): + default(item_question_answers) + + +def competition(item_competition): + for item_slide in item_competition.slides: slide(item_slide) - # Remove all teams from competition - for item_team in item.teams: + for item_team in item_competition.teams: team(item_team) - - default(item) + default(item_competition) diff --git a/server/app/core/controller/edit.py b/server/app/core/controller/edit.py index c8b1633b..0a1b190f 100644 --- a/server/app/core/controller/edit.py +++ b/server/app/core/controller/edit.py @@ -31,6 +31,17 @@ def slide(item, title=None, timer=None): return item +def team(item_team, name=None, competition_id=None): + if name: + item_team.name = name + if competition_id: + item_team.competition_id = competition_id + + db.session.commit() + db.session.refresh(item_team) + return item_team + + def competition(item, name=None, year=None, city_id=None): if name: item.name = name @@ -61,3 +72,23 @@ def user(item, name=None, email=None, city_id=None, role_id=None): db.session.commit() db.session.refresh(item) return item + + +def question(item_question, name=None, total_score=None, type_id=None, slide_id=None): + + if name: + item_question.name = name + + if total_score: + item_question.total_score = total_score + + if type_id: + item_question.type_id = type_id + + if slide_id: + item_question.slide_id = slide_id + + db.session.commit() + db.session.refresh(item_question) + + return item_question diff --git a/server/app/core/controller/get.py b/server/app/core/controller/get.py index 7c3842d9..f695deee 100644 --- a/server/app/core/controller/get.py +++ b/server/app/core/controller/get.py @@ -1,4 +1,4 @@ -from app.core.models import Competition, Slide, Team, User +from app.core.models import Competition, Question, Slide, Team, User def slide_by_order(CID, order): @@ -13,6 +13,13 @@ 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) @@ -43,6 +50,59 @@ def search_user(email=None, name=None, city_id=None, role_id=None, page=0, page_ return _search(query, order_column, page, page_size, order) +def search_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) + if title: + query = query.filter(Slide.title.like(f"%{title}%")) + if body: + query = query.filter(Slide.body.like(f"%{body}%")) + if competition_id: + query = query.filter(Slide.competition_id == competition_id) + + order_column = Slide.id # Default order_by + if order_by: + order_column = getattr(Slide.__table__.c, order_by) + + return _search(query, order_column, page, page_size, order) + + +def search_questions( + name=None, + total_score=None, + type_id=None, + slide_id=None, + competition_id=None, + page=0, + page_size=15, + order=1, + order_by=None, +): + query = Question.query + if name: + query = query.filter(Question.name.like(f"%{name}%")) + if total_score: + query = query.filter(Question.total_score == total_score) + if type_id: + query = query.filter(Question.type_id == type_id) + if slide_id: + query = query.filter(Question.slide_id == slide_id) + if competition_id: + slide_ids = set( + [x.id for x in Slide.query.filter(Slide.competition_id == competition_id).all()] + ) # TODO: Filter using database instead of creating a set of slide_ids + query = query.filter(Question.slide_id.in_(slide_ids)) + + order_column = Question.id # Default order_by + 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: diff --git a/server/app/core/dto.py b/server/app/core/dto.py index 5eca7963..e2f36bbb 100644 --- a/server/app/core/dto.py +++ b/server/app/core/dto.py @@ -40,3 +40,9 @@ class MiscDTO: question_type_schema = schemas.QuestionTypeSchema(many=True) media_type_schema = schemas.MediaTypeSchema(many=True) city_schema = schemas.CitySchema(many=True) + + +class QuestionDTO: + api = Namespace("questions") + schema = rich_schemas.QuestionSchemaRich(many=False) + list_schema = schemas.QuestionSchema(many=True) diff --git a/server/app/core/http_codes.py b/server/app/core/http_codes.py index cfa3a2b1..95a99175 100644 --- a/server/app/core/http_codes.py +++ b/server/app/core/http_codes.py @@ -1,4 +1,5 @@ OK = 200 +NO_CONTENT = 204 BAD_REQUEST = 400 UNAUTHORIZED = 401 FORBIDDEN = 403 diff --git a/server/app/core/parsers.py b/server/app/core/parsers.py index d4aaf0ec..f05adc10 100644 --- a/server/app/core/parsers.py +++ b/server/app/core/parsers.py @@ -5,7 +5,7 @@ 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("order", type=int, default=1, location="args") -search_parser.add_argument("order_by", type=str, location="args") +search_parser.add_argument("order_by", type=str, default=None, location="args") ###LOGIN#### login_parser = reqparse.RequestParser() @@ -14,7 +14,6 @@ login_parser.add_argument("password", required=True, location="json") ###CREATE_USER#### create_user_parser = login_parser.copy() -create_user_parser.add_argument("email", type=inputs.email(), required=True, location="json") create_user_parser.add_argument("city_id", type=int, required=True, location="json") create_user_parser.add_argument("role_id", type=int, required=True, location="json") @@ -33,22 +32,35 @@ user_search_parser.add_argument("city_id", type=int, default=None, location="arg user_search_parser.add_argument("role_id", type=int, default=None, location="args") -###COMPETIION#### +###COMPETITION#### competition_parser = reqparse.RequestParser() -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("name", type=str, location="json") +competition_parser.add_argument("year", type=int, location="json") +competition_parser.add_argument("city_id", type=int, location="json") -###SEARCH_COMPETITOIN#### +###SEARCH_COMPETITION#### competition_search_parser = search_parser.copy() 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("year", type=int, default=None, location="args") competition_search_parser.add_argument("city_id", type=int, default=None, location="args") ###SLIDER_PARSER#### 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) +slide_parser.add_argument("order", type=int, default=None, location="json") +slide_parser.add_argument("title", type=str, default=None, location="json") +slide_parser.add_argument("timer", type=int, default=None, location="json") + + +###QUESTION#### +question_parser = reqparse.RequestParser() +question_parser.add_argument("name", type=str, default=None, location="json") +question_parser.add_argument("total_score", type=int, default=None, location="json") +question_parser.add_argument("slide_id", type=int, default=None, location="json") +question_parser.add_argument("type_id", type=int, default=None, location="json") + + +###TEAM#### +team_parser = reqparse.RequestParser() +team_parser.add_argument("name", type=str, location="json") diff --git a/server/app/core/rich_schemas.py b/server/app/core/rich_schemas.py index e4dc4a84..d714d76d 100644 --- a/server/app/core/rich_schemas.py +++ b/server/app/core/rich_schemas.py @@ -22,6 +22,17 @@ class UserSchemaRich(RichSchema): city = fields.Nested(schemas.CitySchema, many=False) +class QuestionSchemaRich(RichSchema): + class Meta(RichSchema.Meta): + model = models.Question + + id = ma.auto_field() + name = ma.auto_field() + total_score = ma.auto_field() + type = fields.Nested(schemas.QuestionTypeSchema, many=False) + slide = fields.Nested(schemas.SlideSchema, many=False) + + class CompetitionSchemaRich(RichSchema): class Meta(RichSchema.Meta): model = models.Competition @@ -31,5 +42,3 @@ class CompetitionSchemaRich(RichSchema): year = ma.auto_field() slides = fields.Nested(schemas.SlideSchema, many=True) city = fields.Nested(schemas.CitySchema, many=False) - - diff --git a/server/app/core/schemas.py b/server/app/core/schemas.py index 26fba469..c1d91c0d 100644 --- a/server/app/core/schemas.py +++ b/server/app/core/schemas.py @@ -18,6 +18,17 @@ class QuestionTypeSchema(BaseSchema): name = ma.auto_field() +class QuestionSchema(BaseSchema): + class Meta(BaseSchema.Meta): + model = models.Question + + id = ma.auto_field() + name = ma.auto_field() + total_score = ma.auto_field() + type_id = ma.auto_field() + slide_id = ma.auto_field() + + class MediaTypeSchema(BaseSchema): class Meta(BaseSchema.Meta): model = models.MediaType diff --git a/server/app/core/sockets.py b/server/app/core/sockets.py new file mode 100644 index 00000000..d9407a69 --- /dev/null +++ b/server/app/core/sockets.py @@ -0,0 +1,36 @@ +from flask.globals import request +from flask_socketio import SocketIO, emit, join_room + +sio = SocketIO(cors_allowed_origins="http://localhost:3000") + + +@sio.on("connect") +def connect(): + print(f"[Connected]: {request.sid}") + + +@sio.on("disconnect") +def disconnect(): + print(f"[Disconnected]: {request.sid}") + + +@sio.on("join_competition") +def join_competition(data): + competitionID = data["competitionID"] + join_room(data["competitionID"]) + print(f"[Join room]: {request.sid} -> {competitionID}") + + +@sio.on("sync_slide") +def sync_slide(data): + slide, competitionID = data["slide"], data["competitionID"] + emit("sync_slide", {"slide": slide}, room=competitionID, include_self=False) + print(f"[Sync slide]: {slide} -> {competitionID}") + + +@sio.on("sync_timer") +def sync_timer(data): + competitionID = data["competitionID"] + timer = data["timer"] + emit("sync_timer", {"timer": timer}, room=competitionID, include_self=False) + print(f"[Sync timer]: {competitionID=} {timer=}") diff --git a/server/tests/test_app.py b/server/tests/test_app.py index e4e0f290..38a103b9 100644 --- a/server/tests/test_app.py +++ b/server/tests/test_app.py @@ -1,119 +1,446 @@ +import app.core.http_codes as codes +from app.core.models import Slide + from tests import app, client, db -from tests.test_helpers import add_default_values, delete, get, post, put +from tests.test_helpers import add_default_values, change_order_test, delete, get, post, put -def test_misc(client): +def test_misc_api(client): add_default_values() # Login in with default user response, body = post(client, "/api/auth/login", {"email": "test@test.se", "password": "password"}) - assert response.status_code == 200 + assert response.status_code == codes.OK headers = {"Authorization": "Bearer " + body["access_token"]} ## Get misc response, body = get(client, "/api/misc/roles", headers=headers) + assert response.status_code == codes.OK assert body["count"] >= 2 response, body = get(client, "/api/misc/cities", headers=headers) - assert body["count"] >= 1 + assert response.status_code == codes.OK + assert body["count"] == 2 assert body["items"][0]["name"] == "Linköping" + assert body["items"][1]["name"] == "Testköping" response, body = get(client, "/api/misc/media_types", headers=headers) + assert response.status_code == codes.OK assert body["count"] >= 2 response, body = get(client, "/api/misc/question_types", headers=headers) + assert response.status_code == codes.OK assert body["count"] >= 3 ## Cities response, body = post(client, "/api/misc/cities", {"name": "Göteborg"}, headers=headers) + assert response.status_code == codes.OK assert body["count"] >= 2 - assert body["items"][1]["name"] == "Göteborg" + assert body["items"][2]["name"] == "Göteborg" - response, body = put(client, "/api/misc/cities/2", {"name": "Gbg"}, headers=headers) + # Rename city + response, body = put(client, "/api/misc/cities/3", {"name": "Gbg"}, headers=headers) + assert response.status_code == codes.OK assert body["count"] >= 2 - assert body["items"][1]["name"] == "Gbg" + assert body["items"][2]["name"] == "Gbg" + + # Delete city + # First checks current cities + response, body = get(client, "/api/misc/cities", headers=headers) + assert response.status_code == codes.OK + assert body["count"] == 3 + assert body["items"][0]["name"] == "Linköping" + assert body["items"][1]["name"] == "Testköping" + assert body["items"][2]["name"] == "Gbg" + # Deletes city + response, body = delete(client, "/api/misc/cities/3", headers=headers) + assert response.status_code == codes.OK + assert body["count"] == 2 + assert body["items"][0]["name"] == "Linköping" + assert body["items"][1]["name"] == "Testköping" -def test_competition(client): +def test_competition_api(client): add_default_values() # Login in with default user response, body = post(client, "/api/auth/login", {"email": "test@test.se", "password": "password"}) - assert response.status_code == 200 + assert response.status_code == codes.OK headers = {"Authorization": "Bearer " + body["access_token"]} # Create competition data = {"name": "c1", "year": 2020, "city_id": 1} response, body = post(client, "/api/competitions", data, headers=headers) - assert response.status_code == 200 + assert response.status_code == codes.OK assert body["name"] == "c1" + competition_id = body["id"] + + # Save number of slides + num_slides = len(Slide.query.all()) # Get competition - response, body = get(client, "/api/competitions/1", headers=headers) - assert response.status_code == 200 + response, body = get(client, f"/api/competitions/{competition_id}", headers=headers) + assert response.status_code == codes.OK assert body["name"] == "c1" - response, body = post(client, "/api/competitions/1/slides", {}, headers=headers) - assert response.status_code == 200 + response, body = post(client, f"/api/competitions/{competition_id}/slides", headers=headers) + assert response.status_code == codes.OK - response, body = get(client, "/api/competitions/1/slides", headers=headers) - assert response.status_code == 200 + response, body = get(client, f"/api/competitions/{competition_id}/slides", headers=headers) + assert response.status_code == codes.OK assert len(body["items"]) == 2 - response, body = put(client, "/api/competitions/1/slides/1/order", {"order": 1}, headers=headers) - assert response.status_code == 200 + response, body = put( + client, f"/api/competitions/{competition_id}/slides/{num_slides}/order", {"order": 1}, headers=headers + ) + assert response.status_code == codes.OK - response, body = post(client, "/api/competitions/1/teams", {"name": "t1"}, headers=headers) - assert response.status_code == 200 + response, body = post(client, f"/api/competitions/{competition_id}/teams", {"name": "t1"}, headers=headers) + assert response.status_code == codes.OK - response, body = get(client, "/api/competitions/1/teams", headers=headers) - assert response.status_code == 200 + response, body = get(client, f"/api/competitions/{competition_id}/teams", headers=headers) + assert response.status_code == codes.OK assert len(body["items"]) == 1 assert body["items"][0]["name"] == "t1" - response, body = delete(client, "/api/competitions/1", {}, headers=headers) - assert response.status_code == 200 + response, body = delete(client, f"/api/competitions/{competition_id}", headers=headers) + assert response.status_code == codes.OK -def test_app(client): +def test_auth_and_user_api(client): add_default_values() # Login in with default user response, body = post(client, "/api/auth/login", {"email": "test@test.se", "password": "password"}) - assert response.status_code == 200 + assert response.status_code == codes.OK headers = {"Authorization": "Bearer " + body["access_token"]} # Create user register_data = {"email": "test1@test.se", "password": "abc123", "role_id": 2, "city_id": 1} response, body = post(client, "/api/auth/signup", register_data, headers) - assert response.status_code == 200 + assert response.status_code == codes.OK assert body["id"] == 2 assert "password" not in body assert "_password" not in body + # Try to create user with same email + register_data = {"email": "test1@test.se", "password": "354213", "role_id": 1, "city_id": 1} + response, body = post(client, "/api/auth/signup", register_data, headers) + assert response.status_code == codes.BAD_REQUEST + # Try loggin with wrong PASSWORD response, body = post(client, "/api/auth/login", {"email": "test1@test.se", "password": "abc1234"}) - assert response.status_code == 401 + assert response.status_code == codes.UNAUTHORIZED # Try loggin with wrong Email response, body = post(client, "/api/auth/login", {"email": "testx@test.se", "password": "abc1234"}) - assert response.status_code == 401 + assert response.status_code == codes.UNAUTHORIZED # Try loggin with right PASSWORD response, body = post(client, "/api/auth/login", {"email": "test1@test.se", "password": "abc123"}) - assert response.status_code == 200 + assert response.status_code == codes.OK + refresh_token = body["refresh_token"] headers = {"Authorization": "Bearer " + body["access_token"]} # Get the current user response, body = get(client, "/api/users", headers=headers) - assert response.status_code == 200 + assert response.status_code == codes.OK assert body["email"] == "test1@test.se" # Edit current user name - response, body = put(client, "/api/users", {"name": "carl carlsson"}, headers=headers) - assert response.status_code == 200 + response, body = put(client, "/api/users", {"name": "carl carlsson", "city_id": 2, "role_id": 1}, headers=headers) + assert response.status_code == codes.OK assert body["name"] == "Carl Carlsson" + assert body["city"]["id"] == 2 + assert body["role"]["id"] == 1 + + # Find other user + response, body = get( + client, + "/api/users/search", + query_string={"name": "Olle Olsson", "email": "test@test.se", "role_id": 1, "city_id": 1}, + headers=headers, + ) + assert response.status_code == codes.OK + assert body["count"] == 1 + + # Get user from ID + searched_user = body["items"][0] + user_id = searched_user["id"] + response, body = get(client, f"/api/users/{user_id}", headers=headers) + assert response.status_code == codes.OK + assert searched_user["name"] == body["name"] + assert searched_user["email"] == body["email"] + assert searched_user["role_id"] == body["role"]["id"] + assert searched_user["city_id"] == body["city"]["id"] + assert searched_user["id"] == body["id"] + + # Edit user from ID + response, body = put(client, f"/api/users/{user_id}", {"email": "carl@carlsson.test"}, headers=headers) + assert response.status_code == codes.OK + assert body["email"] == "carl@carlsson.test" + + # Edit user from ID but add the same email as other user + response, body = put(client, f"/api/users/{user_id}", {"email": "test1@test.se"}, headers=headers) + assert response.status_code == codes.BAD_REQUEST + + # Delete other user + response, body = delete(client, f"/api/auth/delete/{user_id}", headers=headers) + assert response.status_code == codes.OK + + # Try to delete other user again + response, body = delete(client, f"/api/auth/delete/{user_id}", headers=headers) + assert response.status_code == codes.NOT_FOUND + + # Logout and try to access current user + response, body = post(client, f"/api/auth/logout", headers=headers) + assert response.status_code == codes.OK + + # TODO: Check if current users jwt (jti) is in blacklist after logging out + + response, body = get(client, "/api/users", headers=headers) + assert response.status_code == codes.UNAUTHORIZED + + # Login in again with default user + response, body = post(client, "/api/auth/login", {"email": "test1@test.se", "password": "abc123"}) + assert response.status_code == codes.OK + headers = {"Authorization": "Bearer " + body["access_token"]} + + # TODO: Add test for refresh api for current user + # response, body = post(client, "/api/auth/refresh", headers={**headers, "refresh_token": refresh_token}) + # assert response.status_code == codes.OK + + # Find current user + response, body = get(client, "/api/users", headers=headers) + assert response.status_code == codes.OK + assert body["email"] == "test1@test.se" + assert body["city"]["id"] == 2 + assert body["role"]["id"] == 1 + + # Delete current user + user_id = body["id"] + response, body = delete(client, f"/api/auth/delete/{user_id}", headers=headers) + assert response.status_code == codes.OK + + # TODO: Check that user was blacklisted + # Look for current users jwt in blacklist + # Blacklist.query.filter(Blacklist.jti == ) + + +def test_slide_api(client): + add_default_values() + + # Login in with default user + response, body = post(client, "/api/auth/login", {"email": "test@test.se", "password": "password"}) + assert response.status_code == codes.OK + headers = {"Authorization": "Bearer " + body["access_token"]} + + # Get slides from empty competition + CID = 1 + response, body = get(client, f"/api/competitions/{CID}/slides", headers=headers) + assert response.status_code == codes.OK + assert body["count"] == 0 + + # Get slides + CID = 2 + num_slides = 3 + response, body = get(client, f"/api/competitions/{CID}/slides", headers=headers) + assert response.status_code == codes.OK + assert body["count"] == num_slides + + # Add slide + response, body = post(client, f"/api/competitions/{CID}/slides", headers=headers) + num_slides += 1 + assert response.status_code == codes.OK + assert body["count"] == num_slides + + # Get slide + SID = 1 + response, item_slide = get(client, f"/api/competitions/{CID}/slides/{SID}", headers=headers) + assert response.status_code == codes.OK + assert item_slide["id"] == SID + + # Edit slide + order = 6 + title = "Ny titel" + body = "Ny body" + timer = 43 + assert item_slide["order"] != order + assert item_slide["title"] != title + # assert item_slide["body"] != body + assert item_slide["timer"] != timer + response, item_slide = put( + client, + f"/api/competitions/{CID}/slides/{SID}", + # TODO: Implement so these commented lines can be edited + # {"order": order, "title": title, "body": body, "timer": timer}, + {"title": title, "timer": timer}, + headers=headers, + ) + assert response.status_code == codes.OK + # assert item_slide["order"] == order + assert item_slide["title"] == title + # assert item_slide["body"] == body + assert item_slide["timer"] == timer + + # Delete slide + response, _ = delete(client, f"/api/competitions/{CID}/slides/{SID}", headers=headers) + num_slides -= 1 + assert response.status_code == codes.NO_CONTENT + # Checks that there are fewer slides + response, body = get(client, f"/api/competitions/{CID}/slides", headers=headers) + assert response.status_code == codes.OK + assert body["count"] == num_slides + + # Tries to delete slide again + response, _ = delete(client, f"/api/competitions/{CID}/slides/{SID}", headers=headers) + assert response.status_code == codes.NOT_FOUND + + # Changes the order to the same order + i = 0 + 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 + + # Changes the order + change_order_test(client, CID, SID, order + 1, headers) + + # Changes order to 0 + SID = 7 + change_order_test(client, CID, SID, -1, headers) + + +def test_question_api(client): + add_default_values() + + # Login in with default user + response, body = post(client, "/api/auth/login", {"email": "test@test.se", "password": "password"}) + assert response.status_code == codes.OK + headers = {"Authorization": "Bearer " + body["access_token"]} + + # Get questions from empty competition + CID = 1 # TODO: Fix api-calls so that the ones not using CID don't require one + response, body = get(client, f"/api/competitions/{CID}/questions", headers=headers) + assert response.status_code == codes.OK + assert body["count"] == 0 + + # Get questions from another competition that should have some questions + CID = 3 + num_questions = 3 + response, body = get(client, f"/api/competitions/{CID}/questions", headers=headers) + assert response.status_code == codes.OK + assert body["count"] == num_questions + + # # Get specific question + # name = "Q2" + # # total_score = 2 + # type_id = 5 + # slide_id = 5 + # response, body = get( + # client, + # f"/api/competitions/{CID}/questions/", + # headers=headers, + # ) + # # print(f"357: {body['items']}") + # assert response.status_code == codes.OK + # assert body["count"] == 1 + # item_question = body["items"][0] + # # print(f"338: {item_question}") + # 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 + + # Add question + name = "Nytt namn" + # total_score = 2 + type_id = 2 + slide_id = 5 + response, item_question = post( + client, + f"/api/competitions/{CID}/questions", + {"name": name, "type_id": type_id, "slide_id": slide_id}, + headers=headers, + ) + num_questions += 1 + assert response.status_code == codes.OK + 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 + # Checks number of questions + response, body = get(client, f"/api/competitions/{CID}/questions", headers=headers) + assert response.status_code == codes.OK + assert body["count"] == num_questions + + # Try to get question in another competition + QID = 1 + response, item_question = get(client, f"/api/competitions/{CID}/questions/{QID}", headers=headers) + assert response.status_code == codes.NOT_FOUND + + # Get question + QID = 4 + response, item_question = get(client, f"/api/competitions/{CID}/questions/{QID}", headers=headers) + assert response.status_code == codes.OK + assert item_question["id"] == QID + + # Try to edit question in another competition + name = "Nyare namn" + # total_score = 2 + type_id = 3 + slide_id = 1 + QID = 1 + response, _ = put( + client, + f"/api/competitions/{CID}/questions/{QID}", + # {"name": name, "total_score": total_score, "type_id": type_id, "slide_id": slide_id}, + {"name": name, "type_id": type_id, "slide_id": slide_id}, + headers=headers, + ) + assert response.status_code == codes.NOT_FOUND + # Checks number of questions + response, body = get(client, f"/api/competitions/{CID}/questions", headers=headers) + assert response.status_code == codes.OK + assert body["count"] == num_questions + + # Edit question + name = "Nyare namn" + # total_score = 2 + type_id = 3 + slide_id = 5 + QID = 4 + 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 + response, item_question = put( + client, + f"/api/competitions/{CID}/questions/{QID}", + # {"name": name, "total_score": total_score, "type_id": type_id, "slide_id": slide_id}, + {"name": name, "type_id": type_id, "slide_id": slide_id}, + headers=headers, + ) + assert response.status_code == codes.OK + 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 + # Checks number of questions + response, body = get(client, f"/api/competitions/{CID}/questions", headers=headers) + assert response.status_code == codes.OK + assert body["count"] == num_questions + + # Delete question + response, _ = delete(client, f"/api/competitions/{CID}/questions/{QID}", headers=headers) + num_questions -= 1 + assert response.status_code == codes.NO_CONTENT + + # Checks that there are fewer questions + response, body = get(client, f"/api/competitions/{CID}/questions", headers=headers) + assert response.status_code == codes.OK + assert body["count"] == num_questions - # Delete created user - response, body = delete(client, "/api/auth/delete/1", {}, headers=headers) - assert response.status_code == 200 + # Tries to delete question again + response, _ = delete(client, f"/api/competitions/{CID}/questions/{QID}", headers=headers) + assert response.status_code == codes.NOT_FOUND diff --git a/server/tests/test_db.py b/server/tests/test_db.py index 49ca53ad..68f49d5a 100644 --- a/server/tests/test_db.py +++ b/server/tests/test_db.py @@ -60,7 +60,7 @@ def test_question(client): item_competition_2 = Competition.query.filter_by(name="teknik9").first() assert item_competition is not None - assert item_competition.id == 1 + assert item_competition.id == 4 assert item_competition.city.name == "Linköping" # Add teams @@ -85,7 +85,7 @@ def test_question(client): # Try add slide with same order assert_insert_fail(Slide, 1, item_competition.id) - assert_exists(Slide, 1, order=1) + assert_exists(Slide, 3, order=1) item_slide1 = Slide.query.filter_by(order=0).first() item_slide2 = Slide.query.filter_by(order=1).first() @@ -109,3 +109,62 @@ def test_question(client): item_q2 = Question.query.filter_by(name="Fråga2").first() assert item_q1.type.name == "Boolean" assert item_q2.type.name == "Multiple" + + # Get question + CID = 3 + QID = 4 + item_q1 = dbc.get.question(CID, QID) + assert item_q1.id == QID + item_slide = dbc.get.slide(CID, item_q1.slide_id) + assert item_q1.slide_id == item_slide.id + + # Edit question + print(item_q1.type_id) + print(item_q1.slide_id) + name = "Nytt namn" + total_score = 44 + type_id = 2 + slide_id = 4 + dbc.edit.question(item_q1, name=name, total_score=total_score, type_id=type_id, slide_id=slide_id) + item_q1 = Question.query.filter_by(name=name).first() + assert item_q1.name == name + assert item_q1.total_score == total_score + assert item_q1.type_id == type_id + assert item_q1.slide_id == slide_id + + # Search for question + item_q2, _ = dbc.get.search_questions( + name=name, total_score=total_score, type_id=type_id, slide_id=slide_id, competition_id=CID + ) + assert item_q1 == item_q2[0] + + +def test_slide(client): + add_default_values() + + # Get all slides + slides = Slide.query.all() + item_slides = dbc.get.search_slide() + assert slides == item_slides[0] + + # Search using all parameters + item_comp = Competition.query.filter(Competition.name == "Tävling 1").first() + aux = dbc.get.search_slide(slide_order=1, title="Title 1", body="Body 1", competition_id=item_comp.id) + item_slide = aux[0][0] + assert item_comp.slides[1] == item_slide + + # Edit all parameters of a slide + title = "Ändrad titel" + timer = 42 + slide_id = item_slide.id + dbc.edit.slide(item_slide, title=title, timer=timer) + aux = dbc.get.search_slide(slide_order=1, title=title, body="Body 1", competition_id=item_comp.id) + item_slide = aux[0][0] + assert item_slide.id == slide_id + assert item_slide.title == title + assert item_slide.timer == timer + + # Delete slide + 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 6ce0f54c..fbb58ac1 100644 --- a/server/tests/test_helpers.py +++ b/server/tests/test_helpers.py @@ -1,15 +1,16 @@ import json import app.core.controller as dbc +import app.core.http_codes as codes from app.core import db -from app.core.models import City, MediaType, QuestionType, Role, User +from app.core.models import City, Role def add_default_values(): media_types = ["Image", "Video"] question_types = ["Boolean", "Multiple", "Text"] roles = ["Admin", "Editor"] - cities = ["Linköping"] + cities = ["Linköping", "Testköping"] # Add media types for item in media_types: @@ -29,7 +30,26 @@ def add_default_values(): item_admin = Role.query.filter(Role.name == "Admin").one() item_city = City.query.filter(City.name == "Linköping").one() # Add user with role and city - dbc.add.user("test@test.se", "password", item_admin.id, item_city.id) + dbc.add.user("test@test.se", "password", item_admin.id, item_city.id, "Olle Olsson") + + # Add competitions + dbc.add.competition("Tom tävling", 2012, item_city.id) + for j in range(2): + item_comp = dbc.add.competition(f"Tävling {j}", 2012, item_city.id) + + # Add slides + for i in range(len(question_types)): + # Add slide to competition + item_slide = dbc.add.slide(item_comp) + + # Populate slide with data + item_slide.title = f"Title {i}" + item_slide.body = f"Body {i}" + item_slide.timer = 100 + i + # item_slide.settings = "{}" + + # Add question to competition + dbc.add.question(name=f"Q{i+1}", total_score=i + 1, type_id=i + 1, item_slide=item_slide) def get_body(response): @@ -40,33 +60,38 @@ def get_body(response): return body -def post(client, url, data, headers=None): +def post(client, url, data=None, headers=None): if headers is None: headers = {} headers["Content-Type"] = "application/json" - response = client.post(url, data=json.dumps(data), headers=headers) + response = client.post(url, json=data, headers=headers) body = get_body(response) return response, body def get(client, url, query_string=None, headers=None): + if headers is None: + headers = {} + headers["Content-Type"] = "" response = client.get(url, query_string=query_string, headers=headers) body = get_body(response) return response, body -def put(client, url, data, headers=None): +def put(client, url, data=None, headers=None): if headers is None: headers = {} headers["Content-Type"] = "application/json" - - response = client.put(url, data=json.dumps(data), headers=headers) + response = client.put(url, json=data, headers=headers) body = get_body(response) return response, body -def delete(client, url, data, headers=None): - response = client.delete(url, data=json.dumps(data), headers=headers) +def delete(client, url, data=None, headers=None): + if headers is None: + headers = {} + headers["Content-Type"] = "" + response = client.delete(url, json=data, headers=headers) body = get_body(response) return response, body @@ -90,3 +115,40 @@ def assert_exists(db_type, length, **kwargs): def assert_object_values(obj, values): for k, v in values.items(): assert getattr(obj, k) == v + + +# Changes order of slides +def change_order_test(client, cid, sid, order, h): + sid_at_order = -1 + actual_order = 0 if order < 0 else order # used to find the slide_id + response, body = get(client, f"/api/competitions/{cid}/slides", headers=h) + assert response.status_code == codes.OK + + # Finds the slide_id of the slide that will be swapped with + for item_slide in body["items"]: + if item_slide["order"] == actual_order: + assert item_slide["id"] != sid + sid_at_order = item_slide["id"] + assert sid_at_order != -1 + + # Gets old versions of slides + response, item_slide_10 = get(client, f"/api/competitions/{cid}/slides/{sid}", headers=h) + assert response.status_code == codes.OK + response, item_slide_20 = get(client, f"/api/competitions/{cid}/slides/{sid_at_order}", headers=h) + assert response.status_code == codes.OK + + # Changes order + response, _ = put( + client, f"/api/competitions/{cid}/slides/{sid}/order", {"order": order}, headers=h + ) # uses order to be able to test negative order + assert response.status_code == codes.OK + + # Gets new versions of slides + response, item_slide_11 = get(client, f"/api/competitions/{cid}/slides/{sid}", headers=h) + assert response.status_code == codes.OK + response, item_slide_21 = get(client, f"/api/competitions/{cid}/slides/{sid_at_order}", headers=h) + assert response.status_code == codes.OK + + # Checks that the order was indeed swapped + assert item_slide_10["order"] == item_slide_21["order"] + assert item_slide_11["order"] == item_slide_20["order"] -- GitLab