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 CheckBoxOutlinedIcon from '@material-ui/icons/CheckBoxOutlined'
import CreateOutlinedIcon from '@material-ui/icons/CreateOutlined'
import DnsOutlinedIcon from '@material-ui/icons/DnsOutlined'
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) => {
if (slide.questions && slide.questions[0] && slide.questions[0].type_id) {
......@@ -13,7 +14,9 @@ export const renderSlideIcon = (slide: RichSlide) => {
case 2:
return <BuildOutlinedIcon /> // practical qustion
case 3:
return <DnsOutlinedIcon /> // multiple choice question
return <CheckBoxOutlinedIcon /> // multiple choice question
case 4:
return <RadioButtonCheckedIcon /> // single choice question
}
} else {
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.database.controller as dbc
from app.apis import item_response, list_response, protect_route
from app.core.dto import QuestionAlternativeDTO
from flask_restx import Resource
from flask_restx import reqparse
from app.core.parsers import sentinel
from flask_restx import Resource, reqparse
api = QuestionAlternativeDTO.api
schema = QuestionAlternativeDTO.schema
......@@ -24,11 +28,22 @@ alternative_parser_edit.add_argument("value", type=int, default=sentinel, locati
class QuestionAlternativeList(Resource):
@protect_route(allowed_roles=["*"], allowed_views=["*"])
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))
@protect_route(allowed_roles=["*"])
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)
item = dbc.add.question_alternative(**args, question_id=question_id)
return item_response(schema.dump(item))
......@@ -39,18 +54,41 @@ class QuestionAlternativeList(Resource):
class QuestionAlternatives(Resource):
@protect_route(allowed_roles=["*"], allowed_views=["*"])
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))
@protect_route(allowed_roles=["*"])
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)
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)
return item_response(schema.dump(item))
@protect_route(allowed_roles=["*"])
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)
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
from app.apis import item_response, list_response, protect_route
from app.core.dto import QuestionAnswerDTO
from flask_restx import Resource
from flask_restx import reqparse
from app.core.parsers import sentinel
from flask_restx import Resource, reqparse
api = QuestionAnswerDTO.api
schema = QuestionAnswerDTO.schema
......@@ -24,11 +28,18 @@ answer_parser_edit.add_argument("score", type=int, default=sentinel, location="j
class QuestionAnswerList(Resource):
@protect_route(allowed_roles=["*"], allowed_views=["*"])
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)
return list_response(list_schema.dump(items))
@protect_route(allowed_roles=["*"], allowed_views=["*"])
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)
item = dbc.add.question_answer(**args, team_id=team_id)
return item_response(schema.dump(item))
......@@ -39,12 +50,19 @@ class QuestionAnswerList(Resource):
class QuestionAnswers(Resource):
@protect_route(allowed_roles=["*"], allowed_views=["*"])
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)
return item_response(schema.dump(item))
@protect_route(allowed_roles=["*"], allowed_views=["*"])
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)
item = dbc.get.question_answer(competition_id, team_id, answer_id)
item = dbc.edit.default(item, **args)
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.database.controller as dbc
......@@ -6,7 +11,8 @@ from app.apis import item_response, protect_route, text_response
from app.core import sockets
from app.core.codes import verify_code
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.utils import get_jti
from flask_restx import Resource, inputs, reqparse
......@@ -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.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):
""" Gets user details for jwt-token. """
return {"role": item_user.role.name, "city_id": item_user.city_id}
def get_code_claims(item_code):
""" Gets code details for jwt-token. """
return {
"view": item_code.view_type.name,
"competition_id": item_code.competition_id,
......@@ -44,6 +57,8 @@ def get_code_claims(item_code):
class AuthSignup(Resource):
@protect_route(allowed_roles=["Admin"], allowed_views=["*"])
def get(self):
""" Tests that the user is an admin. """
return "ok"
......@@ -51,12 +66,16 @@ class AuthSignup(Resource):
class AuthSignup(Resource):
@protect_route(allowed_roles=["Admin"])
def post(self):
""" Creates a new user if the user does not already exist. """
args = create_user_parser.parse_args(strict=True)
email = args.get("email")
# Check if email is already used
if dbc.get.user_exists(email):
api.abort(codes.BAD_REQUEST, "User already exists")
# Add user
item_user = dbc.add.user(**args)
return item_response(schema.dump(item_user))
......@@ -66,28 +85,65 @@ class AuthSignup(Resource):
class AuthDelete(Resource):
@protect_route(allowed_roles=["Admin"])
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.default(item_user)
# Delete user
dbc.delete.default(item_user)
return text_response(f"User {user_id} deleted")
@api.route("/login")
class AuthLogin(Resource):
def post(self):
""" Logs in the specified user and creates a jwt-token. """
args = login_parser.parse_args(strict=True)
email = args.get("email")
password = args.get("password")
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")
# 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))
# 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}
# Whitelist the created jwt
dbc.add.whitelist(get_jti(access_token), item_user.id)
return response
......@@ -95,9 +151,12 @@ class AuthLogin(Resource):
@api.route("/login/code")
class AuthLoginCode(Resource):
def post(self):
""" Logs in using the provided competition code. """
args = login_code_parser.parse_args()
code = args["code"]
# Check so the code string is valid
if not verify_code(code):
api.abort(codes.UNAUTHORIZED, "Invalid code")
......@@ -107,10 +166,12 @@ class AuthLoginCode(Resource):
if item_code.competition_id not in sockets.presentations:
api.abort(codes.UNAUTHORIZED, "Competition not active")
# Create jwt that is only valid for 8 hours
access_token = create_access_token(
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)
response = {
"competition_id": item_code.competition_id,
......@@ -125,9 +186,16 @@ class AuthLoginCode(Resource):
class AuthLogout(Resource):
@protect_route(allowed_roles=["*"], allowed_views=["*"])
def post(self):
""" Logs out. """
jti = get_raw_jwt()["jti"]
# Blacklist the token so the user cannot access the api anymore
dbc.add.blacklist(jti)
# Remove the the token from the whitelist since it's blacklisted now
Whitelist.query.filter(Whitelist.jti == jti).delete()
dbc.utils.commit()
return text_response("Logout")
......
"""
All API calls concerning competition codes.
Default route: /api/competitions/<competition_id>/codes
"""
import app.database.controller as dbc
from app.apis import item_response, list_response, protect_route
from app.core.dto import CodeDTO
......@@ -14,6 +19,8 @@ list_schema = CodeDTO.list_schema
class CodesList(Resource):
@protect_route(allowed_roles=["*"], allowed_views=["Operator"])
def get(self, competition_id):
""" Gets the all competition codes. """
items = dbc.get.code_list(competition_id)
return list_response(list_schema.dump(items), len(items))
......@@ -23,6 +30,8 @@ class CodesList(Resource):
class CodesById(Resource):
@protect_route(allowed_roles=["*"])
def put(self, competition_id, code_id):
""" Generates a new competition code. """
item = dbc.get.one(Code, code_id)
item.code = dbc.utils.generate_unique_code()
dbc.utils.commit_and_refresh(item)
......
"""
All API calls concerning competitions.
Default route: /api/competitions
"""
import app.database.controller as dbc
from app.apis import item_response, list_response, protect_route
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.database.models import Competition
from flask_restx import Resource, reqparse
api = CompetitionDTO.api
schema = CompetitionDTO.schema
......@@ -32,6 +36,8 @@ competition_parser_search.add_argument("city_id", type=int, default=sentinel, lo
class CompetitionsList(Resource):
@protect_route(allowed_roles=["*"])
def post(self):
""" Posts a new competition. """
args = competition_parser_add.parse_args(strict=True)
# Add competition
......@@ -47,12 +53,16 @@ class CompetitionsList(Resource):
class Competitions(Resource):
@protect_route(allowed_roles=["*"], allowed_views=["*"])
def get(self, competition_id):
""" Gets the specified competition. """
item = dbc.get.competition(competition_id)
return item_response(rich_schema.dump(item))
@protect_route(allowed_roles=["*"])
def put(self, competition_id):
""" Edits the specified competition with the specified arguments. """
args = competition_parser_edit.parse_args(strict=True)
item = dbc.get.one(Competition, competition_id)
item = dbc.edit.default(item, **args)
......@@ -61,6 +71,8 @@ class Competitions(Resource):
@protect_route(allowed_roles=["*"])
def delete(self, competition_id):
""" Deletes the specified competition. """
item = dbc.get.one(Competition, competition_id)
dbc.delete.competition(item)
......@@ -71,6 +83,8 @@ class Competitions(Resource):
class CompetitionSearch(Resource):
@protect_route(allowed_roles=["*"])
def get(self):
""" Finds a specific competition based on the provided arguments. """
args = competition_parser_search.parse_args(strict=True)
items, total = dbc.search.competition(**args)
return list_response(list_schema.dump(items), total)
......@@ -81,6 +95,8 @@ class CompetitionSearch(Resource):
class SlidesOrder(Resource):
@protect_route(allowed_roles=["*"])
def post(self, competition_id):
""" Creates a deep copy of the specified competition. """
item_competition = dbc.get.competition(competition_id)
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.database.controller as dbc
from app.apis import item_response, list_response, protect_route
from app.core.dto import ComponentDTO
from flask_restx import Resource
from flask_restx import reqparse
from app.core.parsers import sentinel
from flask_restx import Resource, reqparse
api = ComponentDTO.api
schema = ComponentDTO.schema
list_schema = ComponentDTO.list_schema
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("w", 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="
component_parser_add.add_argument("question_id", type=int, default=None, location="json")
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("w", 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
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.param("competition_id, slide_id, component_id")
class ComponentByID(Resource):
@protect_route(allowed_roles=["*"], allowed_views=["*"])
def get(self, competition_id, slide_id, component_id):
""" Gets the specified component. """
item = dbc.get.component(competition_id, slide_id, component_id)
return item_response(schema.dump(item))
@protect_route(allowed_roles=["*"])
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)
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}
......@@ -49,6 +76,8 @@ class ComponentByID(Resource):
@protect_route(allowed_roles=["*"])
def delete(self, competition_id, slide_id, component_id):
""" Deletes the specified component. """
item = dbc.get.component(competition_id, slide_id, component_id)
dbc.delete.component(item)
return {}, codes.NO_CONTENT
......@@ -59,21 +88,12 @@ class ComponentByID(Resource):
class ComponentList(Resource):
@protect_route(allowed_roles=["*"])
def post(self, competition_id, slide_id, component_id, view_type_id):
item_component = dbc.get.component(competition_id, slide_id, component_id)
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))
""" Creates a deep copy of the specified component. """
@protect_route(allowed_roles=["*"])
def post(self, competition_id, slide_id):
args = component_parser_add.parse_args()
item = dbc.add.component(slide_id=slide_id, **args)
item_component = dbc.get.component(
competition_id,
slide_id,
component_id,
)
item = dbc.copy.component(item_component, slide_id, view_type_id)
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.database.controller as dbc
from app.apis import item_response, list_response, protect_route
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 flask import request
from flask_jwt_extended import get_jwt_identity
from flask_restx import Resource
from flask_uploads import UploadNotAllowed
from sqlalchemy import exc
import app.core.files as files
from app.core.parsers import sentinel
api = MediaDTO.api
image_set = MediaDTO.image_set
......@@ -25,12 +29,16 @@ media_parser_search.add_argument("filename", type=str, default=sentinel, locatio
class ImageList(Resource):
@protect_route(allowed_roles=["*"])
def get(self):
""" Gets a list of all images with the specified filename. """
args = media_parser_search.parse_args(strict=True)
items, total = dbc.search.image(**args)
return list_response(list_schema.dump(items), total)
@protect_route(allowed_roles=["*"])
def post(self):
""" Posts the specified image. """
if "image" not in request.files:
api.abort(codes.BAD_REQUEST, "Missing image in request.files")
try:
......@@ -51,11 +59,15 @@ class ImageList(Resource):
class ImageList(Resource):
@protect_route(allowed_roles=["*"], allowed_views=["*"])
def get(self, ID):
""" Gets the specified image. """
item = dbc.get.one(Media, ID)
return item_response(schema.dump(item))
@protect_route(allowed_roles=["*"])
def delete(self, ID):
""" Deletes the specified image. """
item = dbc.get.one(Media, ID)
try:
files.delete_image_and_thumbnail(item.filename)
......
"""
All misc API calls.
Default route: /api/misc
"""
import app.database.controller as dbc
from app.apis import list_response, protect_route
from app.core import http_codes
from app.core.dto import MiscDTO
from app.database.models import City, Competition, ComponentType, MediaType, QuestionType, Role, User, ViewType
from flask_restx import Resource, reqparse
from flask_restx import reqparse
api = MiscDTO.api
......@@ -24,6 +28,8 @@ name_parser.add_argument("name", type=str, required=True, location="json")
@api.route("/types")
class TypesList(Resource):
def get(self):
""" Gets a list of all types. """
result = {}
result["media_types"] = media_type_schema.dump(dbc.get.all(MediaType))
result["component_types"] = component_type_schema.dump(dbc.get.all(ComponentType))
......@@ -36,6 +42,8 @@ class TypesList(Resource):
class RoleList(Resource):
@protect_route(allowed_roles=["*"])
def get(self):
""" Gets a list of all roles. """
items = dbc.get.all(Role)
return list_response(role_schema.dump(items))
......@@ -44,11 +52,15 @@ class RoleList(Resource):
class CitiesList(Resource):
@protect_route(allowed_roles=["*"])
def get(self):
""" Gets a list of all cities. """
items = dbc.get.all(City)
return list_response(city_schema.dump(items))
@protect_route(allowed_roles=["Admin"])
def post(self):
""" Posts the specified city. """
args = name_parser.parse_args(strict=True)
dbc.add.city(args["name"])
items = dbc.get.all(City)
......@@ -60,6 +72,8 @@ class CitiesList(Resource):
class Cities(Resource):
@protect_route(allowed_roles=["Admin"])
def put(self, ID):
""" Edits the specified city with the provided arguments. """
item = dbc.get.one(City, ID)
args = name_parser.parse_args(strict=True)
item.name = args["name"]
......@@ -69,6 +83,8 @@ class Cities(Resource):
@protect_route(allowed_roles=["Admin"])
def delete(self, ID):
""" Deletes the specified city. """
item = dbc.get.one(City, ID)
dbc.delete.default(item)
items = dbc.get.all(City)
......@@ -79,6 +95,8 @@ class Cities(Resource):
class Statistics(Resource):
@protect_route(allowed_roles=["*"])
def get(self):
""" Gets statistics. """
user_count = User.query.count()
competition_count = Competition.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.database.controller as dbc
from app.apis import item_response, list_response, protect_route
from app.core.dto import QuestionDTO
from flask_restx import Resource
from flask_restx import reqparse
from app.core.parsers import sentinel
from flask_restx import Resource, reqparse
api = QuestionDTO.api
schema = QuestionDTO.schema
......@@ -28,6 +32,8 @@ question_parser_edit.add_argument("correcting_instructions", type=str, default=s
class QuestionList(Resource):
@protect_route(allowed_roles=["*"])
def get(self, competition_id):
""" Gets all questions in the specified competition. """
items = dbc.get.question_list_for_competition(competition_id)
return list_response(list_schema.dump(items))
......@@ -37,11 +43,15 @@ class QuestionList(Resource):
class QuestionListForSlide(Resource):
@protect_route(allowed_roles=["*"])
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)
return list_response(list_schema.dump(items))
@protect_route(allowed_roles=["*"])
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)
item = dbc.add.question(slide_id=slide_id, **args)
return item_response(schema.dump(item))
......@@ -52,11 +62,17 @@ class QuestionListForSlide(Resource):
class QuestionById(Resource):
@protect_route(allowed_roles=["*"])
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)
return item_response(schema.dump(item_question))
@protect_route(allowed_roles=["*"])
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)
item_question = dbc.get.question(competition_id, slide_id, question_id)
......@@ -66,6 +82,8 @@ class QuestionById(Resource):
@protect_route(allowed_roles=["*"])
def delete(self, competition_id, slide_id, question_id):
""" Deletes the specified question. """
item_question = dbc.get.question(competition_id, slide_id, question_id)
dbc.delete.question(item_question)
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.database.controller as dbc
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
class SlidesList(Resource):
@protect_route(allowed_roles=["*"])
def get(self, competition_id):
""" Gets all slides from the specified competition. """
items = dbc.get.slide_list(competition_id)
return list_response(list_schema.dump(items))
@protect_route(allowed_roles=["*"])
def post(self, competition_id):
""" Posts a new slide to the specified competition. """
item_slide = dbc.add.slide(competition_id)
return item_response(schema.dump(item_slide))
@api.route("/<slide_id>")
@api.param("competition_id,slide_id")
@api.param("competition_id, slide_id")
class Slides(Resource):
@protect_route(allowed_roles=["*"])
def get(self, competition_id, slide_id):
""" Gets the specified slide. """
item_slide = dbc.get.slide(competition_id, slide_id)
return item_response(schema.dump(item_slide))
@protect_route(allowed_roles=["*"])
def put(self, competition_id, slide_id):
""" Edits the specified slide using the provided arguments. """
args = slide_parser_edit.parse_args(strict=True)
item_slide = dbc.get.slide(competition_id, slide_id)
......@@ -49,6 +62,8 @@ class Slides(Resource):
@protect_route(allowed_roles=["*"])
def delete(self, competition_id, slide_id):
""" Deletes the specified slide. """
item_slide = dbc.get.slide(competition_id, slide_id)
dbc.delete.slide(item_slide)
......@@ -56,10 +71,12 @@ class Slides(Resource):
@api.route("/<slide_id>/order")
@api.param("competition_id,slide_id")
@api.param("competition_id, slide_id")
class SlideOrder(Resource):
@protect_route(allowed_roles=["*"])
def put(self, competition_id, slide_id):
""" Edits the specified slide order using the provided arguments. """
args = slide_parser_edit.parse_args(strict=True)
order = args.get("order")
......@@ -89,8 +106,9 @@ class SlideOrder(Resource):
class SlideCopy(Resource):
@protect_route(allowed_roles=["*"])
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)
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.database.controller as dbc
from app.apis import item_response, list_response, protect_route
from app.core.dto import TeamDTO
from flask_restx import Resource, reqparse
from app.core.parsers import sentinel
from flask_restx import Resource, reqparse
api = TeamDTO.api
schema = TeamDTO.schema
......@@ -21,11 +26,15 @@ team_parser_edit.add_argument("name", type=str, default=sentinel, location="json
class TeamsList(Resource):
@protect_route(allowed_roles=["*"])
def get(self, competition_id):
""" Gets all teams to the specified competition. """
items = dbc.get.team_list(competition_id)
return list_response(list_schema.dump(items))
@protect_route(allowed_roles=["*"])
def post(self, competition_id):
""" Posts a new team to the specified competition. """
args = team_parser_add.parse_args(strict=True)
item_team = dbc.add.team(args["name"], competition_id)
return item_response(schema.dump(item_team))
......@@ -36,18 +45,15 @@ class TeamsList(Resource):
class Teams(Resource):
@protect_route(allowed_roles=["*"])
def get(self, competition_id, team_id):
""" Gets the specified team. """
item = dbc.get.team(competition_id, team_id)
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=["*"])
def put(self, competition_id, team_id):
""" Edits the specified team using the provided arguments. """
args = team_parser_edit.parse_args(strict=True)
name = args.get("name")
......@@ -55,3 +61,12 @@ class Teams(Resource):
item_team = dbc.edit.default(item_team, name=name, competition_id=competition_id)
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.database.controller as dbc
from app.apis import item_response, list_response, protect_route
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.database.models import User
from flask_jwt_extended import get_jwt_identity
from flask_restx import Resource, inputs, reqparse
api = UserDTO.api
schema = UserDTO.schema
......@@ -25,13 +30,14 @@ user_search_parser.add_argument("role_id", type=int, default=sentinel, location=
def _edit_user(item_user, args):
""" Edits a user using the provided arguments. """
email = args.get("email")
name = args.get("name")
if email:
if dbc.get.user_exists(email):
api.abort(codes.BAD_REQUEST, "Email is already in use")
if name:
args["name"] = args["name"].title()
......@@ -42,13 +48,17 @@ def _edit_user(item_user, args):
class UsersList(Resource):
@protect_route(allowed_roles=["*"])
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))
@protect_route(allowed_roles=["*"])
def put(self):
""" Posts a new user using the specified arguments. """
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)
return item_response(schema.dump(item))
......@@ -58,13 +68,17 @@ class UsersList(Resource):
class Users(Resource):
@protect_route(allowed_roles=["*"])
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))
@protect_route(allowed_roles=["Admin"])
def put(self, ID):
""" Edits the specified team using the provided arguments. """
args = user_parser_edit.parse_args(strict=True)
item = dbc.get.user(ID)
item = dbc.get.one(User, ID)
item = _edit_user(item, args)
return item_response(schema.dump(item))
......@@ -73,6 +87,8 @@ class Users(Resource):
class UserSearch(Resource):
@protect_route(allowed_roles=["*"])
def get(self):
""" Finds a specific user based on the provided arguments. """
args = user_search_parser.parse_args(strict=True)
items, total = dbc.search.user(**args)
return list_response(list_schema.dump(items), total)
......@@ -49,25 +49,34 @@ def db_add(item):
except (exc.SQLAlchemyError, exc.DBAPIError):
db.session.rollback()
# 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:
db.session.rollback()
# 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
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
provided size and data .
Adds a component to the slide at the specified
coordinates with the provided size and data.
"""
if type_id == 2: # 2 is image
item_image = get.one(Media, data["media_id"])
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:
h = im.height
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):
h *= ratio
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")
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")
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")
else:
abort(codes.BAD_REQUEST, f"Invalid type_id{type_id}")
......@@ -122,7 +137,7 @@ def slide(competition_id):
item_slide = db_add(Slide(order, competition_id))
# 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)
return item_slide
......@@ -258,8 +273,18 @@ def question(name, total_score, type_id, slide_id, correcting_instructions=None)
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))
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))
......@@ -8,7 +8,9 @@ from app.database.types import ID_IMAGE_COMPONENT, ID_QUESTION_COMPONENT, ID_TEX
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)
......@@ -73,7 +75,7 @@ def component(item_component, slide_id_new, view_type_id):
def slide(item_slide_old):
"""
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)
......@@ -98,7 +100,6 @@ def slide_to_competition(item_slide_old, item_competition):
for item_component in item_slide_old.components:
_component(item_component, item_slide_new)
for item_question in item_slide_old.questions:
_question(item_question, item_slide_new.id)
......@@ -123,7 +124,7 @@ def competition(item_competition_old):
item_competition_old.city_id,
item_competition_old.font,
)
# TODO: Add background image
item_competition_new.background_image_id = item_competition_old.background_image_id
for item_slide in item_competition_old.slides:
......
......@@ -11,19 +11,25 @@ from flask_restx import abort
def default(item):
""" Deletes item and commits. """
try:
db.session.delete(item)
db.session.commit()
except:
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):
"""
Remove whitelist by condition(filters) and insert those into blacklist
Example: When delete user all whitelisted tokens for that user should be blacklisted
Remove whitelist by condition(filters) and insert those into blacklist.
Example: When delete user all whitelisted tokens for that user should
be blacklisted.
"""
whitelist = Whitelist.query.filter(filters).all()
for item in whitelist:
dbc.add.blacklist(item.jti)
......@@ -43,7 +49,6 @@ def _slide(item_slide):
for item_question in item_slide.questions:
question(item_question)
for item_component in item_slide.components:
default(item_component)
......@@ -85,6 +90,7 @@ def question(item_question):
question_answers(item_question_answer)
for item_alternative in item_question.alternatives:
alternatives(item_alternative)
default(item_question)
......
......@@ -2,7 +2,6 @@
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 http_codes as codes
from app.database.models import (
......@@ -18,11 +17,12 @@ from app.database.models import (
TextComponent,
User,
)
from sqlalchemy.orm import joinedload, subqueryload
from sqlalchemy.orm import joinedload
from sqlalchemy.orm.util import with_polymorphic
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()
......@@ -43,7 +43,10 @@ def code_by_code(code):
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
join_competition = Competition.id == Code.competition_id
filters = Competition.id == competition_id
......@@ -57,20 +60,18 @@ def user_exists(email):
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):
""" Gets the user object associated with the provided email. """
return User.query.filter(User.email == email).first_extended(error_code=codes.UNAUTHORIZED)
### Slides ###
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
filters = (Competition.id == competition_id) & (Slide.id == slide_id)
......@@ -78,7 +79,10 @@ def slide(competition_id, slide_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
filters = Competition.id == competition_id
......@@ -91,15 +95,10 @@ def slide_count(competition_id):
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 ###
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
filters = (Competition.id == competition_id) & (Team.id == team_id)
......@@ -107,19 +106,22 @@ def team(competition_id, team_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
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()
### Questions ###
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_slide = Slide.id == Question.slide_id
......@@ -129,7 +131,10 @@ def question(competition_id, slide_id, question_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_slide = Slide.id == Question.slide_id
......@@ -139,7 +144,10 @@ def question_list(competition_id, slide_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_slide = Slide.id == Question.slide_id
......@@ -149,8 +157,16 @@ def question_list_for_competition(competition_id):
### Question Alternative ###
def question_alternative(competition_id, slide_id, question_id, alternative_id):
""" Get question alternative for a given question based on its competition and slide and ID. """
def question_alternative(
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_slide = Slide.id == Question.slide_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):
""" 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_slide = Slide.id == Question.slide_id
join_question = Question.id == QuestionAlternative.question_id
......@@ -186,18 +206,13 @@ def question_alternative_list(competition_id, slide_id, question_id):
.all()
)
return (
QuestionAlternative.query.join(Competition, join_competition)
.join(Slide, join_slide)
.join(Question, join_question)
.filter(filters)
.all()
)
### Question Answers ###
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_team = Team.id == QuestionAnswer.team_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):
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_team = Team.id == QuestionAnswer.team_id
filters = (Competition.id == competition_id) & (Team.id == team_id)
......@@ -216,7 +234,10 @@ def question_answer_list(competition_id, team_id):
### Components ###
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_slide = Slide.id == Component.slide_id
......@@ -233,7 +254,10 @@ def component(competition_id, slide_id, component_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_slide = Slide.id == Component.slide_id
......@@ -243,7 +267,8 @@ def component_list(competition_id, slide_id):
### Competitions ###
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)
os2 = joinedload(Competition.slides).joinedload(Slide.questions).joinedload(Question.alternatives)
ot = joinedload(Competition.teams).joinedload(Team.question_answers)
......
......@@ -10,6 +10,8 @@ from flask_restx import abort
def move_slides(item_competition, start_order, end_order):
""" Changes a slide order and then arranges other affected slides. """
slides = item_competition.slides
# Move up
if start_order < end_order:
......@@ -40,6 +42,7 @@ def generate_unique_code():
def refresh(item):
""" Refreshes the provided item. """
try:
db.session.refresh(item)
except Exception as e:
......@@ -49,7 +52,8 @@ def refresh(item):
def commit():
""" Commits. """
""" Commits to the database. """
try:
db.session.commit()
except Exception as e:
......
......@@ -61,8 +61,9 @@ class User(db.Model):
_password = db.Column(db.LargeBinary(60), nullable=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)
city_id = db.Column(db.Integer, db.ForeignKey("city.id"), nullable=False)
......
......@@ -17,6 +17,8 @@ class Config:
THUMBNAIL_SIZE = (120, 120)
SECRET_KEY = os.urandom(24)
SQLALCHEMY_ECHO = False
USER_LOGIN_LOCKED_ATTEMPTS = 12
USER_LOGIN_LOCKED_EXPIRES = timedelta(hours=3)
class DevelopmentConfig(Config):
......@@ -34,6 +36,8 @@ class DevelopmentConfig(Config):
class TestingConfig(Config):
TESTING = True
SQLALCHEMY_DATABASE_URI = "sqlite:///test.db"
USER_LOGIN_LOCKED_ATTEMPTS = 4
USER_LOGIN_LOCKED_EXPIRES = timedelta(seconds=4)
class ProductionConfig(Config):
......