diff --git a/server/app/__init__.py b/server/app/__init__.py index 2abece1db200d6b1cdb269d1fa5809bb90faca07..677eb3d8dc99158a976708232801107a340e2dfb 100644 --- a/server/app/__init__.py +++ b/server/app/__init__.py @@ -2,8 +2,8 @@ from flask import Flask from flask_bcrypt import Bcrypt from flask_jwt_extended.jwt_manager import JWTManager from flask_sqlalchemy import SQLAlchemy -from app.database import Base +from app.database import Base bcrypt = Bcrypt() jwt = JWTManager() diff --git a/server/app/api/users.py b/server/app/api/users.py index 0d92ee52ca97406b99b98939bdc5bea54411c787..5678e92cbc1b3d3d27a1a45150af3b5e580e3a18 100644 --- a/server/app/api/users.py +++ b/server/app/api/users.py @@ -2,6 +2,7 @@ import datetime from app import db from app.api import api_blueprint +from app.database.controller import add_user from app.database.models import Blacklist, User from app.utils.validator import edit_user_schema, login_schema, register_schema, validateObject from flask import request @@ -90,11 +91,12 @@ def create(): if existing_user != None: return {"message": "User already exists"}, 400 - user = User(json_dict["email"], json_dict["password"]) - db.session.add(user) + add_user(json_dict["email"], json_dict["password"], json_dict["role"], json_dict["city"]) db.session.commit() - return user.get_dict(), 200 + item_user = User.query.filter_by(email=json_dict["email"]).first() + + return item_user.get_dict(), 200 @api_blueprint.route("/users/", methods=["PUT"]) diff --git a/server/app/database/controller.py b/server/app/database/controller.py new file mode 100644 index 0000000000000000000000000000000000000000..385e9f05c28ca97031643c5184d9a809a8e1eea5 --- /dev/null +++ b/server/app/database/controller.py @@ -0,0 +1,9 @@ +from app import db +from app.database.models import City, Role, User + + +def add_user(email, plaintext_password, role, city): + item_role = Role.query.filter_by(name=role).first() + item_city = City.query.filter_by(name=city).first() + user = User(email, plaintext_password, item_role.id, item_city.id) + db.session.add(user) diff --git a/server/app/database/models.py b/server/app/database/models.py index f7a3b30b5238900e356a458ebd369a593e20745b..7cf7e5dc933948d98123f12d2a33e05193de0499 100644 --- a/server/app/database/models.py +++ b/server/app/database/models.py @@ -1,3 +1,5 @@ +from enum import unique + from app import bcrypt, db from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.ext.hybrid import hybrid_method, hybrid_property @@ -7,7 +9,8 @@ STRING_SIZE = 254 class Blacklist(db.Model): id = db.Column(db.Integer, primary_key=True) - jti = db.Column(db.String, unique=True, nullable=False) + jti = db.Column(db.String, unique=True) + expire_date = db.Column(db.Integer, nullable=True) def __init__(self, jti): self.jti = jti @@ -23,6 +26,7 @@ class Role(db.Model): self.name = name +# TODO Region? class City(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(STRING_SIZE), unique=True) @@ -36,26 +40,26 @@ class City(db.Model): class User(db.Model): id = db.Column(db.Integer, primary_key=True) - email = db.Column(db.String(254), unique=True, nullable=False) - name = db.Column(db.String(50), nullable=True) + email = db.Column(db.String(STRING_SIZE), unique=True) + name = db.Column(db.String(STRING_SIZE), nullable=True) _password = db.Column(db.LargeBinary(60), nullable=False) - authenticated = db.Column(db.Boolean, default=False) - twoAuthConfirmed = db.Column(db.Boolean, default=True) # Change to false for Two factor authen - twoAuthCode = db.Column(db.String(100), nullable=True) + # Change to false for Two factor authen + authenticated = db.Column(db.Boolean, default=False) + twoAuthConfirmed = db.Column(db.Boolean, default=True) + twoAuthCode = db.Column(db.String(STRING_SIZE), nullable=True) - role_id = db.Column(db.Integer, db.ForeignKey("role.id"), nullable=True) # Change to false - city_id = db.Column(db.Integer, db.ForeignKey("city.id"), nullable=True) # Change to 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) media = db.relationship("Media", backref="upload_by") - def __init__(self, email, plaintext_password, role_id=None, city_id=None, name=None): + def __init__(self, email, plaintext_password, role_id, city_id): self._password = bcrypt.generate_password_hash(plaintext_password) self.email = email self.role_id = role_id self.city_id = city_id - self.name = name self.authenticated = False def get_dict(self): @@ -77,26 +81,26 @@ class User(db.Model): class Media(db.Model): id = db.Column(db.Integer, primary_key=True) filename = db.Column(db.String(STRING_SIZE), unique=True) - type = db.Column(db.Integer, nullable=False) + type_id = db.Column(db.Integer, db.ForeignKey("media_type.id"), nullable=False) upload_by_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False) styles = db.relationship("Style", backref="bg_image") - def __init__(self, filename, type, upload_by_id): + def __init__(self, filename, type_id, upload_by_id): self.filename = filename - self.type = type + self.type_id = type_id self.upload_by_id = upload_by_id class Style(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(STRING_SIZE), unique=True) - css = db.Column(db.Text, nullable=True) + css = db.Column(db.Text, nullable=False) bg_image_id = db.Column(db.Integer, db.ForeignKey("media.id"), nullable=True) competition = db.relationship("Competition", backref="style") - def __init__(self, name, css=None, bg_image_id=None): + def __init__(self, name, css, bg_image_id=None): self.name = name self.css = css self.bg_image_id = bg_image_id @@ -118,11 +122,12 @@ class Competition(db.Model): class Team(db.Model): + __table_args__ = (db.UniqueConstraint("competition_id", "name"),) id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(STRING_SIZE), unique=True) + name = db.Column(db.String(STRING_SIZE), nullable=False) competition_id = db.Column(db.Integer, db.ForeignKey("competition.id"), nullable=False) - answered_questions = db.relationship("AnsweredQuestion", backref="team") + question_answers = db.relationship("QuestionAnswer", backref="team") def __init__(self, name, competition_id): self.name = name @@ -130,58 +135,107 @@ class Team(db.Model): class Slide(db.Model): + __table_args__ = (db.UniqueConstraint("order", "competition_id"),) id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(STRING_SIZE), unique=True) order = db.Column(db.Integer, nullable=False) - tweak_settings = db.Column(db.Text, nullable=True) + title = db.Column(db.String(STRING_SIZE), nullable=False, default="") + body = db.Column(db.Text, nullable=False, default="") + timer = db.Column(db.Integer, nullable=False, default=0) + tweak_settings = db.Column(db.Text, nullable=False, default="") competition_id = db.Column(db.Integer, db.ForeignKey("competition.id"), nullable=False) questions = db.relationship("Question", backref="slide") - def __init__(self, name, order, competition_id, tweak_settings=None): - self.name = name + def __init__(self, order, competition_id): self.order = order self.competition_id = competition_id - self.tweak_settings = tweak_settings class Question(db.Model): + __table_args__ = (db.UniqueConstraint("slide_id", "name"), db.UniqueConstraint("slide_id", "order")) id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(STRING_SIZE), unique=True) - title = db.Column(db.String(STRING_SIZE), nullable=False) - timer = db.Column(db.Integer, nullable=False) + name = db.Column(db.String(STRING_SIZE), nullable=False) + order = db.Column(db.Integer, nullable=False) + type_id = db.Column(db.Integer, db.ForeignKey("question_type.id"), nullable=False) slide_id = db.Column(db.Integer, db.ForeignKey("slide.id"), nullable=False) + question_answers = db.relationship("QuestionAnswer", backref="question") + alternatives = db.relationship("QuestionAlternative", backref="question") - answered_questions = db.relationship("AnsweredQuestion", backref="question") - - def __init__(self, name, title, timer, slide_id): + def __init__(self, name, order, type_id, slide_id): self.name = name - self.title = title - self.timer = timer + self.order = order + self.type_id = type_id self.slide_id = slide_id -class TrueFalseQuestion(db.Model): +class QuestionAlternative(db.Model): + __table_args__ = (db.UniqueConstraint("question_id", "order"),) id = db.Column(db.Integer, primary_key=True) - true_false = db.Column(db.Boolean, nullable=False, default=False) + text = db.Column(db.String(STRING_SIZE), nullable=False) + value = db.Column(db.Boolean, nullable=False) + order = db.Column(db.Integer, nullable=False) question_id = db.Column(db.Integer, db.ForeignKey("question.id"), nullable=False) - question = db.relationship("Question", foreign_keys=[question_id], uselist=False) - - def __init__(self, true_false, question_id): - self.true_false = true_false + def __init__(self, text, value, order, question_id): + self.text = text + self.value = value + self.order = order self.question_id = question_id -class TextQuestion(db.Model): +# TODO QuestionAnswer +class QuestionAnswer(db.Model): + __table_args__ = (db.UniqueConstraint("question_id", "team_id"),) id = db.Column(db.Integer, primary_key=True) + data = db.Column(db.Text, nullable=False) + score = db.Column(db.Integer, nullable=False, default=0) # 0: False, 1: True question_id = db.Column(db.Integer, db.ForeignKey("question.id"), nullable=False) + team_id = db.Column(db.Integer, db.ForeignKey("team.id"), nullable=False) - question = db.relationship("Question", foreign_keys=[question_id], uselist=False) - alternatives = db.relationship("TextQuestionAlternative", backref="text_question") - - def __init__(self, question_id): + def __init__(self, data, score, question_id, team_id): + self.data = data + self.score = score self.question_id = question_id + self.team_id = team_id + + +class MediaType(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(STRING_SIZE), unique=True) + media = db.relationship("Media", backref="type") + + def __init__(self, name): + self.name = name + + +class QuestionType(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(STRING_SIZE), unique=True) + questions = db.relationship("Question", backref="type") + + def __init__(self, name): + self.name = name + + +""" +QuestionHandler = db.Table( + "question_handler", + db.Column("question_id", db.Integer, db.ForeignKey("question.id"), primary_key=True), + db.Column("sub_question_id", db.Integer, unique=True), + db.Column("question_type", db.Integer, nullable=False), +) + +class TrueFalseQuestion(db.Model): + id = db.Column(db.Integer, primary_key=True) + true_false = db.Column(db.Boolean, nullable=False, default=False) + + def __init__(self, true_false): + self.true_false = true_false + + +class TextQuestion(db.Model): + id = db.Column(db.Integer, primary_key=True) + alternatives = db.relationship("TextQuestionAlternative", backref="text_question") class TextQuestionAlternative(db.Model): @@ -198,15 +252,11 @@ class MCQuestion(db.Model): id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(STRING_SIZE), nullable=False) timer = db.Column(db.Integer, nullable=False) - question_id = db.Column(db.Integer, db.ForeignKey("question.id"), nullable=False) - - question = db.relationship("Question", foreign_keys=[question_id], uselist=False) alternatives = db.relationship("MCQuestionAlternative", backref="mc_question") - def __init__(self, title, timer, slide_id): + def __init__(self, title, timer): self.title = title self.timer = timer - self.slide_id = slide_id class MCQuestionAlternative(db.Model): @@ -221,15 +271,5 @@ class MCQuestionAlternative(db.Model): self.mc_id = mc_id -class AnsweredQuestion(db.Model): - id = db.Column(db.Integer, primary_key=True) - data = db.Column(db.Text, nullable=False) - score = db.Column(db.Integer, nullable=False) - question_id = db.Column(db.Integer, db.ForeignKey("question.id"), nullable=False) - team_id = db.Column(db.Integer, db.ForeignKey("team.id"), nullable=False) - def __init__(self, data, score, question_id, team_id): - self.data = data - self.score = score - self.question_id = question_id - self.team_id = team_id +""" diff --git a/server/app/database/populate.py b/server/app/database/populate.py new file mode 100644 index 0000000000000000000000000000000000000000..47891acff7d7a49eeaf3df56ac50304d1796ad98 --- /dev/null +++ b/server/app/database/populate.py @@ -0,0 +1,34 @@ +from app import db +from app.database.controller import add_user +from app.database.models import City, MediaType, QuestionType, Role + +media_types = ["Image", "Video"] +question_types = ["Boolean", "Multiple", "Text"] +roles = ["Admin", "Editor"] +cities = ["Linköping"] + + +def add_default_values(): + + # Add media types + for type in media_types: + db.session.add(MediaType(type)) + + # Add question types + for type in question_types: + db.session.add(QuestionType(type)) + + # Add roles + for role in roles: + db.session.add(Role(role)) + + # Add cities + for city in cities: + db.session.add(City(city)) + + # Commit changes to db + db.session.commit() + + # Add user with role and city + add_user("test@test.se", "password", "Admin", "Linköping") + db.session.commit() diff --git a/server/app/utils/populate.py b/server/app/utils/populate.py deleted file mode 100644 index 713bd9277c3a192b28409e92d966940dee1ee8ba..0000000000000000000000000000000000000000 --- a/server/app/utils/populate.py +++ /dev/null @@ -1,17 +0,0 @@ -from requests import * -import json - -HOST = "http://localhost:5000/" - - -def _post(url: str, jdict: dict): - post(HOST + url, json.dumps(jdict)) - - -def asdasd(i: int): - return i - - -print("Populate default database data") - -_post("user/", {"email": "admin@test.com", "password": "password", "name": "Admin Adminsson"}) diff --git a/server/app/utils/test_helpers.py b/server/app/utils/test_helpers.py new file mode 100644 index 0000000000000000000000000000000000000000..e6d34cce5688189b98f0d5e31c5e9b06e97507fb --- /dev/null +++ b/server/app/utils/test_helpers.py @@ -0,0 +1,22 @@ +from app import db + + +# Try insert invalid row. If it fails then the test is passed +def assert_insert_fail(type, *args): + try: + db.session.add(type(*args)) + db.session.commit() + assert False + except: + db.session.rollback() + + +def assert_exists(type, length, **kwargs): + items = type.query.filter_by(**kwargs).all() + assert len(items) == length + return items[0] + + +def assert_object_values(object, dict): + for k, v in dict.items(): + assert getattr(object, k) == v diff --git a/server/app/utils/validator.py b/server/app/utils/validator.py index f84e7d31a29ed78d162ad5c395c411e3e0cf4ecf..497d3b413b32131d3ded9f1cd3845b6c630fe163 100644 --- a/server/app/utils/validator.py +++ b/server/app/utils/validator.py @@ -17,6 +17,8 @@ login_schema = { register_schema = { "email": {"type": "string", "required": True, "regex": _email_regex}, "password": {"type": "string", "required": True, "minlength": 6, "maxlength": 128}, + "role": {"type": "string", "required": True}, + "city": {"type": "string", "required": True}, } edit_user_schema = { diff --git a/server/tests/__init__.py b/server/tests/__init__.py index 20860256b8216ddac13ed51667ee0e3c0cccf051..0b0deaf0c57b8faae6fa76a21b35cb394c049afa 100644 --- a/server/tests/__init__.py +++ b/server/tests/__init__.py @@ -6,9 +6,15 @@ from app import create_app, db def app(): app = create_app("configmodule.TestingConfig") + """ with app.app_context(): db.drop_all() db.create_all() + yield app + """ + app.app_context().push() + db.drop_all() + db.create_all() return app diff --git a/server/tests/test_app.py b/server/tests/test_app.py index 2ee2664724c4a273d5c3f26a4539ebecb1cfcb70..88a5cfbfd24f24a82de51617a366f9a3fac2c936 100644 --- a/server/tests/test_app.py +++ b/server/tests/test_app.py @@ -1,31 +1,36 @@ -from tests import app, client import json +from app.database.populate import add_default_values + +from tests import app, client + def test_app(client): + add_default_values() + register_data = {"email": "test1@test.se", "password": "abc123", "role": "Admin", "city": "Linköping"} # Create user rv = client.post( "/api/users/", - data=json.dumps({"email": "test@test.se", "password": "abc123"}), + data=json.dumps(register_data), ) rv_dict = json.loads(rv.data.decode()) assert rv.status_code == 200 - assert rv_dict["id"] == 1 + assert rv_dict["id"] == 2 assert "password" not in rv_dict - assert rv_dict["email"] == "test@test.se" + assert rv_dict["email"] == "test1@test.se" # Try loggin with wrong PASSWORD - rv = client.post("/api/users/login", data=json.dumps({"email": "test@test.se", "password": "abc1234"})) + rv = client.post("/api/users/login", data=json.dumps({"email": "test1@test.se", "password": "abc1234"})) assert rv.status_code == 401 # Try loggin with wrong Email - rv = client.post("/api/users/login", data=json.dumps({"email": "test1@test.se", "password": "abc1234"})) + rv = client.post("/api/users/login", data=json.dumps({"email": "testx@test.se", "password": "abc1234"})) assert rv.status_code == 401 # Try loggin with right PASSWORD - rv = client.post("/api/users/login", data=json.dumps({"email": "test@test.se", "password": "abc123"})) + rv = client.post("/api/users/login", data=json.dumps({"email": "test1@test.se", "password": "abc123"})) rv_dict = json.loads(rv.data.decode()) assert rv.status_code == 200 headers = {"Authorization": "Bearer " + rv_dict["access_token"]} @@ -34,7 +39,7 @@ def test_app(client): rv = client.get("/api/users/", headers=headers) rv_dict = json.loads(rv.data.decode()) assert rv.status_code == 200 - assert rv_dict["email"] == "test@test.se" + assert rv_dict["email"] == "test1@test.se" rv = client.put("/api/users/", data=json.dumps({"name": "carl carlsson"}), headers=headers) rv_dict = json.loads(rv.data.decode()) diff --git a/server/tests/test_db.py b/server/tests/test_db.py new file mode 100644 index 0000000000000000000000000000000000000000..e561f97026d87f1a7bcfea6e8b51f6007e4a631f --- /dev/null +++ b/server/tests/test_db.py @@ -0,0 +1,152 @@ +import pytest +from app.database.controller import add_user +from app.database.models import ( + City, + Competition, + Media, + MediaType, + Question, + QuestionAnswer, + QuestionType, + Role, + Slide, + Style, + Team, + User, +) +from app.database.populate import add_default_values +from app.utils.test_helpers import * + +from tests import app, client, db + +# server/env/Scripts/pytest.exe --cov app server/tests/ + + +def test_user(client): + add_default_values() + item_user = User.query.filter_by(email="test@test.se").first() + + # Assert user + assert item_user != None + assert item_user.city.name == "Linköping" + assert item_user.role.name == "Admin" + + item_role = Role.query.filter_by(name="Admin").first() + item_city = City.query.filter_by(name="Linköping").first() + + # Assert user with role and city + assert len(item_role.users) == 1 and item_role.users[0].id == item_user.id + assert len(item_city.users) == 1 and item_city.users[0].id == item_user.id + + +def test_media_style(client): + add_default_values() + item_user = User.query.filter_by(email="test@test.se").first() + + # Get image type + image_type = MediaType.query.filter_by(name="Image").first() + + # Add image + db.session.add(Media("bild.png", image_type.id, item_user.id)) + db.session.commit() + + # Assert image + item_media = Media.query.filter_by(filename="bild.png").first() + assert item_media != None + assert len(item_user.media) == 1 + assert item_media.upload_by.email == "test@test.se" + + # Add style + db.session.add(Style("template", "hej", item_media.id)) + db.session.commit() + + # Assert style + item_style = Style.query.filter_by(name="template").first() + assert item_style != None + assert len(item_media.styles) == 1 + assert item_style.bg_image.filename == "bild.png" + + # Assert lazy loading + assert item_user.media[0].styles[0].name == "template" + + +def test_question(client): + add_default_values() + item_user = User.query.filter_by(email="test@test.se").first() + + # Get image type + image_type = MediaType.query.filter_by(name="Image").first() + + # Add image + db.session.add(Media("bild.png", image_type.id, item_user.id)) + db.session.commit() + item_media = Media.query.filter_by(filename="bild.png").first() + + # Add style + db.session.add(Style("template", "hej", item_media.id)) + db.session.commit() + item_style = Style.query.filter_by(name="template").first() + + # Add competition + item_city = City.query.filter_by(name="Linköping").first() + db.session.add(Competition("teknik8", item_style.id, item_city.id)) + db.session.add(Competition("teknik9", item_style.id, item_city.id)) + db.session.commit() + item_competition = Competition.query.filter_by(name="teknik8").first() + item_competition_2 = Competition.query.filter_by(name="teknik9").first() + + assert item_competition != None + assert item_competition.id == 1 + assert item_competition.style.name == "template" + assert item_competition.city.name == "Linköping" + + # Add teams + db.session.add(Team("Lag1", item_competition.id)) + db.session.add(Team("Lag2", item_competition.id)) + db.session.commit() + + assert_insert_fail(Team, "Lag1", item_competition.id) + + db.session.add(Team("Lag1", item_competition_2.id)) + db.session.commit() + + assert Team.query.filter((Team.competition_id == item_competition.id) & (Team.name == "Lag1")).count() == 1 + assert Team.query.filter((Team.competition_id == item_competition.id) & (Team.name == "Lag2")).count() == 1 + assert Team.query.filter((Team.competition_id == item_competition_2.id) & (Team.name == "Lag1")).count() == 1 + assert Team.query.filter(Team.name == "Lag1").count() == 2 + assert Team.query.filter(Team.competition_id == item_competition.id).count() == 2 + assert Team.query.count() == 3 + + # Add slides + db.session.add(Slide(1, item_competition.id)) + db.session.add(Slide(2, item_competition.id)) + db.session.add(Slide(3, item_competition.id)) + db.session.commit() + + # Try add slide with same order + assert_insert_fail(Slide, 1, item_competition.id) + assert_exists(Slide, 1, order=1) + + item_slide1 = Slide.query.filter_by(order=1).first() + item_slide2 = Slide.query.filter_by(order=2).first() + item_slide3 = Slide.query.filter_by(order=3).first() + + assert item_slide1 != None + assert item_slide2 != None + assert item_slide3 != None + + # Add questions + question_type_bool = QuestionType.query.filter_by(name="Boolean").first() + question_type_multiple = QuestionType.query.filter_by(name="Multiple").first() + + db.session.add(Question("Fråga1", 0, question_type_bool.id, item_slide2.id)) + db.session.add(Question("Fråga2", 1, question_type_multiple.id, item_slide3.id)) + db.session.commit() + + assert question_type_bool != None + assert question_type_multiple != None + + item_q1 = Question.query.filter_by(name="Fråga1").first() + item_q2 = Question.query.filter_by(name="Fråga2").first() + assert item_q1.type.name == "Boolean" + assert item_q2.type.name == "Multiple"