diff --git a/client/src/actions/types.ts b/client/src/actions/types.ts index a8736c4b26b751a48f6f01d888dc46e630c4f6ea..d1084c2077290b65b27531d9d9234f5eb521eb1d 100644 --- a/client/src/actions/types.ts +++ b/client/src/actions/types.ts @@ -23,4 +23,5 @@ export default { SET_CITIES: 'SET_CITIES', SET_CITIES_TOTAL: 'SET_CITIES_TOTAL', SET_CITIES_COUNT: 'SET_CITIES_COUNT', + SET_TYPES: 'SET_TYPES', } diff --git a/client/src/actions/typesAction.ts b/client/src/actions/typesAction.ts new file mode 100644 index 0000000000000000000000000000000000000000..4fcde4f55ee72cb9f72f6d79f0e5f1db6995fa4b --- /dev/null +++ b/client/src/actions/typesAction.ts @@ -0,0 +1,15 @@ +import axios from 'axios' +import { AppDispatch } from './../store' +import Types from './types' + +export const getTypes = () => async (dispatch: AppDispatch) => { + await axios + .get('/misc/types') + .then((res) => { + dispatch({ + type: Types.SET_TYPES, + payload: res.data, + }) + }) + .catch((err) => console.log(err)) +} diff --git a/client/src/interfaces/ApiModels.ts b/client/src/interfaces/ApiModels.ts index 194fb2e727449b39befe2e1d43ee9913cca1ee00..0ffc91af0f04c49e11595d1aa5f7a40a5ae56c45 100644 --- a/client/src/interfaces/ApiModels.ts +++ b/client/src/interfaces/ApiModels.ts @@ -1,4 +1,4 @@ -interface NameID { +export interface NameID { id: number name: string } @@ -6,6 +6,15 @@ export interface City extends NameID {} export interface Role extends NameID {} export interface MediaType extends NameID {} export interface QuestionType extends NameID {} +export interface ComponentType extends NameID {} +export interface ViewType extends NameID {} + +export interface AllTypes { + media_types: MediaType[] + question_types: QuestionType[] + component_types: ComponentType[] + view_types: ViewType[] +} export interface Media { id: number diff --git a/client/src/pages/admin/AdminPage.tsx b/client/src/pages/admin/AdminPage.tsx index a0dbe2bd54f721ddad785b5b03776c64f87d2030..8bbb68fa80ce1a20ea72a2b9d4de49ad18a3bb83 100644 --- a/client/src/pages/admin/AdminPage.tsx +++ b/client/src/pages/admin/AdminPage.tsx @@ -20,6 +20,7 @@ import React, { useEffect } from 'react' import { Link, Route, Switch, useRouteMatch } from 'react-router-dom' import { getCities } from '../../actions/cities' import { getRoles } from '../../actions/roles' +import { getTypes } from '../../actions/typesAction' import { logoutUser } from '../../actions/user' import { useAppDispatch, useAppSelector } from '../../hooks' import CompetitionManager from './competitions/CompetitionManager' @@ -57,12 +58,17 @@ const AdminView: React.FC = () => { const classes = useStyles() const [openIndex, setOpenIndex] = React.useState(0) const { path, url } = useRouteMatch() + const currentUser = useAppSelector((state) => state.user.userInfo) + const isAdmin = () => currentUser && currentUser.role.name === 'Admin' + const dispatch = useAppDispatch() const handleLogout = () => { dispatch(logoutUser()) } - const dispatch = useAppDispatch() - const currentUser = useAppSelector((state) => state.user.userInfo) - const isAdmin = () => currentUser && currentUser.role.name === 'Admin' + useEffect(() => { + dispatch(getCities()) + dispatch(getRoles()) + dispatch(getTypes()) + }, []) const menuAdminItems = [ { text: 'Startsida', icon: DashboardIcon }, @@ -93,11 +99,6 @@ const AdminView: React.FC = () => { )) } - useEffect(() => { - dispatch(getCities()) - dispatch(getRoles()) - }, []) - return ( <div className={classes.root}> <CssBaseline /> diff --git a/client/src/reducers/allReducers.ts b/client/src/reducers/allReducers.ts index 94743ff18c546c80e9ce7400e5afdace7e871bf4..d0f9e8012d5e85f9352ac239499d49fc0a7e7322 100644 --- a/client/src/reducers/allReducers.ts +++ b/client/src/reducers/allReducers.ts @@ -6,6 +6,7 @@ import competitionsReducer from './competitionsReducer' import presentationReducer from './presentationReducer' import rolesReducer from './rolesReducer' import searchUserReducer from './searchUserReducer' +import typesReducer from './typesReducer' import uiReducer from './uiReducer' import userReducer from './userReducer' @@ -18,5 +19,6 @@ const allReducers = combineReducers({ presentation: presentationReducer, roles: rolesReducer, searchUsers: searchUserReducer, + types: typesReducer, }) export default allReducers diff --git a/client/src/reducers/typesReducer.ts b/client/src/reducers/typesReducer.ts new file mode 100644 index 0000000000000000000000000000000000000000..3540ef86fbd4a921738d896b2b0bebb14b3216e0 --- /dev/null +++ b/client/src/reducers/typesReducer.ts @@ -0,0 +1,29 @@ +import { AnyAction } from 'redux' +import Types from '../actions/types' +import { ComponentType, MediaType, QuestionType, ViewType } from '../interfaces/ApiModels' + +interface TypesState { + componentTypes: ComponentType[] + viewTypes: ViewType[] + questionTypes: QuestionType[] + mediaTypes: MediaType[] +} +const initialState: TypesState = { + componentTypes: [], + viewTypes: [], + questionTypes: [], + mediaTypes: [], +} + +export default function (state = initialState, action: AnyAction) { + switch (action.type) { + case Types.SET_TYPES: + state.componentTypes = action.payload.component_types as ComponentType[] + state.viewTypes = action.payload.view_types as ViewType[] + state.questionTypes = action.payload.question_types as QuestionType[] + state.mediaTypes = action.payload.media_types as MediaType[] + return state + default: + return state + } +} diff --git a/server/app/__init__.py b/server/app/__init__.py index 8aa7a08a36108f683361768752aaf3e0f47e737e..a25293952d75dd64ff86e4fe76b105447edf8cfe 100644 --- a/server/app/__init__.py +++ b/server/app/__init__.py @@ -1,5 +1,5 @@ from flask import Flask, redirect, request -from flask_uploads import IMAGES, UploadSet, configure_uploads +from flask_uploads import configure_uploads 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 3b2ed213ac140a49096aeee16cdd3924a76ae30d..b48b8b33704902bca098e76af9080cf13cf6074c 100644 --- a/server/app/apis/__init__.py +++ b/server/app/apis/__init__.py @@ -23,7 +23,7 @@ def admin_required(): def text_response(message, code=codes.OK): - return {"message": message}, codes.OK + return {"message": message}, code def list_response(items, total=None, code=codes.OK): @@ -43,7 +43,9 @@ def item_response(item, code=codes.OK): from flask_restx import Api from .auth import api as auth_ns +from .codes import api as code_ns from .competitions import api as comp_ns +from .components import api as component_ns from .media import api as media_ns from .misc import api as misc_ns from .questions import api as question_ns @@ -59,5 +61,6 @@ 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") -# flask_api.add_namespace(question_ns, path="/api/competitions/<CID>/slides/<SID>/question") +flask_api.add_namespace(code_ns, path="/api/competitions/<CID>/codes") +flask_api.add_namespace(question_ns, path="/api/competitions/<CID>") +flask_api.add_namespace(component_ns, path="/api/competitions/<CID>/slides/<SOrder>/components") diff --git a/server/app/apis/auth.py b/server/app/apis/auth.py index 1510df642682f2d00d94bb695af45e7fb1cf24f4..10d820f88d1a5570635a05e54a2ef45492a1c645 100644 --- a/server/app/apis/auth.py +++ b/server/app/apis/auth.py @@ -1,7 +1,8 @@ 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.codes import verify_code +from app.core.dto import AuthDTO, CodeDTO from app.core.parsers import create_user_parser, login_parser from app.database.models import User from flask_jwt_extended import ( @@ -69,6 +70,20 @@ class AuthLogin(Resource): return response +@api.route("/login/<code>") +@api.param("code") +class AuthLogin(Resource): + def post(self, code): + if not verify_code(code): + api.abort(codes.BAD_REQUEST, "Invalid code") + + item_code = dbc.get.code_by_code(code) + if not item_code: + api.abort(codes.UNAUTHORIZED, "A presentation with that code does not exist") + + return item_response(CodeDTO.schema.dump(item_code)), codes.OK + + @api.route("/logout") class AuthLogout(Resource): @jwt_required diff --git a/server/app/apis/codes.py b/server/app/apis/codes.py new file mode 100644 index 0000000000000000000000000000000000000000..af6aee8499dd595ecb189a72f9f575a96511bee1 --- /dev/null +++ b/server/app/apis/codes.py @@ -0,0 +1,32 @@ +import app.database.controller as dbc +from app.apis import admin_required, item_response, list_response +from app.core import http_codes as codes +from app.core.dto import CodeDTO +from app.core.parsers import code_parser +from app.database.models import Code, Competition +from flask_jwt_extended import jwt_required +from flask_restx import Resource + +api = CodeDTO.api +schema = CodeDTO.schema +list_schema = CodeDTO.list_schema + + +@api.route("/") +@api.param("CID") +class CodesList(Resource): + @jwt_required + def get(self, CID): + items = dbc.get.code_list(CID) + return list_response(list_schema.dump(items), len(items)), codes.OK + + +@api.route("/<code_id>") +@api.param("CID, code_id") +class CodesById(Resource): + @jwt_required + def put(self, CID, code_id): + item = dbc.get.one(Code, code_id) + item.code = dbc.utils.generate_unique_code() + dbc.utils.commit_and_refresh(item) + return item_response(schema.dump(item)), codes.OK diff --git a/server/app/apis/competitions.py b/server/app/apis/competitions.py index be3762062647a576be9f76861961d2a30cb5d5fe..26b6f363831fb4508d75898c666e25897322eec0 100644 --- a/server/app/apis/competitions.py +++ b/server/app/apis/competitions.py @@ -30,20 +30,20 @@ class CompetitionsList(Resource): class Competitions(Resource): @jwt_required def get(self, CID): - item = dbc.get.competition(CID) + item = dbc.get.one(Competition, CID) return item_response(schema.dump(item)) @jwt_required def put(self, CID): args = competition_parser.parse_args(strict=True) - item = dbc.get.competition(CID) + item = dbc.get.one(Competition, CID) item = dbc.edit.competition(item, **args) return item_response(schema.dump(item)) @jwt_required def delete(self, CID): - item = dbc.get.competition(CID) + item = dbc.get.one(Competition, CID) dbc.delete.competition(item) return "deleted" diff --git a/server/app/apis/components.py b/server/app/apis/components.py index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..f88e1e2de7eeca66859a46d5a790dbe6ef84662a 100644 --- a/server/app/apis/components.py +++ b/server/app/apis/components.py @@ -0,0 +1,50 @@ +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 ComponentDTO +from app.core.parsers import component_create_parser, component_parser +from app.database.models import Competition, Component +from flask.globals import request +from flask_jwt_extended import jwt_required +from flask_restx import Resource + +api = ComponentDTO.api +schema = ComponentDTO.schema +list_schema = ComponentDTO.list_schema + + +@api.route("/<component_id>") +@api.param("CID, SOrder, component_id") +class ComponentByID(Resource): + @jwt_required + def get(self, CID, SOrder, component_id): + item = dbc.get.one(Component, component_id) + return item_response(schema.dump(item)) + + @jwt_required + def put(self, CID, SOrder, component_id): + args = component_parser.parse_args() + item = dbc.edit.component(**args) + return item_response(schema.dump(item)) + + @jwt_required + def delete(self, CID, SOrder, component_id): + item = dbc.get.one(Component, component_id) + dbc.delete.component(item) + return {}, codes.NO_CONTENT + + +@api.route("/") +@api.param("CID, SOrder") +class ComponentList(Resource): + @jwt_required + def get(self, CID, SOrder): + items = dbc.get.component_list(SOrder) + return list_response(list_schema.dump(items)) + + @jwt_required + def post(self, CID, SOrder): + args = component_create_parser.parse_args() + item_slide = dbc.get.slide(CID, SOrder) + item = dbc.add.component(item_slide=item_slide, **args) + return item_response(schema.dump(item)) diff --git a/server/app/apis/misc.py b/server/app/apis/misc.py index 7fbb222d23408b59de47485e21e8c16653ce1ffa..a78c2f8c82ee7b44740226babc0ededd8485b978 100644 --- a/server/app/apis/misc.py +++ b/server/app/apis/misc.py @@ -1,7 +1,7 @@ import app.database.controller as dbc from app.apis import admin_required, item_response, list_response from app.core.dto import MiscDTO -from app.database.models import City, MediaType, QuestionType, Role +from app.database.models import City, ComponentType, MediaType, QuestionType, Role, ViewType from flask_jwt_extended import jwt_required from flask_restx import Resource, reqparse @@ -9,6 +9,9 @@ api = MiscDTO.api question_type_schema = MiscDTO.question_type_schema media_type_schema = MiscDTO.media_type_schema +component_type_schema = MiscDTO.component_type_schema +view_type_schema = MiscDTO.view_type_schema + role_schema = MiscDTO.role_schema city_schema = MiscDTO.city_schema @@ -17,27 +20,23 @@ name_parser = reqparse.RequestParser() name_parser.add_argument("name", type=str, required=True, location="json") -@api.route("/media_types") -class MediaTypeList(Resource): - @jwt_required - def get(self): - items = MediaType.query.all() - return list_response(media_type_schema.dump(items)) - - -@api.route("/question_types") -class QuestionTypeList(Resource): +@api.route("/types") +class TypesList(Resource): @jwt_required def get(self): - items = QuestionType.query.all() - return list_response(question_type_schema.dump(items)) + result = {} + result["media_types"] = media_type_schema.dump(dbc.get.all(MediaType)) + result["component_types"] = component_type_schema.dump(dbc.get.all(ComponentType)) + result["question_types"] = question_type_schema.dump(dbc.get.all(QuestionType)) + result["view_types"] = view_type_schema.dump(dbc.get.all(ViewType)) + return result @api.route("/roles") class RoleList(Resource): @jwt_required def get(self): - items = Role.query.all() + items = dbc.get.all(Role) return list_response(role_schema.dump(items)) @@ -45,14 +44,14 @@ class RoleList(Resource): class CitiesList(Resource): @jwt_required def get(self): - items = City.query.all() + items = dbc.get.all(City) return list_response(city_schema.dump(items)) @jwt_required def post(self): args = name_parser.parse_args(strict=True) dbc.add.city(args["name"]) - items = City.query.all() + items = dbc.get.all(City) return list_response(city_schema.dump(items)) @@ -61,16 +60,16 @@ class CitiesList(Resource): class Cities(Resource): @jwt_required def put(self, ID): - item = City.query.filter(City.id == ID).first() + item = dbc.get.one(City, ID) args = name_parser.parse_args(strict=True) item.name = args["name"] - dbc.commit_and_refresh(item) - items = City.query.all() + dbc.utils.commit_and_refresh(item) + items = dbc.get.all(City) return list_response(city_schema.dump(items)) @jwt_required def delete(self, ID): - item = City.query.filter(City.id == ID).first() + item = dbc.get.one(City, ID) dbc.delete.default(item) - items = City.query.all() + items = dbc.get.all(City) return list_response(city_schema.dump(items)) diff --git a/server/app/apis/questions.py b/server/app/apis/questions.py index 86929bb4eb6f9dc90164845e45c281e4b3620afc..55db2819949adae273c230c0f9548a6b2843db37 100644 --- a/server/app/apis/questions.py +++ b/server/app/apis/questions.py @@ -12,7 +12,7 @@ schema = QuestionDTO.schema list_schema = QuestionDTO.list_schema -@api.route("/") +@api.route("/questions") @api.param("CID") class QuestionsList(Resource): @jwt_required @@ -20,40 +20,40 @@ class QuestionsList(Resource): items = dbc.get.question_list(CID) return list_response(list_schema.dump(items)) + +@api.route("/slides/<SID>/questions") +@api.param("CID, SID") +class QuestionsList(Resource): @jwt_required - def post(self, CID): + def post(self, SID, CID): args = question_parser.parse_args(strict=True) + del args["slide_id"] - 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) + item_slide = dbc.get.slide(CID, SID) + item = dbc.add.question(item_slide=item_slide, **args) return item_response(schema.dump(item)) -@api.route("/<QID>") -@api.param("CID,QID") +@api.route("/slides/<SID>/questions/<QID>") +@api.param("CID, SID, QID") class Questions(Resource): @jwt_required - def get(self, CID, QID): - item_question = dbc.get.question(CID, QID) + def get(self, CID, SID, QID): + item_question = dbc.get.question(CID, SID, QID) return item_response(schema.dump(item_question)) @jwt_required - def put(self, CID, QID): + def put(self, CID, SID, QID): args = question_parser.parse_args(strict=True) - item_question = dbc.get.question(CID, QID) + item_question = dbc.get.question(CID, SID, QID) 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) + def delete(self, CID, SID, QID): + item_question = dbc.get.question(CID, SID, QID) dbc.delete.question(item_question) return {}, codes.NO_CONTENT diff --git a/server/app/apis/slides.py b/server/app/apis/slides.py index 972e1bd622e31d4fa18d717c9ba5718d7a75b837..9aeb793ad0e6e967eda3055d9490485f63a35547 100644 --- a/server/app/apis/slides.py +++ b/server/app/apis/slides.py @@ -22,49 +22,49 @@ class SlidesList(Resource): @jwt_required def post(self, CID): - item_comp = dbc.get.competition(CID) + item_comp = dbc.get.one(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) + dbc.utils.refresh(item_comp) return list_response(list_schema.dump(item_comp.slides)) -@api.route("/<SID>") -@api.param("CID,SID") +@api.route("/<SOrder>") +@api.param("CID,SOrder") class Slides(Resource): @jwt_required - def get(self, CID, SID): - item_slide = dbc.get.slide(CID, SID) + def get(self, CID, SOrder): + item_slide = dbc.get.slide(CID, SOrder) return item_response(schema.dump(item_slide)) @jwt_required - def put(self, CID, SID): + def put(self, CID, SOrder): args = slide_parser.parse_args(strict=True) title = args.get("title") timer = args.get("timer") - item_slide = dbc.get.slide(CID, SID) + item_slide = dbc.get.slide(CID, SOrder) item_slide = dbc.edit.slide(item_slide, title, timer) return item_response(schema.dump(item_slide)) @jwt_required - def delete(self, CID, SID): - item_slide = dbc.get.slide(CID, SID) + def delete(self, CID, SOrder): + item_slide = dbc.get.slide(CID, SOrder) dbc.delete.slide(item_slide) return {}, codes.NO_CONTENT -@api.route("/<SID>/order") -@api.param("CID,SID") +@api.route("/<SOrder>/order") +@api.param("CID,SOrder") class SlidesOrder(Resource): @jwt_required - def put(self, CID, SID): + def put(self, CID, SOrder): args = slide_parser.parse_args(strict=True) order = args.get("order") - item_slide = dbc.get.slide(CID, SID) + item_slide = dbc.get.slide(CID, SOrder) if order == item_slide.order: return item_response(schema.dump(item_slide)) @@ -77,7 +77,7 @@ class SlidesOrder(Resource): order = order_count - 1 # get slide at the requested order - item_slide_order = dbc.get.slide_by_order(CID, order) + item_slide_order = dbc.get.slide(CID, order) # switch place between them item_slide = dbc.edit.switch_order(item_slide, item_slide_order) diff --git a/server/app/apis/teams.py b/server/app/apis/teams.py index 9600e2a4c4ce7fe170162e5e7e2eedeed617b5b8..6729ccf8984e6ba85de5b79e2cfa0c3edde0f4ee 100644 --- a/server/app/apis/teams.py +++ b/server/app/apis/teams.py @@ -23,7 +23,7 @@ class TeamsList(Resource): @jwt_required def post(self, CID): args = team_parser.parse_args(strict=True) - item_comp = dbc.get.competition(CID) + item_comp = dbc.get.one(Competition,CID) item_team = dbc.add.team(args["name"], item_comp) return item_response(schema.dump(item_team)) diff --git a/server/app/core/__init__.py b/server/app/core/__init__.py index 09c321efa46844b46acc67a0e5513df9d553746b..acfca55431cf0f9b538e1a1487cea7299e3ad4e4 100644 --- a/server/app/core/__init__.py +++ b/server/app/core/__init__.py @@ -1,4 +1,4 @@ -from app.database.base import Base, ExtendedQuery +from app.database import Base, ExtendedQuery from flask_bcrypt import Bcrypt from flask_jwt_extended.jwt_manager import JWTManager from flask_marshmallow import Marshmallow diff --git a/server/app/core/codes.py b/server/app/core/codes.py new file mode 100644 index 0000000000000000000000000000000000000000..47150767b17fb1b352f0b1ae3130dc6d4ffefb5a --- /dev/null +++ b/server/app/core/codes.py @@ -0,0 +1,15 @@ +import random +import re +import string + +CODE_LENGTH = 6 +ALLOWED_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" +CODE_RE = re.compile(f"^[{ALLOWED_CHARS}]{{{CODE_LENGTH}}}$") + + +def generate_code_string(): + return "".join(random.choices(ALLOWED_CHARS, k=CODE_LENGTH)) + + +def verify_code(c): + return CODE_RE.search(c.upper()) is not None diff --git a/server/app/core/dto.py b/server/app/core/dto.py index 99d467ba7251962de5de2ec24ed1cbb150d001ed..6541ef75903db43ecd9a239e3b970d1e4506e240 100644 --- a/server/app/core/dto.py +++ b/server/app/core/dto.py @@ -1,10 +1,15 @@ import app.core.rich_schemas as rich_schemas import app.core.schemas as schemas -import marshmallow as ma -from flask_restx import Namespace, fields +from flask_restx import Namespace from flask_uploads import IMAGES, UploadSet +class ComponentDTO: + api = Namespace("component") + schema = schemas.ComponentSchema(many=False) + list_schema = schemas.ComponentSchema(many=True) + + class MediaDTO: api = Namespace("media") image_set = UploadSet("photos", IMAGES) @@ -30,6 +35,12 @@ class CompetitionDTO: list_schema = schemas.CompetitionSchema(many=True) +class CodeDTO: + api = Namespace("codes") + schema = rich_schemas.CodeSchemaRich(many=False) + list_schema = schemas.CodeSchema(many=True) + + class SlideDTO: api = Namespace("slides") schema = schemas.SlideSchema(many=False) @@ -47,6 +58,8 @@ class MiscDTO: role_schema = schemas.RoleSchema(many=True) question_type_schema = schemas.QuestionTypeSchema(many=True) media_type_schema = schemas.MediaTypeSchema(many=True) + component_type_schema = schemas.ComponentTypeSchema(many=True) + view_type_schema = schemas.ViewTypeSchema(many=True) city_schema = schemas.CitySchema(many=True) diff --git a/server/app/core/parsers.py b/server/app/core/parsers.py index 7a1c6089c1893a01d70b858e13097eb84a74029b..f691ea8d3924a8679068713bf85ca47e2d065376 100644 --- a/server/app/core/parsers.py +++ b/server/app/core/parsers.py @@ -57,8 +57,13 @@ slide_parser.add_argument("timer", type=int, default=None, location="json") 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") +question_parser.add_argument("slide_id", type=int, location="json") + +###QUESTION#### +code_parser = reqparse.RequestParser() +code_parser.add_argument("pointer", type=str, default=None, location="json") +code_parser.add_argument("view_type_id", type=int, default=None, location="json") ###TEAM#### @@ -68,3 +73,16 @@ team_parser.add_argument("name", type=str, location="json") ###SEARCH_COMPETITION#### media_parser_search = search_parser.copy() media_parser_search.add_argument("filename", type=str, default=None, location="args") + + +###COMPONENT### +component_parser = reqparse.RequestParser() +component_parser.add_argument("x", type=str, default=None, location="json") +component_parser.add_argument("y", type=int, default=None, location="json") +component_parser.add_argument("w", type=int, default=None, location="json") +component_parser.add_argument("h", type=int, default=None, location="json") +component_parser.add_argument("data", type=dict, default=None, location="json") + +component_create_parser = component_parser.copy() +component_create_parser.replace_argument("data", type=dict, required=True, location="json") +component_create_parser.add_argument("type_id", type=int, required=True, location="json") diff --git a/server/app/core/rich_schemas.py b/server/app/core/rich_schemas.py index ab1b3abb9ff50f8888fe13bf5c89b4580faee01b..fa6daac9ef59117b76ab9d2244f80af3224dbbe5 100644 --- a/server/app/core/rich_schemas.py +++ b/server/app/core/rich_schemas.py @@ -43,6 +43,16 @@ class TeamSchemaRich(RichSchema): question_answers = fields.Nested(schemas.QuestionAnswerSchema, many=True) +class CodeSchemaRich(RichSchema): + class Meta(RichSchema.Meta): + model = models.Code + + id = ma.auto_field() + code = ma.auto_field() + pointer = ma.auto_field() + view_type = fields.Nested(schemas.ViewTypeSchema, many=False) + + class SlideSchemaRich(RichSchema): class Meta(RichSchema.Meta): model = models.Slide @@ -53,6 +63,7 @@ class SlideSchemaRich(RichSchema): timer = ma.auto_field() competition_id = ma.auto_field() questions = fields.Nested(QuestionSchemaRich, many=True) + components = fields.Nested(schemas.ComponentSchema, many=True) class CompetitionSchemaRich(RichSchema): diff --git a/server/app/core/schemas.py b/server/app/core/schemas.py index 274406420a2f7f55832f8676b2995276704a8f31..4f9646a55e2e64d9498f96589b59886824845a45 100644 --- a/server/app/core/schemas.py +++ b/server/app/core/schemas.py @@ -10,12 +10,40 @@ class BaseSchema(ma.SQLAlchemySchema): include_relationships = False -class QuestionTypeSchema(BaseSchema): +class IdNameSchema(BaseSchema): + + id = fields.fields.Integer() + name = fields.fields.String() + + +class QuestionTypeSchema(IdNameSchema): class Meta(BaseSchema.Meta): model = models.QuestionType + +class MediaTypeSchema(IdNameSchema): + class Meta(BaseSchema.Meta): + model = models.MediaType + + +class ComponentTypeSchema(IdNameSchema): + class Meta(BaseSchema.Meta): + model = models.ComponentType + + +class CodeSchema(IdNameSchema): + class Meta(BaseSchema.Meta): + model = models.Code + id = ma.auto_field() - name = ma.auto_field() + code = ma.auto_field() + pointer = ma.auto_field() + view_type_id = ma.auto_field() + + +class ViewTypeSchema(IdNameSchema): + class Meta(BaseSchema.Meta): + model = models.ViewType class QuestionSchema(BaseSchema): @@ -40,14 +68,6 @@ class QuestionAnswerSchema(BaseSchema): team_id = ma.auto_field() -class MediaTypeSchema(BaseSchema): - class Meta(BaseSchema.Meta): - model = models.MediaType - - id = ma.auto_field() - name = ma.auto_field() - - class RoleSchema(BaseSchema): class Meta(BaseSchema.Meta): model = models.Role @@ -113,3 +133,17 @@ class CompetitionSchema(BaseSchema): name = ma.auto_field() year = ma.auto_field() city_id = ma.auto_field() + + +class ComponentSchema(BaseSchema): + class Meta(BaseSchema.Meta): + model = models.Component + + id = ma.auto_field() + x = ma.auto_field() + y = ma.auto_field() + w = ma.auto_field() + h = ma.auto_field() + data = ma.Function(lambda obj: obj.data) + slide_id = ma.auto_field() + type_id = ma.auto_field() diff --git a/server/app/database/__init__.py b/server/app/database/__init__.py index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..ead77d9cd731952e4f478f64604bbf49a150ea9a 100644 --- a/server/app/database/__init__.py +++ b/server/app/database/__init__.py @@ -0,0 +1,55 @@ +import json + +from flask_restx import abort +from flask_sqlalchemy import BaseQuery +from flask_sqlalchemy.model import Model +from sqlalchemy import Column, DateTime, Text +from sqlalchemy.sql import func +from sqlalchemy.types import TypeDecorator + + +class Base(Model): + __abstract__ = True + _created = Column(DateTime(timezone=True), server_default=func.now()) + _updated = Column(DateTime(timezone=True), onupdate=func.now()) + + +class ExtendedQuery(BaseQuery): + def first_extended(self, required=True, error_message=None, error_code=404): + 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 + + +class Dictionary(TypeDecorator): + + impl = Text(1024) + + def process_bind_param(self, value, dialect): + if value is not None: + value = json.dumps(value).replace("'", '"') + + return value + + def process_result_value(self, value, dialect): + if value is not None: + value = json.loads(value) + return value diff --git a/server/app/database/base.py b/server/app/database/base.py deleted file mode 100644 index 7a16a7525d73a87b1bc4b19680ac950c15860a6a..0000000000000000000000000000000000000000 --- a/server/app/database/base.py +++ /dev/null @@ -1,37 +0,0 @@ -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/database/controller/__init__.py b/server/app/database/controller/__init__.py index d31ded5698957d50c02634f8ca3eff9f90f756c6..2865b52300df3a80dce6554e9ad018faf5f06edc 100644 --- a/server/app/database/controller/__init__.py +++ b/server/app/database/controller/__init__.py @@ -1,16 +1,3 @@ # import add, get from app.core import db -from app.database.controller import add, delete, edit, get, search - - -def commit_and_refresh(item): - db.session.commit() - db.session.refresh(item) - - -def refresh(item): - db.session.refresh(item) - - -def commit(item): - db.session.commit() +from app.database.controller import add, delete, edit, get, search, utils diff --git a/server/app/database/controller/add.py b/server/app/database/controller/add.py index f612cdd67a858cc68b5ec2ca952089c2a77bd756..755849bb8f81e398af27310c56f3156beddf0d63 100644 --- a/server/app/database/controller/add.py +++ b/server/app/database/controller/add.py @@ -1,9 +1,13 @@ import app.core.http_codes as codes from app.core import db +from app.database.controller import utils from app.database.models import ( Blacklist, City, + Code, Competition, + Component, + ComponentType, Media, MediaType, Question, @@ -12,76 +16,98 @@ from app.database.models import ( Slide, Team, User, + ViewType, ) from flask_restx import abort -def db_add(func): - def wrapper(*args, **kwargs): - item = func(*args, **kwargs) - db.session.add(item) - db.session.commit() - db.session.refresh(item) +def db_add(item): + db.session.add(item) + db.session.commit() + db.session.refresh(item) - if not item: - abort(codes.BAD_REQUEST, f"Object could not be created") + if not item: + abort(codes.BAD_REQUEST, f"Object could not be created") - return item + return item - return wrapper - -@db_add def blacklist(jti): - return Blacklist(jti) + return db_add(Blacklist(jti)) -@db_add -def image(filename, user_id): - return Media(filename, 1, user_id) +def mediaType(name): + return db_add(MediaType(name)) -@db_add -def slide(item_competition): - order = Slide.query.filter(Slide.competition_id == item_competition.id).count() # first element has index 0 - return Slide(order, item_competition.id) +def questionType(name): + return db_add(QuestionType(name)) + + +def componentType(name): + return db_add(ComponentType(name)) + + +def viewType(name): + return db_add(ViewType(name)) + + +def role(name): + return db_add(Role(name)) + + +def city(name): + return db_add(City(name)) + + +def component(type_id, item_slide, data, x=0, y=0, w=0, h=0): + return db_add(Component(item_slide.id, type_id, data, x, y, w, h)) + + +def image(filename, user_id): + return db_add(Media(filename, 1, user_id)) -@db_add def user(email, password, role_id, city_id, name=None): - return User(email, password, role_id, city_id, name) + return db_add(User(email, password, role_id, city_id, name)) -@db_add def question(name, total_score, type_id, item_slide): - return Question(name, total_score, type_id, item_slide.id) + return db_add(Question(name, total_score, type_id, item_slide.id)) -@db_add -def competition(name, year, city_id): - return Competition(name, year, city_id) +def code(pointer, view_type_id): + code_string = utils.generate_unique_code() + return db_add(Code(code_string, pointer, view_type_id)) -@db_add def team(name, item_competition): - return Team(name, item_competition.id) + item = db_add(Team(name, item_competition.id)) + # Add code for the team + code(item.id, 1) -@db_add -def mediaType(name): - return MediaType(name) + return item -@db_add -def questionType(name): - return QuestionType(name) +def slide(item_competition): + order = Slide.query.filter(Slide.competition_id == item_competition.id).count() # first element has index 0 + return db_add(Slide(order, item_competition.id)) -@db_add -def role(name): - return Role(name) +def competition(name, year, city_id): + item_competition = db_add(Competition(name, year, city_id)) + # Add one slide for the competition + slide(item_competition) -@db_add -def city(name): - return City(name) + # Add code for Judge view + code(item_competition.id, 2) + + # Add code for Audience view + code(item_competition.id, 3) + + # Add two teams + + utils.refresh(item_competition) + return item_competition diff --git a/server/app/database/controller/delete.py b/server/app/database/controller/delete.py index 65527ee9dafe1f80169b408e83546e5f4a404dc3..33bea2174058e21df2d25042effe8957bfa139ff 100644 --- a/server/app/database/controller/delete.py +++ b/server/app/database/controller/delete.py @@ -8,18 +8,30 @@ def default(item): db.session.commit() -def slide(item_slide): +def component(item_component): + default(item_component) + + +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 + for item_component in item_slide.components: + default(item_component) + default(item_slide) + +def slide(item_slide): + competition_id = item_slide.competition_id + slide_order = item_slide.order + + _slide(item_slide) + # Update slide order for all slides after the deleted slide - slides_in_same_competition = dbc.get.slide_list(deleted_slide_competition_id) + slides_in_same_competition = dbc.get.slide_list(competition_id) for other_slide in slides_in_same_competition: - if other_slide.order > deleted_slide_order: + if other_slide.order > slide_order: other_slide.order -= 1 db.session.commit() @@ -49,7 +61,9 @@ def question_answers(item_question_answers): def competition(item_competition): for item_slide in item_competition.slides: - slide(item_slide) + _slide(item_slide) for item_team in item_competition.teams: team(item_team) + + # TODO codes default(item_competition) diff --git a/server/app/database/controller/edit.py b/server/app/database/controller/edit.py index 0a1b190f83f2150972e932d9d3bc899c5fe3280a..3afcb4d45c8ba0c0f9cfdaab83d88b286f566215 100644 --- a/server/app/database/controller/edit.py +++ b/server/app/database/controller/edit.py @@ -20,6 +20,23 @@ def switch_order(item1, item2): return item1 +def component(item, x, y, w, h, data): + if x: + item.x = x + if y: + item.y = y + if w: + item.w = w + if h: + item.h = h + if data: + item.data = data + + db.session.commit() + db.session.refresh(item) + return item + + def slide(item, title=None, timer=None): if title: item.title = title diff --git a/server/app/database/controller/get.py b/server/app/database/controller/get.py index a2b45fedf42bbec2c1874cdf35060d9c177582ef..269640b3c9af00d03aaa9d1a7d132154f4af66e2 100644 --- a/server/app/database/controller/get.py +++ b/server/app/database/controller/get.py @@ -1,13 +1,23 @@ -from app.database.models import Competition, Question, Slide, Team, User -from sqlalchemy.sql.expression import outerjoin +from app.core import db +from app.database.models import (City, Code, Competition, Component, + ComponentType, MediaType, Question, + QuestionType, Role, Slide, Team, User, + ViewType) + + +def all(db_type): + return db_type.query.all() + + +def one(db_type, id, required=True, error_msg=None): + return db_type.query.filter(db_type.id == id).first_extended(required, error_msg) 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 code_by_code(code): + return Code.query.filter(Code.code == code.upper()).first() def user(UID, required=True, error_msg=None): @@ -18,30 +28,38 @@ 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 slide(CID, SOrder, required=True, error_msg=None): + filters = (Slide.competition_id == CID) & (Slide.order == SOrder) + return Slide.query.filter(filters).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(CID, SOrder, QID, required=True, error_msg=None): + join_filters = (Slide.competition_id == CID) & (Slide.order == SOrder) & (Slide.id == Question.slide_id) + return Question.query.join(Slide, join_filters).filter(Question.id == QID).first_extended(required, error_msg) + + + +def code_list(competition_id): + team_view_id = 1 + join_filters = (Code.view_type_id == team_view_id) & (Team.id == Code.pointer) + filters = ((Code.view_type_id != team_view_id) & (Code.pointer == competition_id))( + (Code.view_type_id == team_view_id) & (competition_id == Team.competition_id) ) + return Code.query.join(Team, join_filters, isouter=True).filter(filters).all() def question_list(CID): - return Question.query.join(Slide, (Slide.competition_id == CID) & (Slide.id == Question.slide_id)).all() + join_filters = (Slide.competition_id == CID) & (Slide.id == Question.slide_id) + return Question.query.join(Slide, join_filters).all() + + +def component_list(CID, SOrder): + join_filters = (Slide.competition_id == CID) & (Slide.order == SOrder) & (Component.slide_id == Slide.id) + return Component.query.join(Slide, join_filters).all() def team_list(CID): diff --git a/server/app/database/controller/utils.py b/server/app/database/controller/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..61be34cae0520cb3db6ea58a0d23518e4f7af341 --- /dev/null +++ b/server/app/database/controller/utils.py @@ -0,0 +1,23 @@ +from app.core import db +from app.core.codes import generate_code_string +from app.database.models import Code + + +def generate_unique_code(): + code = generate_code_string() + while db.session.query(Code).filter(Code.code == code).count(): + code = generate_code_string() + return code + + +def commit_and_refresh(item): + db.session.commit() + db.session.refresh(item) + + +def refresh(item): + db.session.refresh(item) + + +def commit(item): + db.session.commit() diff --git a/server/app/database/models.py b/server/app/database/models.py index 5a17fb0d7bd28353827c3e9cb97049292ac7ef77..cfedd923b0202ef75517f5d3d1ce7443e60a0471 100644 --- a/server/app/database/models.py +++ b/server/app/database/models.py @@ -1,7 +1,6 @@ from app.core import bcrypt, db from sqlalchemy.ext.hybrid import hybrid_method, hybrid_property -from sqlalchemy.orm import backref - +from app.database import Dictionary STRING_SIZE = 254 @@ -130,6 +129,8 @@ class Slide(db.Model): background_image_id = db.Column(db.Integer, db.ForeignKey("media.id"), nullable=True) background_image = db.relationship("Media", uselist=False) + components = db.relationship("Component", backref="slide") + def __init__(self, order, competition_id): self.order = order self.competition_id = competition_id @@ -181,6 +182,61 @@ class QuestionAnswer(db.Model): self.team_id = team_id + + + +class Component(db.Model): + id = db.Column(db.Integer, primary_key=True) + x = db.Column(db.Integer, nullable=False, default=0) + y = db.Column(db.Integer, nullable=False, default=0) + w = db.Column(db.Integer, nullable=False, default=1) + h = db.Column(db.Integer, nullable=False, default=1) + data = db.Column(Dictionary()) + slide_id = db.Column(db.Integer, db.ForeignKey("slide.id"), nullable=False) + type_id = db.Column(db.Integer, db.ForeignKey("component_type.id"), nullable=False) + + def __init__(self, slide_id, type_id, data, x=0, y=0, w=1, h=1): + self.x = x + self.y = y + self.w = w + self.h = h + self.data = data + self.slide_id = slide_id + self.type_id = type_id + + +class Code(db.Model): + table_args = (db.UniqueConstraint("pointer", "type"),) + id = db.Column(db.Integer, primary_key=True) + code = db.Column(db.Text, unique=True) + pointer = db.Column(db.Integer, nullable=False) + + view_type_id = db.Column(db.Integer, db.ForeignKey("view_type.id"), nullable=False) + + def __init__(self, code, pointer, view_type_id): + self.code = code + self.pointer = pointer + self.view_type_id = view_type_id + + +class ViewType(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(STRING_SIZE), unique=True) + codes = db.relationship("Code", backref="view_type") + + def __init__(self, name): + self.name = name + + +class ComponentType(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(STRING_SIZE), unique=True) + components = db.relationship("Component", backref="component_type") + + def __init__(self, name): + self.name = name + + class MediaType(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(STRING_SIZE), unique=True) diff --git a/server/configmodule.py b/server/configmodule.py index fcf23cbf5624e977be6cb3f3f27aae3d173a0528..2d525424e891aed9d05ada1d2a75e57bc3171a2a 100644 --- a/server/configmodule.py +++ b/server/configmodule.py @@ -15,12 +15,13 @@ class Config: JWT_REFRESH_TOKEN_EXPIRES = timedelta(days=30) UPLOADED_PHOTOS_DEST = "static/images" # os.getcwd() SECRET_KEY = os.urandom(24) + SQLALCHEMY_ECHO = False class DevelopmentConfig(Config): DEBUG = True SQLALCHEMY_DATABASE_URI = "sqlite:///database.db" - SQLALCHEMY_ECHO = True + SQLALCHEMY_ECHO = False class TestingConfig(Config): diff --git a/server/populate.py b/server/populate.py index e2ca47767f3414e1a26915a98d4b95e17edb059f..bebcaa8b098cdffeaf070b71eb0570cd359e5bf9 100644 --- a/server/populate.py +++ b/server/populate.py @@ -1,28 +1,35 @@ -from sqlalchemy.sql.expression import true - import app.database.controller as dbc from app import create_app, db -from app.database.models import City, Competition, MediaType, QuestionType, Role +from app.database.models import City, Competition, QuestionType, Role def _add_items(): media_types = ["Image", "Video"] question_types = ["Boolean", "Multiple", "Text"] + component_types = ["Text", "Image"] + view_types = ["Team", "Judge", "Audience"] + roles = ["Admin", "Editor"] cities = ["Linköping", "Stockholm", "Norrköping", "Örkelljunga"] teams = ["Gymnasieskola A", "Gymnasieskola B", "Gymnasieskola C"] - for team_name in media_types: - dbc.add.mediaType(team_name) + for name in media_types: + dbc.add.mediaType(name) + + for name in question_types: + dbc.add.questionType(name) - for team_name in question_types: - dbc.add.questionType(team_name) + for name in component_types: + dbc.add.componentType(name) - for team_name in roles: - dbc.add.role(team_name) + for name in view_types: + dbc.add.viewType(name) - for team_name in cities: - dbc.add.city(team_name) + for name in roles: + dbc.add.role(name) + + for name in cities: + dbc.add.city(name) admin_id = Role.query.filter(Role.name == "Admin").one().id editor_id = Role.query.filter(Role.name == "Editor").one().id @@ -40,14 +47,11 @@ def _add_items(): dbc.add.competition(f"Test{i+1}", 1971, city_id) item_comps = Competition.query.all() - # Add - for item_comp in item_comps: - for i in range(3): - # Add slide to competition - item_slide = dbc.add.slide(item_comp) - # Add question to competition - dbc.add.question(f"Q{i+1}", i + 1, text_id, item_slide) + for item_comp in item_comps: + for item_slide in item_comp.slides: + for i in range(3): + dbc.add.question(f"Q{i+1}", i + 1, text_id, item_slide) # Add teams to competition for team_name in teams: diff --git a/server/tests/test_app.py b/server/tests/test_app.py index 1cbacc33844b2cc4b180d87a51b440b0588d8c88..948b9ae42827cec8d14d4bb247fb04a4c4863f5f 100644 --- a/server/tests/test_app.py +++ b/server/tests/test_app.py @@ -2,8 +2,7 @@ import app.core.http_codes as codes 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): @@ -14,6 +13,14 @@ def test_misc_api(client): assert response.status_code == codes.OK headers = {"Authorization": "Bearer " + body["access_token"]} + # Get types + response, body = get(client, "/api/misc/types", headers=headers) + assert response.status_code == codes.OK + assert len(body["media_types"]) >= 2 + assert len(body["question_types"]) >= 3 + assert len(body["component_types"]) >= 2 + assert len(body["view_types"]) >= 2 + ## Get misc response, body = get(client, "/api/misc/roles", headers=headers) assert response.status_code == codes.OK @@ -21,44 +28,33 @@ def test_misc_api(client): response, body = get(client, "/api/misc/cities", 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" - - 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 + assert body["items"][0]["name"] == "Linköping" and body["items"][1]["name"] == "Testköping" ## 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"][2]["name"] == "Göteborg" + assert body["count"] >= 2 and body["items"][2]["name"] == "Göteborg" # 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"][2]["name"] == "Gbg" + assert body["count"] >= 2 and 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["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" + assert body["count"] >= 2 + assert body["items"][0]["name"] == "Linköping" and body["items"][1]["name"] == "Testköping" def test_competition_api(client): @@ -76,9 +72,6 @@ def test_competition_api(client): assert body["name"] == "c1" competition_id = body["id"] - # Save number of slides - num_slides = len(Slide.query.all()) - # Get competition response, body = get(client, f"/api/competitions/{competition_id}", headers=headers) assert response.status_code == codes.OK @@ -89,11 +82,9 @@ def test_competition_api(client): response, body = get(client, f"/api/competitions/{competition_id}/slides", headers=headers) assert response.status_code == codes.OK - assert len(body["items"]) == 2 + assert len(body["items"]) == 3 - response, body = put( - client, f"/api/competitions/{competition_id}/slides/{num_slides}/order", {"order": 1}, headers=headers - ) + response, body = put(client, f"/api/competitions/{competition_id}/slides/{2}/order", {"order": 1}, headers=headers) assert response.status_code == codes.OK response, body = post(client, f"/api/competitions/{competition_id}/teams", {"name": "t1"}, headers=headers) @@ -152,8 +143,7 @@ def test_auth_and_user_api(client): 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 + assert body["city"]["id"] == 2 and body["role"]["id"] == 1 # Find other user response, body = get( @@ -240,26 +230,26 @@ def test_slide_api(client): CID = 1 response, body = get(client, f"/api/competitions/{CID}/slides", headers=headers) assert response.status_code == codes.OK - assert body["count"] == 0 + assert body["count"] == 1 # 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 + assert body["count"] == 3 # 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 + assert body["count"] == 4 + # Add another slide + response, body = post(client, f"/api/competitions/{CID}/slides", headers=headers) # Get slide - SID = 1 - response, item_slide = get(client, f"/api/competitions/{CID}/slides/{SID}", headers=headers) + slide_order = 1 + response, item_slide = get(client, f"/api/competitions/{CID}/slides/{slide_order}", headers=headers) assert response.status_code == codes.OK - assert item_slide["id"] == SID + assert item_slide["order"] == slide_order # Edit slide order = 6 @@ -272,7 +262,7 @@ def test_slide_api(client): assert item_slide["timer"] != timer response, item_slide = put( client, - f"/api/competitions/{CID}/slides/{SID}", + f"/api/competitions/{CID}/slides/{slide_order}", # TODO: Implement so these commented lines can be edited # {"order": order, "title": title, "body": body, "timer": timer}, {"title": title, "timer": timer}, @@ -285,31 +275,26 @@ def test_slide_api(client): assert item_slide["timer"] == timer # Delete slide - response, _ = delete(client, f"/api/competitions/{CID}/slides/{SID}", headers=headers) - num_slides -= 1 + response, _ = delete(client, f"/api/competitions/{CID}/slides/{slide_order}", headers=headers) 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 + assert body["count"] == 4 - # Tries to delete slide again - response, _ = delete(client, f"/api/competitions/{CID}/slides/{SID}", headers=headers) - assert response.status_code == codes.NOT_FOUND + # Tries to delete slide again, should work since the order is now changed + response, _ = delete(client, f"/api/competitions/{CID}/slides/{slide_order}", headers=headers) + assert response.status_code == codes.NO_CONTENT # 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) + slide_order = body["items"][0]["order"] + response, _ = put( + client, f"/api/competitions/{CID}/slides/{slide_order}/order", {"order": slide_order}, headers=headers + ) assert response.status_code == codes.OK # 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) + change_order_test(client, CID, slide_order, slide_order + 1, headers) def test_question_api(client): @@ -322,6 +307,7 @@ def test_question_api(client): # Get questions from empty competition CID = 1 # TODO: Fix api-calls so that the ones not using CID don't require one + slide_order = 1 response, body = get(client, f"/api/competitions/{CID}/questions", headers=headers) assert response.status_code == codes.OK assert body["count"] == 0 @@ -331,110 +317,30 @@ 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 - # 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 + slide_order = 1 response, item_question = post( client, - f"/api/competitions/{CID}/questions", - {"name": name, "type_id": type_id, "slide_id": slide_id}, + f"/api/competitions/{CID}/slides/{slide_order}/questions", + {"name": name, "type_id": type_id}, headers=headers, ) - num_questions += 1 + num_questions = 4 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) + response, _ = delete(client, f"/api/competitions/{CID}/slides/{slide_order}/questions/{QID}", headers=headers) num_questions -= 1 assert response.status_code == codes.NO_CONTENT @@ -444,5 +350,6 @@ def test_question_api(client): assert body["count"] == num_questions # Tries to delete question again - response, _ = delete(client, f"/api/competitions/{CID}/questions/{QID}", headers=headers) + response, _ = delete(client, f"/api/competitions/{CID}/slides/{NEW_slide_order}/questions/{QID}", headers=headers) assert response.status_code == codes.NOT_FOUND + """ diff --git a/server/tests/test_helpers.py b/server/tests/test_helpers.py index 7cbdec8757d66b5a934c7e086f836a4057563f92..fbd77d9ef146676c0cb6af94581c984b730ac3db 100644 --- a/server/tests/test_helpers.py +++ b/server/tests/test_helpers.py @@ -9,23 +9,29 @@ from app.database.models import City, Role def add_default_values(): media_types = ["Image", "Video"] question_types = ["Boolean", "Multiple", "Text"] + component_types = ["Text", "Image"] + view_types = ["Team", "Judge", "Audience"] + roles = ["Admin", "Editor"] cities = ["Linköping", "Testköping"] - # Add media types - for item in media_types: - dbc.add.mediaType(item) + for name in media_types: + dbc.add.mediaType(name) + + for name in question_types: + dbc.add.questionType(name) + + for name in component_types: + dbc.add.componentType(name) + + for name in view_types: + dbc.add.viewType(name) - # Add question types - for item in question_types: - dbc.add.questionType(item) + for name in roles: + dbc.add.role(name) - # Add roles - for item in roles: - dbc.add.role(item) - # Add cities - for item in cities: - dbc.add.city(item) + for name in cities: + dbc.add.city(name) item_admin = Role.query.filter(Role.name == "Admin").one() item_city = City.query.filter(City.name == "Linköping").one() @@ -33,23 +39,25 @@ def add_default_values(): 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) + item_competition = 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 two more slides to competition + dbc.add.slide(item_comp) + dbc.add.slide(item_comp) # Add slides - for i in range(len(question_types)): - # Add slide to competition - item_slide = dbc.add.slide(item_comp) - + i = 1 + for item_slide in item_comp.slides: # Populate slide with data item_slide.title = f"Title {i}" item_slide.body = f"Body {i}" item_slide.timer = 100 + i # item_slide.settings = "{}" - + dbc.utils.commit_and_refresh(item_slide) # Add question to competition - dbc.add.question(name=f"Q{i+1}", total_score=i + 1, type_id=i + 1, item_slide=item_slide) + dbc.add.question(name=f"Q{i}", total_score=i, type_id=1, item_slide=item_slide) + i += 1 def get_body(response): @@ -118,37 +126,12 @@ def assert_object_values(obj, values): # 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) +def change_order_test(client, cid, order, new_order, h): + response, new_order_body = get(client, f"/api/competitions/{cid}/slides/{new_order}", 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) + response, order_body = get(client, f"/api/competitions/{cid}/slides/{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) + response, _ = put(client, f"/api/competitions/{cid}/slides/{order}/order", {"order": new_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"]