Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • tddd96-grupp11/teknikattan-scoring-system
1 result
Show changes
Showing
with 462 additions and 127 deletions
import { RichSlide } from '../interfaces/ApiRichModels'
import React from 'react'
import BuildOutlinedIcon from '@material-ui/icons/BuildOutlined' import BuildOutlinedIcon from '@material-ui/icons/BuildOutlined'
import CheckBoxOutlinedIcon from '@material-ui/icons/CheckBoxOutlined'
import CreateOutlinedIcon from '@material-ui/icons/CreateOutlined' import CreateOutlinedIcon from '@material-ui/icons/CreateOutlined'
import DnsOutlinedIcon from '@material-ui/icons/DnsOutlined'
import InfoOutlinedIcon from '@material-ui/icons/InfoOutlined' import InfoOutlinedIcon from '@material-ui/icons/InfoOutlined'
import RadioButtonCheckedIcon from '@material-ui/icons/RadioButtonChecked'
import React from 'react'
import { RichSlide } from '../interfaces/ApiRichModels'
export const renderSlideIcon = (slide: RichSlide) => { export const renderSlideIcon = (slide: RichSlide) => {
if (slide.questions && slide.questions[0] && slide.questions[0].type_id) { if (slide.questions && slide.questions[0] && slide.questions[0].type_id) {
...@@ -13,7 +14,9 @@ export const renderSlideIcon = (slide: RichSlide) => { ...@@ -13,7 +14,9 @@ export const renderSlideIcon = (slide: RichSlide) => {
case 2: case 2:
return <BuildOutlinedIcon /> // practical qustion return <BuildOutlinedIcon /> // practical qustion
case 3: case 3:
return <DnsOutlinedIcon /> // multiple choice question return <CheckBoxOutlinedIcon /> // multiple choice question
case 4:
return <RadioButtonCheckedIcon /> // single choice question
} }
} else { } else {
return <InfoOutlinedIcon /> // information slide return <InfoOutlinedIcon /> // information slide
......
"""
All API calls concerning question alternatives.
Default route: /api/competitions/<competition_id>/slides/<slide_id>/questions/<question_id>/alternatives
"""
import app.core.http_codes as codes import app.core.http_codes as codes
import app.database.controller as dbc import app.database.controller as dbc
from app.apis import item_response, list_response, protect_route from app.apis import item_response, list_response, protect_route
from app.core.dto import QuestionAlternativeDTO from app.core.dto import QuestionAlternativeDTO
from flask_restx import Resource
from flask_restx import reqparse
from app.core.parsers import sentinel from app.core.parsers import sentinel
from flask_restx import Resource, reqparse
api = QuestionAlternativeDTO.api api = QuestionAlternativeDTO.api
schema = QuestionAlternativeDTO.schema schema = QuestionAlternativeDTO.schema
...@@ -24,11 +28,22 @@ alternative_parser_edit.add_argument("value", type=int, default=sentinel, locati ...@@ -24,11 +28,22 @@ alternative_parser_edit.add_argument("value", type=int, default=sentinel, locati
class QuestionAlternativeList(Resource): class QuestionAlternativeList(Resource):
@protect_route(allowed_roles=["*"], allowed_views=["*"]) @protect_route(allowed_roles=["*"], allowed_views=["*"])
def get(self, competition_id, slide_id, question_id): def get(self, competition_id, slide_id, question_id):
items = dbc.get.question_alternative_list(competition_id, slide_id, question_id) """ Gets the all question alternatives to the specified question. """
items = dbc.get.question_alternative_list(
competition_id,
slide_id,
question_id,
)
return list_response(list_schema.dump(items)) return list_response(list_schema.dump(items))
@protect_route(allowed_roles=["*"]) @protect_route(allowed_roles=["*"])
def post(self, competition_id, slide_id, question_id): def post(self, competition_id, slide_id, question_id):
"""
Posts a new question alternative to the specified
question using the provided arguments.
"""
args = alternative_parser_add.parse_args(strict=True) args = alternative_parser_add.parse_args(strict=True)
item = dbc.add.question_alternative(**args, question_id=question_id) item = dbc.add.question_alternative(**args, question_id=question_id)
return item_response(schema.dump(item)) return item_response(schema.dump(item))
...@@ -39,18 +54,41 @@ class QuestionAlternativeList(Resource): ...@@ -39,18 +54,41 @@ class QuestionAlternativeList(Resource):
class QuestionAlternatives(Resource): class QuestionAlternatives(Resource):
@protect_route(allowed_roles=["*"], allowed_views=["*"]) @protect_route(allowed_roles=["*"], allowed_views=["*"])
def get(self, competition_id, slide_id, question_id, alternative_id): def get(self, competition_id, slide_id, question_id, alternative_id):
items = dbc.get.question_alternative(competition_id, slide_id, question_id, alternative_id) """ Gets the specified question alternative. """
items = dbc.get.question_alternative(
competition_id,
slide_id,
question_id,
alternative_id,
)
return item_response(schema.dump(items)) return item_response(schema.dump(items))
@protect_route(allowed_roles=["*"]) @protect_route(allowed_roles=["*"])
def put(self, competition_id, slide_id, question_id, alternative_id): def put(self, competition_id, slide_id, question_id, alternative_id):
"""
Edits the specified question alternative using the provided arguments.
"""
args = alternative_parser_edit.parse_args(strict=True) args = alternative_parser_edit.parse_args(strict=True)
item = dbc.get.question_alternative(competition_id, slide_id, question_id, alternative_id) item = dbc.get.question_alternative(
competition_id,
slide_id,
question_id,
alternative_id,
)
item = dbc.edit.default(item, **args) item = dbc.edit.default(item, **args)
return item_response(schema.dump(item)) return item_response(schema.dump(item))
@protect_route(allowed_roles=["*"]) @protect_route(allowed_roles=["*"])
def delete(self, competition_id, slide_id, question_id, alternative_id): def delete(self, competition_id, slide_id, question_id, alternative_id):
item = dbc.get.question_alternative(competition_id, slide_id, question_id, alternative_id) """ Deletes the specified question alternative. """
item = dbc.get.question_alternative(
competition_id,
slide_id,
question_id,
alternative_id,
)
dbc.delete.default(item) dbc.delete.default(item)
return {}, codes.NO_CONTENT return {}, codes.NO_CONTENT
"""
All API calls concerning question answers.
Default route: /api/competitions/<competition_id>/teams/<team_id>/answers
"""
import app.database.controller as dbc import app.database.controller as dbc
from app.apis import item_response, list_response, protect_route from app.apis import item_response, list_response, protect_route
from app.core.dto import QuestionAnswerDTO from app.core.dto import QuestionAnswerDTO
from flask_restx import Resource
from flask_restx import reqparse
from app.core.parsers import sentinel from app.core.parsers import sentinel
from flask_restx import Resource, reqparse
api = QuestionAnswerDTO.api api = QuestionAnswerDTO.api
schema = QuestionAnswerDTO.schema schema = QuestionAnswerDTO.schema
...@@ -24,11 +28,18 @@ answer_parser_edit.add_argument("score", type=int, default=sentinel, location="j ...@@ -24,11 +28,18 @@ answer_parser_edit.add_argument("score", type=int, default=sentinel, location="j
class QuestionAnswerList(Resource): class QuestionAnswerList(Resource):
@protect_route(allowed_roles=["*"], allowed_views=["*"]) @protect_route(allowed_roles=["*"], allowed_views=["*"])
def get(self, competition_id, team_id): def get(self, competition_id, team_id):
""" Gets all question answers that the specified team has given. """
items = dbc.get.question_answer_list(competition_id, team_id) items = dbc.get.question_answer_list(competition_id, team_id)
return list_response(list_schema.dump(items)) return list_response(list_schema.dump(items))
@protect_route(allowed_roles=["*"], allowed_views=["*"]) @protect_route(allowed_roles=["*"], allowed_views=["*"])
def post(self, competition_id, team_id): def post(self, competition_id, team_id):
"""
Posts a new question answer to the specified
question using the provided arguments.
"""
args = answer_parser_add.parse_args(strict=True) args = answer_parser_add.parse_args(strict=True)
item = dbc.add.question_answer(**args, team_id=team_id) item = dbc.add.question_answer(**args, team_id=team_id)
return item_response(schema.dump(item)) return item_response(schema.dump(item))
...@@ -39,12 +50,19 @@ class QuestionAnswerList(Resource): ...@@ -39,12 +50,19 @@ class QuestionAnswerList(Resource):
class QuestionAnswers(Resource): class QuestionAnswers(Resource):
@protect_route(allowed_roles=["*"], allowed_views=["*"]) @protect_route(allowed_roles=["*"], allowed_views=["*"])
def get(self, competition_id, team_id, answer_id): def get(self, competition_id, team_id, answer_id):
""" Gets the specified question answer. """
item = dbc.get.question_answer(competition_id, team_id, answer_id) item = dbc.get.question_answer(competition_id, team_id, answer_id)
return item_response(schema.dump(item)) return item_response(schema.dump(item))
@protect_route(allowed_roles=["*"], allowed_views=["*"]) @protect_route(allowed_roles=["*"], allowed_views=["*"])
def put(self, competition_id, team_id, answer_id): def put(self, competition_id, team_id, answer_id):
""" Edits the specified question answer with the provided arguments. """
args = answer_parser_edit.parse_args(strict=True) args = answer_parser_edit.parse_args(strict=True)
item = dbc.get.question_answer(competition_id, team_id, answer_id) item = dbc.get.question_answer(competition_id, team_id, answer_id)
item = dbc.edit.default(item, **args) item = dbc.edit.default(item, **args)
return item_response(schema.dump(item)) return item_response(schema.dump(item))
# No need to delete an answer. It only needs to be deleted
# together with the question or the team.
from datetime import timedelta """
All API calls concerning question answers.
Default route: /api/auth
"""
from datetime import datetime, timedelta
import app.core.http_codes as codes import app.core.http_codes as codes
import app.database.controller as dbc import app.database.controller as dbc
...@@ -6,7 +11,8 @@ from app.apis import item_response, protect_route, text_response ...@@ -6,7 +11,8 @@ from app.apis import item_response, protect_route, text_response
from app.core import sockets from app.core import sockets
from app.core.codes import verify_code from app.core.codes import verify_code
from app.core.dto import AuthDTO from app.core.dto import AuthDTO
from app.database.models import Whitelist from app.database.models import User, Whitelist
from flask import current_app
from flask_jwt_extended import create_access_token, get_jti, get_raw_jwt from flask_jwt_extended import create_access_token, get_jti, get_raw_jwt
from flask_jwt_extended.utils import get_jti from flask_jwt_extended.utils import get_jti
from flask_restx import Resource, inputs, reqparse from flask_restx import Resource, inputs, reqparse
...@@ -26,12 +32,19 @@ create_user_parser.add_argument("role_id", type=int, required=True, location="js ...@@ -26,12 +32,19 @@ create_user_parser.add_argument("role_id", type=int, required=True, location="js
login_code_parser = reqparse.RequestParser() login_code_parser = reqparse.RequestParser()
login_code_parser.add_argument("code", type=str, required=True, location="json") login_code_parser.add_argument("code", type=str, required=True, location="json")
USER_LOGIN_LOCKED_ATTEMPTS = current_app.config["USER_LOGIN_LOCKED_ATTEMPTS"]
USER_LOGIN_LOCKED_EXPIRES = current_app.config["USER_LOGIN_LOCKED_EXPIRES"]
def get_user_claims(item_user): def get_user_claims(item_user):
""" Gets user details for jwt-token. """
return {"role": item_user.role.name, "city_id": item_user.city_id} return {"role": item_user.role.name, "city_id": item_user.city_id}
def get_code_claims(item_code): def get_code_claims(item_code):
""" Gets code details for jwt-token. """
return { return {
"view": item_code.view_type.name, "view": item_code.view_type.name,
"competition_id": item_code.competition_id, "competition_id": item_code.competition_id,
...@@ -44,6 +57,8 @@ def get_code_claims(item_code): ...@@ -44,6 +57,8 @@ def get_code_claims(item_code):
class AuthSignup(Resource): class AuthSignup(Resource):
@protect_route(allowed_roles=["Admin"], allowed_views=["*"]) @protect_route(allowed_roles=["Admin"], allowed_views=["*"])
def get(self): def get(self):
""" Tests that the user is an admin. """
return "ok" return "ok"
...@@ -51,12 +66,16 @@ class AuthSignup(Resource): ...@@ -51,12 +66,16 @@ class AuthSignup(Resource):
class AuthSignup(Resource): class AuthSignup(Resource):
@protect_route(allowed_roles=["Admin"]) @protect_route(allowed_roles=["Admin"])
def post(self): def post(self):
""" Creates a new user if the user does not already exist. """
args = create_user_parser.parse_args(strict=True) args = create_user_parser.parse_args(strict=True)
email = args.get("email") email = args.get("email")
# Check if email is already used
if dbc.get.user_exists(email): if dbc.get.user_exists(email):
api.abort(codes.BAD_REQUEST, "User already exists") api.abort(codes.BAD_REQUEST, "User already exists")
# Add user
item_user = dbc.add.user(**args) item_user = dbc.add.user(**args)
return item_response(schema.dump(item_user)) return item_response(schema.dump(item_user))
...@@ -66,28 +85,65 @@ class AuthSignup(Resource): ...@@ -66,28 +85,65 @@ class AuthSignup(Resource):
class AuthDelete(Resource): class AuthDelete(Resource):
@protect_route(allowed_roles=["Admin"]) @protect_route(allowed_roles=["Admin"])
def delete(self, user_id): def delete(self, user_id):
item_user = dbc.get.user(user_id) """ Deletes the specified user and adds their token to the blacklist. """
item_user = dbc.get.one(User, user_id)
# Blacklist all the whitelisted tokens
# in use for the user that will be deleted
dbc.delete.whitelist_to_blacklist(Whitelist.user_id == user_id) dbc.delete.whitelist_to_blacklist(Whitelist.user_id == user_id)
dbc.delete.default(item_user)
# Delete user
dbc.delete.default(item_user)
return text_response(f"User {user_id} deleted") return text_response(f"User {user_id} deleted")
@api.route("/login") @api.route("/login")
class AuthLogin(Resource): class AuthLogin(Resource):
def post(self): def post(self):
""" Logs in the specified user and creates a jwt-token. """
args = login_parser.parse_args(strict=True) args = login_parser.parse_args(strict=True)
email = args.get("email") email = args.get("email")
password = args.get("password") password = args.get("password")
item_user = dbc.get.user_by_email(email) item_user = dbc.get.user_by_email(email)
if not item_user or not item_user.is_correct_password(password): # Login with unkown email
if not item_user:
api.abort(codes.UNAUTHORIZED, "Invalid email or password") api.abort(codes.UNAUTHORIZED, "Invalid email or password")
# Login with existing email but with wrong password
if not item_user.is_correct_password(password):
# Increase the login attempts every time the user tries to login with wrong password
item_user.login_attempts += 1
# Lock the user out for some time
if item_user.login_attempts == USER_LOGIN_LOCKED_ATTEMPTS:
item_user.locked = datetime.now() + USER_LOGIN_LOCKED_EXPIRES
dbc.utils.commit()
api.abort(codes.UNAUTHORIZED, "Invalid email or password")
# Otherwise if login was successful but the user is locked
if item_user.locked:
# Check if locked is greater than now
if item_user.locked > datetime.now():
api.abort(codes.UNAUTHORIZED, f"Try again in {item_user.locked} hours.")
else:
item_user.locked = None
# If everything else was successful, set login_attempts to 0
item_user.login_attempts = 0
dbc.utils.commit()
# Create the jwt with user.id as the identifier
access_token = create_access_token(item_user.id, user_claims=get_user_claims(item_user)) access_token = create_access_token(item_user.id, user_claims=get_user_claims(item_user))
# refresh_token = create_refresh_token(item_user.id)
# Login response includes the id and jwt for the user
response = {"id": item_user.id, "access_token": access_token} response = {"id": item_user.id, "access_token": access_token}
# Whitelist the created jwt
dbc.add.whitelist(get_jti(access_token), item_user.id) dbc.add.whitelist(get_jti(access_token), item_user.id)
return response return response
...@@ -95,9 +151,12 @@ class AuthLogin(Resource): ...@@ -95,9 +151,12 @@ class AuthLogin(Resource):
@api.route("/login/code") @api.route("/login/code")
class AuthLoginCode(Resource): class AuthLoginCode(Resource):
def post(self): def post(self):
""" Logs in using the provided competition code. """
args = login_code_parser.parse_args() args = login_code_parser.parse_args()
code = args["code"] code = args["code"]
# Check so the code string is valid
if not verify_code(code): if not verify_code(code):
api.abort(codes.UNAUTHORIZED, "Invalid code") api.abort(codes.UNAUTHORIZED, "Invalid code")
...@@ -107,10 +166,12 @@ class AuthLoginCode(Resource): ...@@ -107,10 +166,12 @@ class AuthLoginCode(Resource):
if item_code.competition_id not in sockets.presentations: if item_code.competition_id not in sockets.presentations:
api.abort(codes.UNAUTHORIZED, "Competition not active") api.abort(codes.UNAUTHORIZED, "Competition not active")
# Create jwt that is only valid for 8 hours
access_token = create_access_token( access_token = create_access_token(
item_code.id, user_claims=get_code_claims(item_code), expires_delta=timedelta(hours=8) item_code.id, user_claims=get_code_claims(item_code), expires_delta=timedelta(hours=8)
) )
# Whitelist the created jwt
dbc.add.whitelist(get_jti(access_token), competition_id=item_code.competition_id) dbc.add.whitelist(get_jti(access_token), competition_id=item_code.competition_id)
response = { response = {
"competition_id": item_code.competition_id, "competition_id": item_code.competition_id,
...@@ -125,9 +186,16 @@ class AuthLoginCode(Resource): ...@@ -125,9 +186,16 @@ class AuthLoginCode(Resource):
class AuthLogout(Resource): class AuthLogout(Resource):
@protect_route(allowed_roles=["*"], allowed_views=["*"]) @protect_route(allowed_roles=["*"], allowed_views=["*"])
def post(self): def post(self):
""" Logs out. """
jti = get_raw_jwt()["jti"] jti = get_raw_jwt()["jti"]
# Blacklist the token so the user cannot access the api anymore
dbc.add.blacklist(jti) dbc.add.blacklist(jti)
# Remove the the token from the whitelist since it's blacklisted now
Whitelist.query.filter(Whitelist.jti == jti).delete() Whitelist.query.filter(Whitelist.jti == jti).delete()
dbc.utils.commit() dbc.utils.commit()
return text_response("Logout") return text_response("Logout")
......
"""
All API calls concerning competition codes.
Default route: /api/competitions/<competition_id>/codes
"""
import app.database.controller as dbc import app.database.controller as dbc
from app.apis import item_response, list_response, protect_route from app.apis import item_response, list_response, protect_route
from app.core.dto import CodeDTO from app.core.dto import CodeDTO
...@@ -14,6 +19,8 @@ list_schema = CodeDTO.list_schema ...@@ -14,6 +19,8 @@ list_schema = CodeDTO.list_schema
class CodesList(Resource): class CodesList(Resource):
@protect_route(allowed_roles=["*"], allowed_views=["Operator"]) @protect_route(allowed_roles=["*"], allowed_views=["Operator"])
def get(self, competition_id): def get(self, competition_id):
""" Gets the all competition codes. """
items = dbc.get.code_list(competition_id) items = dbc.get.code_list(competition_id)
return list_response(list_schema.dump(items), len(items)) return list_response(list_schema.dump(items), len(items))
...@@ -23,6 +30,8 @@ class CodesList(Resource): ...@@ -23,6 +30,8 @@ class CodesList(Resource):
class CodesById(Resource): class CodesById(Resource):
@protect_route(allowed_roles=["*"]) @protect_route(allowed_roles=["*"])
def put(self, competition_id, code_id): def put(self, competition_id, code_id):
""" Generates a new competition code. """
item = dbc.get.one(Code, code_id) item = dbc.get.one(Code, code_id)
item.code = dbc.utils.generate_unique_code() item.code = dbc.utils.generate_unique_code()
dbc.utils.commit_and_refresh(item) dbc.utils.commit_and_refresh(item)
......
"""
All API calls concerning competitions.
Default route: /api/competitions
"""
import app.database.controller as dbc import app.database.controller as dbc
from app.apis import item_response, list_response, protect_route from app.apis import item_response, list_response, protect_route
from app.core.dto import CompetitionDTO from app.core.dto import CompetitionDTO
from app.database.models import Competition
from flask_restx import Resource
from flask_restx import reqparse
from app.core.parsers import search_parser, sentinel from app.core.parsers import search_parser, sentinel
from app.database.models import Competition
from flask_restx import Resource, reqparse
api = CompetitionDTO.api api = CompetitionDTO.api
schema = CompetitionDTO.schema schema = CompetitionDTO.schema
...@@ -32,6 +36,8 @@ competition_parser_search.add_argument("city_id", type=int, default=sentinel, lo ...@@ -32,6 +36,8 @@ competition_parser_search.add_argument("city_id", type=int, default=sentinel, lo
class CompetitionsList(Resource): class CompetitionsList(Resource):
@protect_route(allowed_roles=["*"]) @protect_route(allowed_roles=["*"])
def post(self): def post(self):
""" Posts a new competition. """
args = competition_parser_add.parse_args(strict=True) args = competition_parser_add.parse_args(strict=True)
# Add competition # Add competition
...@@ -47,12 +53,16 @@ class CompetitionsList(Resource): ...@@ -47,12 +53,16 @@ class CompetitionsList(Resource):
class Competitions(Resource): class Competitions(Resource):
@protect_route(allowed_roles=["*"], allowed_views=["*"]) @protect_route(allowed_roles=["*"], allowed_views=["*"])
def get(self, competition_id): def get(self, competition_id):
""" Gets the specified competition. """
item = dbc.get.competition(competition_id) item = dbc.get.competition(competition_id)
return item_response(rich_schema.dump(item)) return item_response(rich_schema.dump(item))
@protect_route(allowed_roles=["*"]) @protect_route(allowed_roles=["*"])
def put(self, competition_id): def put(self, competition_id):
""" Edits the specified competition with the specified arguments. """
args = competition_parser_edit.parse_args(strict=True) args = competition_parser_edit.parse_args(strict=True)
item = dbc.get.one(Competition, competition_id) item = dbc.get.one(Competition, competition_id)
item = dbc.edit.default(item, **args) item = dbc.edit.default(item, **args)
...@@ -61,6 +71,8 @@ class Competitions(Resource): ...@@ -61,6 +71,8 @@ class Competitions(Resource):
@protect_route(allowed_roles=["*"]) @protect_route(allowed_roles=["*"])
def delete(self, competition_id): def delete(self, competition_id):
""" Deletes the specified competition. """
item = dbc.get.one(Competition, competition_id) item = dbc.get.one(Competition, competition_id)
dbc.delete.competition(item) dbc.delete.competition(item)
...@@ -71,6 +83,8 @@ class Competitions(Resource): ...@@ -71,6 +83,8 @@ class Competitions(Resource):
class CompetitionSearch(Resource): class CompetitionSearch(Resource):
@protect_route(allowed_roles=["*"]) @protect_route(allowed_roles=["*"])
def get(self): def get(self):
""" Finds a specific competition based on the provided arguments. """
args = competition_parser_search.parse_args(strict=True) args = competition_parser_search.parse_args(strict=True)
items, total = dbc.search.competition(**args) items, total = dbc.search.competition(**args)
return list_response(list_schema.dump(items), total) return list_response(list_schema.dump(items), total)
...@@ -81,6 +95,8 @@ class CompetitionSearch(Resource): ...@@ -81,6 +95,8 @@ class CompetitionSearch(Resource):
class SlidesOrder(Resource): class SlidesOrder(Resource):
@protect_route(allowed_roles=["*"]) @protect_route(allowed_roles=["*"])
def post(self, competition_id): def post(self, competition_id):
""" Creates a deep copy of the specified competition. """
item_competition = dbc.get.competition(competition_id) item_competition = dbc.get.competition(competition_id)
item_competition_copy = dbc.copy.competition(item_competition) item_competition_copy = dbc.copy.competition(item_competition)
......
"""
All API calls concerning competitions.
Default route: /api/competitions/<competition_id>/slides/<slide_id>/components
"""
import app.core.http_codes as codes import app.core.http_codes as codes
import app.database.controller as dbc import app.database.controller as dbc
from app.apis import item_response, list_response, protect_route from app.apis import item_response, list_response, protect_route
from app.core.dto import ComponentDTO from app.core.dto import ComponentDTO
from flask_restx import Resource
from flask_restx import reqparse
from app.core.parsers import sentinel from app.core.parsers import sentinel
from flask_restx import Resource, reqparse
api = ComponentDTO.api api = ComponentDTO.api
schema = ComponentDTO.schema schema = ComponentDTO.schema
list_schema = ComponentDTO.list_schema list_schema = ComponentDTO.list_schema
component_parser_add = reqparse.RequestParser() component_parser_add = reqparse.RequestParser()
component_parser_add.add_argument("x", type=str, default=0, location="json") component_parser_add.add_argument("x", type=int, default=0, location="json")
component_parser_add.add_argument("y", type=int, default=0, location="json") component_parser_add.add_argument("y", type=int, default=0, location="json")
component_parser_add.add_argument("w", type=int, default=1, location="json") component_parser_add.add_argument("w", type=int, default=1, location="json")
component_parser_add.add_argument("h", type=int, default=1, location="json") component_parser_add.add_argument("h", type=int, default=1, location="json")
...@@ -22,7 +26,7 @@ component_parser_add.add_argument("media_id", type=int, default=None, location=" ...@@ -22,7 +26,7 @@ component_parser_add.add_argument("media_id", type=int, default=None, location="
component_parser_add.add_argument("question_id", type=int, default=None, location="json") component_parser_add.add_argument("question_id", type=int, default=None, location="json")
component_parser_edit = reqparse.RequestParser() component_parser_edit = reqparse.RequestParser()
component_parser_edit.add_argument("x", type=str, default=sentinel, location="json") component_parser_edit.add_argument("x", type=int, default=sentinel, location="json")
component_parser_edit.add_argument("y", type=int, default=sentinel, location="json") component_parser_edit.add_argument("y", type=int, default=sentinel, location="json")
component_parser_edit.add_argument("w", type=int, default=sentinel, location="json") component_parser_edit.add_argument("w", type=int, default=sentinel, location="json")
component_parser_edit.add_argument("h", type=int, default=sentinel, location="json") component_parser_edit.add_argument("h", type=int, default=sentinel, location="json")
...@@ -31,16 +35,39 @@ component_parser_edit.add_argument("media_id", type=int, default=sentinel, locat ...@@ -31,16 +35,39 @@ component_parser_edit.add_argument("media_id", type=int, default=sentinel, locat
component_parser_edit.add_argument("question_id", type=int, default=sentinel, location="json") component_parser_edit.add_argument("question_id", type=int, default=sentinel, location="json")
@api.route("")
@api.param("competition_id, slide_id")
class ComponentList(Resource):
@protect_route(allowed_roles=["*"], allowed_views=["*"])
def get(self, competition_id, slide_id):
""" Gets all components in the specified slide and competition. """
items = dbc.get.component_list(competition_id, slide_id)
return list_response(list_schema.dump(items))
@protect_route(allowed_roles=["*"])
def post(self, competition_id, slide_id):
""" Posts a new component to the specified slide. """
args = component_parser_add.parse_args()
item = dbc.add.component(slide_id=slide_id, **args)
return item_response(schema.dump(item))
@api.route("/<component_id>") @api.route("/<component_id>")
@api.param("competition_id, slide_id, component_id") @api.param("competition_id, slide_id, component_id")
class ComponentByID(Resource): class ComponentByID(Resource):
@protect_route(allowed_roles=["*"], allowed_views=["*"]) @protect_route(allowed_roles=["*"], allowed_views=["*"])
def get(self, competition_id, slide_id, component_id): def get(self, competition_id, slide_id, component_id):
""" Gets the specified component. """
item = dbc.get.component(competition_id, slide_id, component_id) item = dbc.get.component(competition_id, slide_id, component_id)
return item_response(schema.dump(item)) return item_response(schema.dump(item))
@protect_route(allowed_roles=["*"]) @protect_route(allowed_roles=["*"])
def put(self, competition_id, slide_id, component_id): def put(self, competition_id, slide_id, component_id):
""" Edits the specified component using the provided arguments. """
args = component_parser_edit.parse_args(strict=True) args = component_parser_edit.parse_args(strict=True)
item = dbc.get.component(competition_id, slide_id, component_id) item = dbc.get.component(competition_id, slide_id, component_id)
args_without_sentinel = {key: value for key, value in args.items() if value is not sentinel} args_without_sentinel = {key: value for key, value in args.items() if value is not sentinel}
...@@ -49,6 +76,8 @@ class ComponentByID(Resource): ...@@ -49,6 +76,8 @@ class ComponentByID(Resource):
@protect_route(allowed_roles=["*"]) @protect_route(allowed_roles=["*"])
def delete(self, competition_id, slide_id, component_id): def delete(self, competition_id, slide_id, component_id):
""" Deletes the specified component. """
item = dbc.get.component(competition_id, slide_id, component_id) item = dbc.get.component(competition_id, slide_id, component_id)
dbc.delete.component(item) dbc.delete.component(item)
return {}, codes.NO_CONTENT return {}, codes.NO_CONTENT
...@@ -59,21 +88,12 @@ class ComponentByID(Resource): ...@@ -59,21 +88,12 @@ class ComponentByID(Resource):
class ComponentList(Resource): class ComponentList(Resource):
@protect_route(allowed_roles=["*"]) @protect_route(allowed_roles=["*"])
def post(self, competition_id, slide_id, component_id, view_type_id): def post(self, competition_id, slide_id, component_id, view_type_id):
item_component = dbc.get.component(competition_id, slide_id, component_id) """ Creates a deep copy of the specified component. """
item = dbc.copy.component(item_component, slide_id, view_type_id)
return item_response(schema.dump(item))
@api.route("")
@api.param("competition_id, slide_id")
class ComponentList(Resource):
@protect_route(allowed_roles=["*"], allowed_views=["*"])
def get(self, competition_id, slide_id):
items = dbc.get.component_list(competition_id, slide_id)
return list_response(list_schema.dump(items))
@protect_route(allowed_roles=["*"]) item_component = dbc.get.component(
def post(self, competition_id, slide_id): competition_id,
args = component_parser_add.parse_args() slide_id,
item = dbc.add.component(slide_id=slide_id, **args) component_id,
)
item = dbc.copy.component(item_component, slide_id, view_type_id)
return item_response(schema.dump(item)) return item_response(schema.dump(item))
"""
All API calls concerning media.
Default route: /api/media
"""
import app.core.files as files
import app.core.http_codes as codes import app.core.http_codes as codes
import app.database.controller as dbc import app.database.controller as dbc
from app.apis import item_response, list_response, protect_route from app.apis import item_response, list_response, protect_route
from app.core.dto import MediaDTO from app.core.dto import MediaDTO
from app.core.parsers import search_parser from app.core.parsers import search_parser, sentinel
from app.database.models import Media from app.database.models import Media
from flask import request from flask import request
from flask_jwt_extended import get_jwt_identity from flask_jwt_extended import get_jwt_identity
from flask_restx import Resource from flask_restx import Resource
from flask_uploads import UploadNotAllowed from flask_uploads import UploadNotAllowed
from sqlalchemy import exc from sqlalchemy import exc
import app.core.files as files
from app.core.parsers import sentinel
api = MediaDTO.api api = MediaDTO.api
image_set = MediaDTO.image_set image_set = MediaDTO.image_set
...@@ -25,12 +29,16 @@ media_parser_search.add_argument("filename", type=str, default=sentinel, locatio ...@@ -25,12 +29,16 @@ media_parser_search.add_argument("filename", type=str, default=sentinel, locatio
class ImageList(Resource): class ImageList(Resource):
@protect_route(allowed_roles=["*"]) @protect_route(allowed_roles=["*"])
def get(self): def get(self):
""" Gets a list of all images with the specified filename. """
args = media_parser_search.parse_args(strict=True) args = media_parser_search.parse_args(strict=True)
items, total = dbc.search.image(**args) items, total = dbc.search.image(**args)
return list_response(list_schema.dump(items), total) return list_response(list_schema.dump(items), total)
@protect_route(allowed_roles=["*"]) @protect_route(allowed_roles=["*"])
def post(self): def post(self):
""" Posts the specified image. """
if "image" not in request.files: if "image" not in request.files:
api.abort(codes.BAD_REQUEST, "Missing image in request.files") api.abort(codes.BAD_REQUEST, "Missing image in request.files")
try: try:
...@@ -51,11 +59,15 @@ class ImageList(Resource): ...@@ -51,11 +59,15 @@ class ImageList(Resource):
class ImageList(Resource): class ImageList(Resource):
@protect_route(allowed_roles=["*"], allowed_views=["*"]) @protect_route(allowed_roles=["*"], allowed_views=["*"])
def get(self, ID): def get(self, ID):
""" Gets the specified image. """
item = dbc.get.one(Media, ID) item = dbc.get.one(Media, ID)
return item_response(schema.dump(item)) return item_response(schema.dump(item))
@protect_route(allowed_roles=["*"]) @protect_route(allowed_roles=["*"])
def delete(self, ID): def delete(self, ID):
""" Deletes the specified image. """
item = dbc.get.one(Media, ID) item = dbc.get.one(Media, ID)
try: try:
files.delete_image_and_thumbnail(item.filename) files.delete_image_and_thumbnail(item.filename)
......
"""
All misc API calls.
Default route: /api/misc
"""
import app.database.controller as dbc import app.database.controller as dbc
from app.apis import list_response, protect_route from app.apis import list_response, protect_route
from app.core import http_codes from app.core import http_codes
from app.core.dto import MiscDTO from app.core.dto import MiscDTO
from app.database.models import City, Competition, ComponentType, MediaType, QuestionType, Role, User, ViewType from app.database.models import City, Competition, ComponentType, MediaType, QuestionType, Role, User, ViewType
from flask_restx import Resource, reqparse from flask_restx import Resource, reqparse
from flask_restx import reqparse
api = MiscDTO.api api = MiscDTO.api
...@@ -24,6 +28,8 @@ name_parser.add_argument("name", type=str, required=True, location="json") ...@@ -24,6 +28,8 @@ name_parser.add_argument("name", type=str, required=True, location="json")
@api.route("/types") @api.route("/types")
class TypesList(Resource): class TypesList(Resource):
def get(self): def get(self):
""" Gets a list of all types. """
result = {} result = {}
result["media_types"] = media_type_schema.dump(dbc.get.all(MediaType)) result["media_types"] = media_type_schema.dump(dbc.get.all(MediaType))
result["component_types"] = component_type_schema.dump(dbc.get.all(ComponentType)) result["component_types"] = component_type_schema.dump(dbc.get.all(ComponentType))
...@@ -36,6 +42,8 @@ class TypesList(Resource): ...@@ -36,6 +42,8 @@ class TypesList(Resource):
class RoleList(Resource): class RoleList(Resource):
@protect_route(allowed_roles=["*"]) @protect_route(allowed_roles=["*"])
def get(self): def get(self):
""" Gets a list of all roles. """
items = dbc.get.all(Role) items = dbc.get.all(Role)
return list_response(role_schema.dump(items)) return list_response(role_schema.dump(items))
...@@ -44,11 +52,15 @@ class RoleList(Resource): ...@@ -44,11 +52,15 @@ class RoleList(Resource):
class CitiesList(Resource): class CitiesList(Resource):
@protect_route(allowed_roles=["*"]) @protect_route(allowed_roles=["*"])
def get(self): def get(self):
""" Gets a list of all cities. """
items = dbc.get.all(City) items = dbc.get.all(City)
return list_response(city_schema.dump(items)) return list_response(city_schema.dump(items))
@protect_route(allowed_roles=["Admin"]) @protect_route(allowed_roles=["Admin"])
def post(self): def post(self):
""" Posts the specified city. """
args = name_parser.parse_args(strict=True) args = name_parser.parse_args(strict=True)
dbc.add.city(args["name"]) dbc.add.city(args["name"])
items = dbc.get.all(City) items = dbc.get.all(City)
...@@ -60,6 +72,8 @@ class CitiesList(Resource): ...@@ -60,6 +72,8 @@ class CitiesList(Resource):
class Cities(Resource): class Cities(Resource):
@protect_route(allowed_roles=["Admin"]) @protect_route(allowed_roles=["Admin"])
def put(self, ID): def put(self, ID):
""" Edits the specified city with the provided arguments. """
item = dbc.get.one(City, ID) item = dbc.get.one(City, ID)
args = name_parser.parse_args(strict=True) args = name_parser.parse_args(strict=True)
item.name = args["name"] item.name = args["name"]
...@@ -69,6 +83,8 @@ class Cities(Resource): ...@@ -69,6 +83,8 @@ class Cities(Resource):
@protect_route(allowed_roles=["Admin"]) @protect_route(allowed_roles=["Admin"])
def delete(self, ID): def delete(self, ID):
""" Deletes the specified city. """
item = dbc.get.one(City, ID) item = dbc.get.one(City, ID)
dbc.delete.default(item) dbc.delete.default(item)
items = dbc.get.all(City) items = dbc.get.all(City)
...@@ -79,6 +95,8 @@ class Cities(Resource): ...@@ -79,6 +95,8 @@ class Cities(Resource):
class Statistics(Resource): class Statistics(Resource):
@protect_route(allowed_roles=["*"]) @protect_route(allowed_roles=["*"])
def get(self): def get(self):
""" Gets statistics. """
user_count = User.query.count() user_count = User.query.count()
competition_count = Competition.query.count() competition_count = Competition.query.count()
region_count = City.query.count() region_count = City.query.count()
......
"""
All API calls concerning question answers.
Default route: /api/competitions/<competition_id>
"""
import app.core.http_codes as codes import app.core.http_codes as codes
import app.database.controller as dbc import app.database.controller as dbc
from app.apis import item_response, list_response, protect_route from app.apis import item_response, list_response, protect_route
from app.core.dto import QuestionDTO from app.core.dto import QuestionDTO
from flask_restx import Resource
from flask_restx import reqparse
from app.core.parsers import sentinel from app.core.parsers import sentinel
from flask_restx import Resource, reqparse
api = QuestionDTO.api api = QuestionDTO.api
schema = QuestionDTO.schema schema = QuestionDTO.schema
...@@ -28,6 +32,8 @@ question_parser_edit.add_argument("correcting_instructions", type=str, default=s ...@@ -28,6 +32,8 @@ question_parser_edit.add_argument("correcting_instructions", type=str, default=s
class QuestionList(Resource): class QuestionList(Resource):
@protect_route(allowed_roles=["*"]) @protect_route(allowed_roles=["*"])
def get(self, competition_id): def get(self, competition_id):
""" Gets all questions in the specified competition. """
items = dbc.get.question_list_for_competition(competition_id) items = dbc.get.question_list_for_competition(competition_id)
return list_response(list_schema.dump(items)) return list_response(list_schema.dump(items))
...@@ -37,11 +43,15 @@ class QuestionList(Resource): ...@@ -37,11 +43,15 @@ class QuestionList(Resource):
class QuestionListForSlide(Resource): class QuestionListForSlide(Resource):
@protect_route(allowed_roles=["*"]) @protect_route(allowed_roles=["*"])
def get(self, competition_id, slide_id): def get(self, competition_id, slide_id):
""" Gets all questions in the specified competition and slide. """
items = dbc.get.question_list(competition_id, slide_id) items = dbc.get.question_list(competition_id, slide_id)
return list_response(list_schema.dump(items)) return list_response(list_schema.dump(items))
@protect_route(allowed_roles=["*"]) @protect_route(allowed_roles=["*"])
def post(self, competition_id, slide_id): def post(self, competition_id, slide_id):
""" Posts a new question to the specified slide using the provided arguments. """
args = question_parser_add.parse_args(strict=True) args = question_parser_add.parse_args(strict=True)
item = dbc.add.question(slide_id=slide_id, **args) item = dbc.add.question(slide_id=slide_id, **args)
return item_response(schema.dump(item)) return item_response(schema.dump(item))
...@@ -52,11 +62,17 @@ class QuestionListForSlide(Resource): ...@@ -52,11 +62,17 @@ class QuestionListForSlide(Resource):
class QuestionById(Resource): class QuestionById(Resource):
@protect_route(allowed_roles=["*"]) @protect_route(allowed_roles=["*"])
def get(self, competition_id, slide_id, question_id): def get(self, competition_id, slide_id, question_id):
"""
Gets the specified question using the specified competition and slide.
"""
item_question = dbc.get.question(competition_id, slide_id, question_id) item_question = dbc.get.question(competition_id, slide_id, question_id)
return item_response(schema.dump(item_question)) return item_response(schema.dump(item_question))
@protect_route(allowed_roles=["*"]) @protect_route(allowed_roles=["*"])
def put(self, competition_id, slide_id, question_id): def put(self, competition_id, slide_id, question_id):
""" Edits the specified question with the provided arguments. """
args = question_parser_edit.parse_args(strict=True) args = question_parser_edit.parse_args(strict=True)
item_question = dbc.get.question(competition_id, slide_id, question_id) item_question = dbc.get.question(competition_id, slide_id, question_id)
...@@ -66,6 +82,8 @@ class QuestionById(Resource): ...@@ -66,6 +82,8 @@ class QuestionById(Resource):
@protect_route(allowed_roles=["*"]) @protect_route(allowed_roles=["*"])
def delete(self, competition_id, slide_id, question_id): def delete(self, competition_id, slide_id, question_id):
""" Deletes the specified question. """
item_question = dbc.get.question(competition_id, slide_id, question_id) item_question = dbc.get.question(competition_id, slide_id, question_id)
dbc.delete.question(item_question) dbc.delete.question(item_question)
return {}, codes.NO_CONTENT return {}, codes.NO_CONTENT
"""
All API calls concerning question alternatives.
Default route: /api/competitions/<competition_id>/slides
"""
import app.core.http_codes as codes import app.core.http_codes as codes
import app.database.controller as dbc import app.database.controller as dbc
from app.apis import item_response, list_response, protect_route from app.apis import item_response, list_response, protect_route
...@@ -21,25 +26,33 @@ slide_parser_edit.add_argument("background_image_id", default=sentinel, type=int ...@@ -21,25 +26,33 @@ slide_parser_edit.add_argument("background_image_id", default=sentinel, type=int
class SlidesList(Resource): class SlidesList(Resource):
@protect_route(allowed_roles=["*"]) @protect_route(allowed_roles=["*"])
def get(self, competition_id): def get(self, competition_id):
""" Gets all slides from the specified competition. """
items = dbc.get.slide_list(competition_id) items = dbc.get.slide_list(competition_id)
return list_response(list_schema.dump(items)) return list_response(list_schema.dump(items))
@protect_route(allowed_roles=["*"]) @protect_route(allowed_roles=["*"])
def post(self, competition_id): def post(self, competition_id):
""" Posts a new slide to the specified competition. """
item_slide = dbc.add.slide(competition_id) item_slide = dbc.add.slide(competition_id)
return item_response(schema.dump(item_slide)) return item_response(schema.dump(item_slide))
@api.route("/<slide_id>") @api.route("/<slide_id>")
@api.param("competition_id,slide_id") @api.param("competition_id, slide_id")
class Slides(Resource): class Slides(Resource):
@protect_route(allowed_roles=["*"]) @protect_route(allowed_roles=["*"])
def get(self, competition_id, slide_id): def get(self, competition_id, slide_id):
""" Gets the specified slide. """
item_slide = dbc.get.slide(competition_id, slide_id) item_slide = dbc.get.slide(competition_id, slide_id)
return item_response(schema.dump(item_slide)) return item_response(schema.dump(item_slide))
@protect_route(allowed_roles=["*"]) @protect_route(allowed_roles=["*"])
def put(self, competition_id, slide_id): def put(self, competition_id, slide_id):
""" Edits the specified slide using the provided arguments. """
args = slide_parser_edit.parse_args(strict=True) args = slide_parser_edit.parse_args(strict=True)
item_slide = dbc.get.slide(competition_id, slide_id) item_slide = dbc.get.slide(competition_id, slide_id)
...@@ -49,6 +62,8 @@ class Slides(Resource): ...@@ -49,6 +62,8 @@ class Slides(Resource):
@protect_route(allowed_roles=["*"]) @protect_route(allowed_roles=["*"])
def delete(self, competition_id, slide_id): def delete(self, competition_id, slide_id):
""" Deletes the specified slide. """
item_slide = dbc.get.slide(competition_id, slide_id) item_slide = dbc.get.slide(competition_id, slide_id)
dbc.delete.slide(item_slide) dbc.delete.slide(item_slide)
...@@ -56,10 +71,12 @@ class Slides(Resource): ...@@ -56,10 +71,12 @@ class Slides(Resource):
@api.route("/<slide_id>/order") @api.route("/<slide_id>/order")
@api.param("competition_id,slide_id") @api.param("competition_id, slide_id")
class SlideOrder(Resource): class SlideOrder(Resource):
@protect_route(allowed_roles=["*"]) @protect_route(allowed_roles=["*"])
def put(self, competition_id, slide_id): def put(self, competition_id, slide_id):
""" Edits the specified slide order using the provided arguments. """
args = slide_parser_edit.parse_args(strict=True) args = slide_parser_edit.parse_args(strict=True)
order = args.get("order") order = args.get("order")
...@@ -89,8 +106,9 @@ class SlideOrder(Resource): ...@@ -89,8 +106,9 @@ class SlideOrder(Resource):
class SlideCopy(Resource): class SlideCopy(Resource):
@protect_route(allowed_roles=["*"]) @protect_route(allowed_roles=["*"])
def post(self, competition_id, slide_id): def post(self, competition_id, slide_id):
item_slide = dbc.get.slide(competition_id, slide_id) """ Creates a deep copy of the specified slide. """
item_slide = dbc.get.slide(competition_id, slide_id)
item_slide_copy = dbc.copy.slide(item_slide) item_slide_copy = dbc.copy.slide(item_slide)
return item_response(schema.dump(item_slide_copy)) return item_response(schema.dump(item_slide_copy))
"""
All API calls concerning question alternatives.
Default route: /api/competitions/<competition_id>/teams
"""
import app.core.http_codes as codes import app.core.http_codes as codes
import app.database.controller as dbc import app.database.controller as dbc
from app.apis import item_response, list_response, protect_route from app.apis import item_response, list_response, protect_route
from app.core.dto import TeamDTO from app.core.dto import TeamDTO
from flask_restx import Resource, reqparse
from app.core.parsers import sentinel from app.core.parsers import sentinel
from flask_restx import Resource, reqparse
api = TeamDTO.api api = TeamDTO.api
schema = TeamDTO.schema schema = TeamDTO.schema
...@@ -21,11 +26,15 @@ team_parser_edit.add_argument("name", type=str, default=sentinel, location="json ...@@ -21,11 +26,15 @@ team_parser_edit.add_argument("name", type=str, default=sentinel, location="json
class TeamsList(Resource): class TeamsList(Resource):
@protect_route(allowed_roles=["*"]) @protect_route(allowed_roles=["*"])
def get(self, competition_id): def get(self, competition_id):
""" Gets all teams to the specified competition. """
items = dbc.get.team_list(competition_id) items = dbc.get.team_list(competition_id)
return list_response(list_schema.dump(items)) return list_response(list_schema.dump(items))
@protect_route(allowed_roles=["*"]) @protect_route(allowed_roles=["*"])
def post(self, competition_id): def post(self, competition_id):
""" Posts a new team to the specified competition. """
args = team_parser_add.parse_args(strict=True) args = team_parser_add.parse_args(strict=True)
item_team = dbc.add.team(args["name"], competition_id) item_team = dbc.add.team(args["name"], competition_id)
return item_response(schema.dump(item_team)) return item_response(schema.dump(item_team))
...@@ -36,18 +45,15 @@ class TeamsList(Resource): ...@@ -36,18 +45,15 @@ class TeamsList(Resource):
class Teams(Resource): class Teams(Resource):
@protect_route(allowed_roles=["*"]) @protect_route(allowed_roles=["*"])
def get(self, competition_id, team_id): def get(self, competition_id, team_id):
""" Gets the specified team. """
item = dbc.get.team(competition_id, team_id) item = dbc.get.team(competition_id, team_id)
return item_response(schema.dump(item)) return item_response(schema.dump(item))
@protect_route(allowed_roles=["*"])
def delete(self, competition_id, team_id):
item_team = dbc.get.team(competition_id, team_id)
dbc.delete.team(item_team)
return {}, codes.NO_CONTENT
@protect_route(allowed_roles=["*"]) @protect_route(allowed_roles=["*"])
def put(self, competition_id, team_id): def put(self, competition_id, team_id):
""" Edits the specified team using the provided arguments. """
args = team_parser_edit.parse_args(strict=True) args = team_parser_edit.parse_args(strict=True)
name = args.get("name") name = args.get("name")
...@@ -55,3 +61,12 @@ class Teams(Resource): ...@@ -55,3 +61,12 @@ class Teams(Resource):
item_team = dbc.edit.default(item_team, name=name, competition_id=competition_id) item_team = dbc.edit.default(item_team, name=name, competition_id=competition_id)
return item_response(schema.dump(item_team)) return item_response(schema.dump(item_team))
@protect_route(allowed_roles=["*"])
def delete(self, competition_id, team_id):
""" Deletes the specified team. """
item_team = dbc.get.team(competition_id, team_id)
dbc.delete.team(item_team)
return {}, codes.NO_CONTENT
"""
All API calls concerning question alternatives.
Default route: /api/users
"""
import app.core.http_codes as codes import app.core.http_codes as codes
import app.database.controller as dbc import app.database.controller as dbc
from app.apis import item_response, list_response, protect_route from app.apis import item_response, list_response, protect_route
from app.core.dto import UserDTO from app.core.dto import UserDTO
from flask_jwt_extended import get_jwt_identity
from flask_restx import Resource
from flask_restx import inputs, reqparse
from app.core.parsers import search_parser, sentinel from app.core.parsers import search_parser, sentinel
from app.database.models import User
from flask_jwt_extended import get_jwt_identity
from flask_restx import Resource, inputs, reqparse
api = UserDTO.api api = UserDTO.api
schema = UserDTO.schema schema = UserDTO.schema
...@@ -25,13 +30,14 @@ user_search_parser.add_argument("role_id", type=int, default=sentinel, location= ...@@ -25,13 +30,14 @@ user_search_parser.add_argument("role_id", type=int, default=sentinel, location=
def _edit_user(item_user, args): def _edit_user(item_user, args):
""" Edits a user using the provided arguments. """
email = args.get("email") email = args.get("email")
name = args.get("name") name = args.get("name")
if email: if email:
if dbc.get.user_exists(email): if dbc.get.user_exists(email):
api.abort(codes.BAD_REQUEST, "Email is already in use") api.abort(codes.BAD_REQUEST, "Email is already in use")
if name: if name:
args["name"] = args["name"].title() args["name"] = args["name"].title()
...@@ -42,13 +48,17 @@ def _edit_user(item_user, args): ...@@ -42,13 +48,17 @@ def _edit_user(item_user, args):
class UsersList(Resource): class UsersList(Resource):
@protect_route(allowed_roles=["*"]) @protect_route(allowed_roles=["*"])
def get(self): def get(self):
item = dbc.get.user(get_jwt_identity()) """ Gets all users. """
item = dbc.get.one(User, get_jwt_identity())
return item_response(schema.dump(item)) return item_response(schema.dump(item))
@protect_route(allowed_roles=["*"]) @protect_route(allowed_roles=["*"])
def put(self): def put(self):
""" Posts a new user using the specified arguments. """
args = user_parser_edit.parse_args(strict=True) args = user_parser_edit.parse_args(strict=True)
item = dbc.get.user(get_jwt_identity()) item = dbc.get.one(User, get_jwt_identity())
item = _edit_user(item, args) item = _edit_user(item, args)
return item_response(schema.dump(item)) return item_response(schema.dump(item))
...@@ -58,13 +68,17 @@ class UsersList(Resource): ...@@ -58,13 +68,17 @@ class UsersList(Resource):
class Users(Resource): class Users(Resource):
@protect_route(allowed_roles=["*"]) @protect_route(allowed_roles=["*"])
def get(self, ID): def get(self, ID):
item = dbc.get.user(ID) """ Gets the specified user. """
item = dbc.get.one(User, ID)
return item_response(schema.dump(item)) return item_response(schema.dump(item))
@protect_route(allowed_roles=["Admin"]) @protect_route(allowed_roles=["Admin"])
def put(self, ID): def put(self, ID):
""" Edits the specified team using the provided arguments. """
args = user_parser_edit.parse_args(strict=True) args = user_parser_edit.parse_args(strict=True)
item = dbc.get.user(ID) item = dbc.get.one(User, ID)
item = _edit_user(item, args) item = _edit_user(item, args)
return item_response(schema.dump(item)) return item_response(schema.dump(item))
...@@ -73,6 +87,8 @@ class Users(Resource): ...@@ -73,6 +87,8 @@ class Users(Resource):
class UserSearch(Resource): class UserSearch(Resource):
@protect_route(allowed_roles=["*"]) @protect_route(allowed_roles=["*"])
def get(self): def get(self):
""" Finds a specific user based on the provided arguments. """
args = user_search_parser.parse_args(strict=True) args = user_search_parser.parse_args(strict=True)
items, total = dbc.search.user(**args) items, total = dbc.search.user(**args)
return list_response(list_schema.dump(items), total) return list_response(list_schema.dump(items), total)
...@@ -49,25 +49,34 @@ def db_add(item): ...@@ -49,25 +49,34 @@ def db_add(item):
except (exc.SQLAlchemyError, exc.DBAPIError): except (exc.SQLAlchemyError, exc.DBAPIError):
db.session.rollback() db.session.rollback()
# SQL errors such as item already exists # SQL errors such as item already exists
abort(codes.INTERNAL_SERVER_ERROR, f"Item of type {type(item)} could not be created") abort(
codes.INTERNAL_SERVER_ERROR,
f"Item of type {type(item)} could not be created",
)
except: except:
db.session.rollback() db.session.rollback()
# Catching other errors # Catching other errors
abort(codes.INTERNAL_SERVER_ERROR, f"Something went wrong when creating {type(item)}") abort(
codes.INTERNAL_SERVER_ERROR,
f"Something went wrong when creating {type(item)}",
)
return item return item
def component(type_id, slide_id, view_type_id, x=0, y=0, w=0, h=0, **data): def component(type_id, slide_id, view_type_id, x=0, y=0, w=0, h=0, **data):
""" """
Adds a component to the slide at the specified coordinates with the Adds a component to the slide at the specified
provided size and data . coordinates with the provided size and data.
""" """
if type_id == 2: # 2 is image if type_id == 2: # 2 is image
item_image = get.one(Media, data["media_id"]) item_image = get.one(Media, data["media_id"])
filename = item_image.filename filename = item_image.filename
path = os.path.join(current_app.config["UPLOADED_PHOTOS_DEST"], filename) path = os.path.join(
current_app.config["UPLOADED_PHOTOS_DEST"],
filename,
)
with Image.open(path) as im: with Image.open(path) as im:
h = im.height h = im.height
w = im.width w = im.width
...@@ -79,13 +88,19 @@ def component(type_id, slide_id, view_type_id, x=0, y=0, w=0, h=0, **data): ...@@ -79,13 +88,19 @@ def component(type_id, slide_id, view_type_id, x=0, y=0, w=0, h=0, **data):
h *= ratio h *= ratio
if type_id == ID_TEXT_COMPONENT: if type_id == ID_TEXT_COMPONENT:
item = db_add(TextComponent(slide_id, type_id, view_type_id, x, y, w, h)) item = db_add(
TextComponent(slide_id, type_id, view_type_id, x, y, w, h),
)
item.text = data.get("text") item.text = data.get("text")
elif type_id == ID_IMAGE_COMPONENT: elif type_id == ID_IMAGE_COMPONENT:
item = db_add(ImageComponent(slide_id, type_id, view_type_id, x, y, w, h)) item = db_add(
ImageComponent(slide_id, type_id, view_type_id, x, y, w, h),
)
item.media_id = data.get("media_id") item.media_id = data.get("media_id")
elif type_id == ID_QUESTION_COMPONENT: elif type_id == ID_QUESTION_COMPONENT:
item = db_add(QuestionComponent(slide_id, type_id, view_type_id, x, y, w, h)) item = db_add(
QuestionComponent(slide_id, type_id, view_type_id, x, y, w, h),
)
item.question_id = data.get("question_id") item.question_id = data.get("question_id")
else: else:
abort(codes.BAD_REQUEST, f"Invalid type_id{type_id}") abort(codes.BAD_REQUEST, f"Invalid type_id{type_id}")
...@@ -122,7 +137,7 @@ def slide(competition_id): ...@@ -122,7 +137,7 @@ def slide(competition_id):
item_slide = db_add(Slide(order, competition_id)) item_slide = db_add(Slide(order, competition_id))
# Add default question # Add default question
question(f"Fråga {item_slide.order + 1}", 10, 0, item_slide.id) question(f"Fråga {item_slide.order + 1}", 10, 1, item_slide.id)
item_slide = utils.refresh(item_slide) item_slide = utils.refresh(item_slide)
return item_slide return item_slide
...@@ -258,8 +273,18 @@ def question(name, total_score, type_id, slide_id, correcting_instructions=None) ...@@ -258,8 +273,18 @@ def question(name, total_score, type_id, slide_id, correcting_instructions=None)
def question_alternative(text, value, question_id): def question_alternative(text, value, question_id):
"""
Adds a question alternative to the specified
question using the provided arguments.
"""
return db_add(QuestionAlternative(text, value, question_id)) return db_add(QuestionAlternative(text, value, question_id))
def question_answer(answer, score, question_id, team_id): def question_answer(answer, score, question_id, team_id):
"""
Adds a question answer to the specified team
and question using the provided arguments.
"""
return db_add(QuestionAnswer(answer, score, question_id, team_id)) return db_add(QuestionAnswer(answer, score, question_id, team_id))
...@@ -8,7 +8,9 @@ from app.database.types import ID_IMAGE_COMPONENT, ID_QUESTION_COMPONENT, ID_TEX ...@@ -8,7 +8,9 @@ from app.database.types import ID_IMAGE_COMPONENT, ID_QUESTION_COMPONENT, ID_TEX
def _alternative(item_old, question_id): def _alternative(item_old, question_id):
"""Internal function. Makes a copy of the provided question alternative""" """
Internal function. Makes a copy of the provided question alternative.
"""
return add.question_alternative(item_old.text, item_old.value, question_id) return add.question_alternative(item_old.text, item_old.value, question_id)
...@@ -73,7 +75,7 @@ def component(item_component, slide_id_new, view_type_id): ...@@ -73,7 +75,7 @@ def component(item_component, slide_id_new, view_type_id):
def slide(item_slide_old): def slide(item_slide_old):
""" """
Deep copies a slide to the same competition. Deep copies a slide to the same competition.
Does not copy team, question answers. Does not copy team and question answers.
""" """
item_competition = get.competition(item_slide_old.competition_id) item_competition = get.competition(item_slide_old.competition_id)
...@@ -98,7 +100,6 @@ def slide_to_competition(item_slide_old, item_competition): ...@@ -98,7 +100,6 @@ def slide_to_competition(item_slide_old, item_competition):
for item_component in item_slide_old.components: for item_component in item_slide_old.components:
_component(item_component, item_slide_new) _component(item_component, item_slide_new)
for item_question in item_slide_old.questions: for item_question in item_slide_old.questions:
_question(item_question, item_slide_new.id) _question(item_question, item_slide_new.id)
...@@ -123,7 +124,7 @@ def competition(item_competition_old): ...@@ -123,7 +124,7 @@ def competition(item_competition_old):
item_competition_old.city_id, item_competition_old.city_id,
item_competition_old.font, item_competition_old.font,
) )
# TODO: Add background image
item_competition_new.background_image_id = item_competition_old.background_image_id item_competition_new.background_image_id = item_competition_old.background_image_id
for item_slide in item_competition_old.slides: for item_slide in item_competition_old.slides:
......
...@@ -11,19 +11,25 @@ from flask_restx import abort ...@@ -11,19 +11,25 @@ from flask_restx import abort
def default(item): def default(item):
""" Deletes item and commits. """ """ Deletes item and commits. """
try: try:
db.session.delete(item) db.session.delete(item)
db.session.commit() db.session.commit()
except: except:
db.session.rollback() db.session.rollback()
abort(codes.INTERNAL_SERVER_ERROR, f"Item of type {type(item)} could not be deleted") abort(
codes.INTERNAL_SERVER_ERROR,
f"Item of type {type(item)} could not be deleted",
)
def whitelist_to_blacklist(filters): def whitelist_to_blacklist(filters):
""" """
Remove whitelist by condition(filters) and insert those into blacklist Remove whitelist by condition(filters) and insert those into blacklist.
Example: When delete user all whitelisted tokens for that user should be blacklisted Example: When delete user all whitelisted tokens for that user should
be blacklisted.
""" """
whitelist = Whitelist.query.filter(filters).all() whitelist = Whitelist.query.filter(filters).all()
for item in whitelist: for item in whitelist:
dbc.add.blacklist(item.jti) dbc.add.blacklist(item.jti)
...@@ -43,7 +49,6 @@ def _slide(item_slide): ...@@ -43,7 +49,6 @@ def _slide(item_slide):
for item_question in item_slide.questions: for item_question in item_slide.questions:
question(item_question) question(item_question)
for item_component in item_slide.components: for item_component in item_slide.components:
default(item_component) default(item_component)
...@@ -85,6 +90,7 @@ def question(item_question): ...@@ -85,6 +90,7 @@ def question(item_question):
question_answers(item_question_answer) question_answers(item_question_answer)
for item_alternative in item_question.alternatives: for item_alternative in item_question.alternatives:
alternatives(item_alternative) alternatives(item_alternative)
default(item_question) default(item_question)
......
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
This file contains functionality to get data from the database. This file contains functionality to get data from the database.
""" """
from sqlalchemy.orm.util import with_polymorphic
from app.core import db from app.core import db
from app.core import http_codes as codes from app.core import http_codes as codes
from app.database.models import ( from app.database.models import (
...@@ -18,11 +17,12 @@ from app.database.models import ( ...@@ -18,11 +17,12 @@ from app.database.models import (
TextComponent, TextComponent,
User, User,
) )
from sqlalchemy.orm import joinedload, subqueryload from sqlalchemy.orm import joinedload
from sqlalchemy.orm.util import with_polymorphic
def all(db_type): def all(db_type):
""" Gets lazy db-item in the provided table. """ """ Gets a list of all lazy db-items in the provided table. """
return db_type.query.all() return db_type.query.all()
...@@ -43,7 +43,10 @@ def code_by_code(code): ...@@ -43,7 +43,10 @@ def code_by_code(code):
def code_list(competition_id): def code_list(competition_id):
""" Gets a list of all code objects associated with a the provided competition. """ """
Gets a list of all code objects associated with the provided competition.
"""
# team_view_id = 1 # team_view_id = 1
join_competition = Competition.id == Code.competition_id join_competition = Competition.id == Code.competition_id
filters = Competition.id == competition_id filters = Competition.id == competition_id
...@@ -57,20 +60,18 @@ def user_exists(email): ...@@ -57,20 +60,18 @@ def user_exists(email):
return User.query.filter(User.email == email).count() > 0 return User.query.filter(User.email == email).count() > 0
def user(user_id):
""" Gets the user object associated with the provided id. """
return User.query.filter(User.id == user_id).first_extended()
def user_by_email(email): def user_by_email(email):
""" Gets the user object associated with the provided email. """ """ Gets the user object associated with the provided email. """
return User.query.filter(User.email == email).first_extended(error_code=codes.UNAUTHORIZED) return User.query.filter(User.email == email).first_extended(error_code=codes.UNAUTHORIZED)
### Slides ### ### Slides ###
def slide(competition_id, slide_id): def slide(competition_id, slide_id):
""" Gets the slide object associated with the provided id and order. """ """
Gets the slide object associated with the provided competition and slide.
"""
join_competition = Competition.id == Slide.competition_id join_competition = Competition.id == Slide.competition_id
filters = (Competition.id == competition_id) & (Slide.id == slide_id) filters = (Competition.id == competition_id) & (Slide.id == slide_id)
...@@ -78,7 +79,10 @@ def slide(competition_id, slide_id): ...@@ -78,7 +79,10 @@ def slide(competition_id, slide_id):
def slide_list(competition_id): def slide_list(competition_id):
""" Gets a list of all slide objects associated with a the provided competition. """ """
Gets a list of all slide objects associated with the provided competition.
"""
join_competition = Competition.id == Slide.competition_id join_competition = Competition.id == Slide.competition_id
filters = Competition.id == competition_id filters = Competition.id == competition_id
...@@ -91,15 +95,10 @@ def slide_count(competition_id): ...@@ -91,15 +95,10 @@ def slide_count(competition_id):
return Slide.query.filter(Slide.competition_id == competition_id).count() return Slide.query.filter(Slide.competition_id == competition_id).count()
def slide_count(competition_id):
""" Gets the number of slides in the provided competition. """
return Slide.query.filter(Slide.competition_id == competition_id).count()
### Teams ### ### Teams ###
def team(competition_id, team_id): def team(competition_id, team_id):
""" Gets the team object associated with the provided id and competition id. """ """ Gets the team object associated with the competition and team. """
join_competition = Competition.id == Team.competition_id join_competition = Competition.id == Team.competition_id
filters = (Competition.id == competition_id) & (Team.id == team_id) filters = (Competition.id == competition_id) & (Team.id == team_id)
...@@ -107,19 +106,22 @@ def team(competition_id, team_id): ...@@ -107,19 +106,22 @@ def team(competition_id, team_id):
def team_list(competition_id): def team_list(competition_id):
""" Gets a list of all team objects associated with a the provided competition. """ """
Gets a list of all team objects associated with the provided competition.
"""
join_competition = Competition.id == Team.competition_id join_competition = Competition.id == Team.competition_id
filters = Competition.id == competition_id filters = Competition.id == competition_id
return Team.query.join(Competition, join_competition).filter(filters).all() return Team.query.join(Competition, join_competition).filter(filters).all()
return Team.query.join(Competition, join_competition).filter(filters).all()
### Questions ### ### Questions ###
def question(competition_id, slide_id, question_id): def question(competition_id, slide_id, question_id):
""" Gets the question object associated with the provided id, slide order and competition id. """ """
Gets the question object associated with the
provided, competition, slide and question.
"""
join_competition = Competition.id == Slide.competition_id join_competition = Competition.id == Slide.competition_id
join_slide = Slide.id == Question.slide_id join_slide = Slide.id == Question.slide_id
...@@ -129,7 +131,10 @@ def question(competition_id, slide_id, question_id): ...@@ -129,7 +131,10 @@ def question(competition_id, slide_id, question_id):
def question_list(competition_id, slide_id): def question_list(competition_id, slide_id):
""" Gets a list of all question objects associated with a the provided competition and slide. """ """
Gets a list of all question objects associated
with the provided competition and slide.
"""
join_competition = Competition.id == Slide.competition_id join_competition = Competition.id == Slide.competition_id
join_slide = Slide.id == Question.slide_id join_slide = Slide.id == Question.slide_id
...@@ -139,7 +144,10 @@ def question_list(competition_id, slide_id): ...@@ -139,7 +144,10 @@ def question_list(competition_id, slide_id):
def question_list_for_competition(competition_id): def question_list_for_competition(competition_id):
""" Gets a list of all question objects associated with a the provided competition. """ """
Gets a list of all question objects associated
with the provided competition.
"""
join_competition = Competition.id == Slide.competition_id join_competition = Competition.id == Slide.competition_id
join_slide = Slide.id == Question.slide_id join_slide = Slide.id == Question.slide_id
...@@ -149,8 +157,16 @@ def question_list_for_competition(competition_id): ...@@ -149,8 +157,16 @@ def question_list_for_competition(competition_id):
### Question Alternative ### ### Question Alternative ###
def question_alternative(competition_id, slide_id, question_id, alternative_id): def question_alternative(
""" Get question alternative for a given question based on its competition and slide and ID. """ competition_id,
slide_id,
question_id,
alternative_id,
):
"""
Get a question alternative for a given question
based on its competition, slide and question.
"""
join_competition = Competition.id == Slide.competition_id join_competition = Competition.id == Slide.competition_id
join_slide = Slide.id == Question.slide_id join_slide = Slide.id == Question.slide_id
...@@ -172,7 +188,11 @@ def question_alternative(competition_id, slide_id, question_id, alternative_id): ...@@ -172,7 +188,11 @@ def question_alternative(competition_id, slide_id, question_id, alternative_id):
def question_alternative_list(competition_id, slide_id, question_id): def question_alternative_list(competition_id, slide_id, question_id):
""" Get all question alternatives for a given question based on its competition and slide. """ """
Get a list of all question alternative objects for a
given question based on its competition and slide.
"""
join_competition = Competition.id == Slide.competition_id join_competition = Competition.id == Slide.competition_id
join_slide = Slide.id == Question.slide_id join_slide = Slide.id == Question.slide_id
join_question = Question.id == QuestionAlternative.question_id join_question = Question.id == QuestionAlternative.question_id
...@@ -186,18 +206,13 @@ def question_alternative_list(competition_id, slide_id, question_id): ...@@ -186,18 +206,13 @@ def question_alternative_list(competition_id, slide_id, question_id):
.all() .all()
) )
return (
QuestionAlternative.query.join(Competition, join_competition)
.join(Slide, join_slide)
.join(Question, join_question)
.filter(filters)
.all()
)
### Question Answers ### ### Question Answers ###
def question_answer(competition_id, team_id, answer_id): def question_answer(competition_id, team_id, answer_id):
""" Get question answer for a given team based on its competition and ID. """ """
Get question answer for a given team based on its competition.
"""
join_competition = Competition.id == Team.competition_id join_competition = Competition.id == Team.competition_id
join_team = Team.id == QuestionAnswer.team_id join_team = Team.id == QuestionAnswer.team_id
filters = (Competition.id == competition_id) & (Team.id == team_id) & (QuestionAnswer.id == answer_id) filters = (Competition.id == competition_id) & (Team.id == team_id) & (QuestionAnswer.id == answer_id)
...@@ -207,7 +222,10 @@ def question_answer(competition_id, team_id, answer_id): ...@@ -207,7 +222,10 @@ def question_answer(competition_id, team_id, answer_id):
def question_answer_list(competition_id, team_id): def question_answer_list(competition_id, team_id):
""" Get question answer for a given team based on its competition. """ """
Get a list of question answers for a given team based on its competition.
"""
join_competition = Competition.id == Team.competition_id join_competition = Competition.id == Team.competition_id
join_team = Team.id == QuestionAnswer.team_id join_team = Team.id == QuestionAnswer.team_id
filters = (Competition.id == competition_id) & (Team.id == team_id) filters = (Competition.id == competition_id) & (Team.id == team_id)
...@@ -216,7 +234,10 @@ def question_answer_list(competition_id, team_id): ...@@ -216,7 +234,10 @@ def question_answer_list(competition_id, team_id):
### Components ### ### Components ###
def component(competition_id, slide_id, component_id): def component(competition_id, slide_id, component_id):
""" Gets a list of all component objects associated with a the provided competition id and slide order. """ """
Gets a component object associated with
the provided competition id and slide order.
"""
join_competition = Competition.id == Slide.competition_id join_competition = Competition.id == Slide.competition_id
join_slide = Slide.id == Component.slide_id join_slide = Slide.id == Component.slide_id
...@@ -233,7 +254,10 @@ def component(competition_id, slide_id, component_id): ...@@ -233,7 +254,10 @@ def component(competition_id, slide_id, component_id):
def component_list(competition_id, slide_id): def component_list(competition_id, slide_id):
""" Gets a list of all component objects associated with a the provided competition id and slide order. """ """
Gets a list of all component objects associated with
the provided competition and slide.
"""
join_competition = Competition.id == Slide.competition_id join_competition = Competition.id == Slide.competition_id
join_slide = Slide.id == Component.slide_id join_slide = Slide.id == Component.slide_id
...@@ -243,7 +267,8 @@ def component_list(competition_id, slide_id): ...@@ -243,7 +267,8 @@ def component_list(competition_id, slide_id):
### Competitions ### ### Competitions ###
def competition(competition_id): def competition(competition_id):
""" Get Competition and all it's sub-entities """ """ Get Competition and all it's sub-entities. """
os1 = joinedload(Competition.slides).joinedload(Slide.components) os1 = joinedload(Competition.slides).joinedload(Slide.components)
os2 = joinedload(Competition.slides).joinedload(Slide.questions).joinedload(Question.alternatives) os2 = joinedload(Competition.slides).joinedload(Slide.questions).joinedload(Question.alternatives)
ot = joinedload(Competition.teams).joinedload(Team.question_answers) ot = joinedload(Competition.teams).joinedload(Team.question_answers)
......
...@@ -10,6 +10,8 @@ from flask_restx import abort ...@@ -10,6 +10,8 @@ from flask_restx import abort
def move_slides(item_competition, start_order, end_order): def move_slides(item_competition, start_order, end_order):
""" Changes a slide order and then arranges other affected slides. """
slides = item_competition.slides slides = item_competition.slides
# Move up # Move up
if start_order < end_order: if start_order < end_order:
...@@ -40,6 +42,7 @@ def generate_unique_code(): ...@@ -40,6 +42,7 @@ def generate_unique_code():
def refresh(item): def refresh(item):
""" Refreshes the provided item. """ """ Refreshes the provided item. """
try: try:
db.session.refresh(item) db.session.refresh(item)
except Exception as e: except Exception as e:
...@@ -49,7 +52,8 @@ def refresh(item): ...@@ -49,7 +52,8 @@ def refresh(item):
def commit(): def commit():
""" Commits. """ """ Commits to the database. """
try: try:
db.session.commit() db.session.commit()
except Exception as e: except Exception as e:
......
...@@ -61,8 +61,9 @@ class User(db.Model): ...@@ -61,8 +61,9 @@ class User(db.Model):
_password = db.Column(db.LargeBinary(60), nullable=False) _password = db.Column(db.LargeBinary(60), nullable=False)
authenticated = db.Column(db.Boolean, default=False) authenticated = db.Column(db.Boolean, default=False)
# twoAuthConfirmed = db.Column(db.Boolean, default=True)
# twoAuthCode = db.Column(db.String(STRING_SIZE), nullable=True) login_attempts = db.Column(db.Integer, nullable=False, default=0)
locked = db.Column(db.DateTime(timezone=True), nullable=True, default=None)
role_id = db.Column(db.Integer, db.ForeignKey("role.id"), nullable=False) role_id = db.Column(db.Integer, db.ForeignKey("role.id"), nullable=False)
city_id = db.Column(db.Integer, db.ForeignKey("city.id"), nullable=False) city_id = db.Column(db.Integer, db.ForeignKey("city.id"), nullable=False)
......
...@@ -17,6 +17,8 @@ class Config: ...@@ -17,6 +17,8 @@ class Config:
THUMBNAIL_SIZE = (120, 120) THUMBNAIL_SIZE = (120, 120)
SECRET_KEY = os.urandom(24) SECRET_KEY = os.urandom(24)
SQLALCHEMY_ECHO = False SQLALCHEMY_ECHO = False
USER_LOGIN_LOCKED_ATTEMPTS = 12
USER_LOGIN_LOCKED_EXPIRES = timedelta(hours=3)
class DevelopmentConfig(Config): class DevelopmentConfig(Config):
...@@ -34,6 +36,8 @@ class DevelopmentConfig(Config): ...@@ -34,6 +36,8 @@ class DevelopmentConfig(Config):
class TestingConfig(Config): class TestingConfig(Config):
TESTING = True TESTING = True
SQLALCHEMY_DATABASE_URI = "sqlite:///test.db" SQLALCHEMY_DATABASE_URI = "sqlite:///test.db"
USER_LOGIN_LOCKED_ATTEMPTS = 4
USER_LOGIN_LOCKED_EXPIRES = timedelta(seconds=4)
class ProductionConfig(Config): class ProductionConfig(Config):
......