From 302ba5d73d3f9ef5ad46735e824bb75cd75fb159 Mon Sep 17 00:00:00 2001 From: SIMAILA Djalim Date: Wed, 7 Dec 2022 15:02:23 +0100 Subject: [PATCH 01/42] added the classes place, locales and questions --- data_persistance/classes.py | 39 +++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 data_persistance/classes.py diff --git a/data_persistance/classes.py b/data_persistance/classes.py new file mode 100644 index 0000000..5af0e21 --- /dev/null +++ b/data_persistance/classes.py @@ -0,0 +1,39 @@ +from sqlalchemy import Column, Integer, Text,ForeignKey +from sqlalchemy.ext.declarative import declarative_base, relationship +Base = declarative_base() + +class Locale(Base): + __tablename__ = 'T_LOCALE' + TEXT_ID = Column(Integer, primary_key=True) + LANG = Column(Text) + TEXT = Column(Text) + def __init__(self, TEXT_ID, LANG,TEXT ): + self.PLACE_ID = TEXT_ID + self.LANG = LANG + self.TEXT = TEXT + def __str__(self): + return self.PLACE_ID + " " + self.LANG + " " + self.TEXT + +class Place(Base): + __tablename__ = 'T_PLACE' + PLACE_ID = Column(Integer, primary_key=True) + NAME_LID = Column(Integer, ForeignKey("T_LOCALE.TEXT_ID")) + LOCALE = relationship("Locale") + def __init__(self, PLACE_ID, NAME_LID ): + self.PLACE_ID = PLACE_ID + self.NAME_LID = NAME_LID + def __str__(self): + return self.PLACE_ID + " " + self.NAME_LID + +class Question(Base): + __tablename__ = "T_QUESTION" + QUESTION_ID = Column(Integer, primary_key=True) + QUESTION_TYPE = Column(Integer) + TEXT_LID = Column(Integer, ForeignKey("T_LOCALE.TEXT_ID")) + LOCALE = relationship("Locale") + def __init__(self, QUESTION_ID,QUESTION_TYPE,TEXT_LID): + self.QUESTION_ID = QUESTION_ID + self.QUESTION_TYPE = QUESTION_TYPE + self.TEXT_LID = TEXT_LID + def __str__(self): + return self.QUESTION_ID + " " + self.QUESTION_TYPE + " "+ self.TEXT_LID From c67539eb7370d147a55d15cb914382f2dc9c4815 Mon Sep 17 00:00:00 2001 From: SIMAILA Djalim Date: Tue, 3 Jan 2023 09:20:13 +0100 Subject: [PATCH 02/42] added all tables classes --- data_persistance/classes.py | 71 +++++++++++++++++++++++++++++++++---- 1 file changed, 64 insertions(+), 7 deletions(-) diff --git a/data_persistance/classes.py b/data_persistance/classes.py index 5af0e21..f4362bd 100644 --- a/data_persistance/classes.py +++ b/data_persistance/classes.py @@ -1,39 +1,96 @@ -from sqlalchemy import Column, Integer, Text,ForeignKey +from sqlalchemy import Column, Integer, Text, ForeignKey from sqlalchemy.ext.declarative import declarative_base, relationship Base = declarative_base() + class Locale(Base): __tablename__ = 'T_LOCALE' TEXT_ID = Column(Integer, primary_key=True) LANG = Column(Text) TEXT = Column(Text) - def __init__(self, TEXT_ID, LANG,TEXT ): + + def __init__(self, TEXT_ID, LANG, TEXT): self.PLACE_ID = TEXT_ID self.LANG = LANG self.TEXT = TEXT + def __str__(self): return self.PLACE_ID + " " + self.LANG + " " + self.TEXT + class Place(Base): __tablename__ = 'T_PLACE' PLACE_ID = Column(Integer, primary_key=True) NAME_LID = Column(Integer, ForeignKey("T_LOCALE.TEXT_ID")) LOCALE = relationship("Locale") - def __init__(self, PLACE_ID, NAME_LID ): + + def __init__(self, PLACE_ID, NAME_LID): self.PLACE_ID = PLACE_ID self.NAME_LID = NAME_LID + def __str__(self): return self.PLACE_ID + " " + self.NAME_LID + class Question(Base): __tablename__ = "T_QUESTION" QUESTION_ID = Column(Integer, primary_key=True) QUESTION_TYPE = Column(Integer) - TEXT_LID = Column(Integer, ForeignKey("T_LOCALE.TEXT_ID")) + TEXT_LID = Column(Integer, ForeignKey("T_LOCALE.TEXT_ID")) LOCALE = relationship("Locale") - def __init__(self, QUESTION_ID,QUESTION_TYPE,TEXT_LID): + + def __init__(self, QUESTION_ID, QUESTION_TYPE, TEXT_LID): self.QUESTION_ID = QUESTION_ID - self.QUESTION_TYPE = QUESTION_TYPE + self.QUESTION_TYPE = QUESTION_TYPE self.TEXT_LID = TEXT_LID + def __str__(self): - return self.QUESTION_ID + " " + self.QUESTION_TYPE + " "+ self.TEXT_LID + return self.QUESTION_ID + " " + self.QUESTION_TYPE + " " + self.TEXT_LID + + +class Answer(Base): + __tablename__ = "T_ANSWER" + ANSWER_ID = Column(Integer, primary_key=True) + QA_TYPE = Column(Integer) + NPC_ID = Column(Integer, ForeignKey("T_NPC.NPC_ID")) + TEXT_LID = Column(Integer, ForeignKey("T_LOCALE.TEXT_ID")) + + def __init__(self, ANSWSER_ID, QA_TYPE, NPC_ID, TEXT_LID): + self.ANSWSER_ID = ANSWSER_ID + self.QA_TYPE = QA_TYPE + self.NPC_ID = NPC_ID + self.TEXT_LID = TEXT_LID + + +class Npc(Base): + __tablename__ = "T_NPC" + NPC_ID = Column(Integer, primary_key=True) + NAME_LID = Column(Integer, ForeignKey("T_LOCALE.TEXT_ID")) + + def __init__(self, NPC_ID, NAME_LID): + self.NPC_ID = NPC_ID + self.NAME_LID = NAME_LID + + +class Trait(Base): + __tablename__ = "T_TRAIT" + TRAIT_ID = Column(Integer, primary_key=True) + NAME_LID = Column(Integer, ForeignKey("T_LOCALE.TEXT_ID")) + + def __init__(self, TRAIT_ID, NAME_LID): + self.TRAIT_ID = TRAIT_ID + self.NAME_LID = NAME_LID + + +class Reaction(Base): + __tablename__ = "T_REACTION" + REACTION_ID = Column(Integer, primary_key=True) + DESC_LID = Column(Integer, ForeignKey("T_LOCALE.TEXT_ID")) + NPC_ID = Column(Integer, ForeignKey("T_LOCALE.NPC_ID")) + TRAIT_ID = Column(Integer, ForeignKey("T_LOCALE.TRAIT_ID")) + + def __init__(self, REACTION_ID, DESC_LID, NPC_ID, TRAIT_ID): + self.REACTION_ID = REACTION_ID + self.DESC_LID = DESC_LID + self.NPC_ID = NPC_ID + self.TRAIT_ID = TRAIT_ID From ff7e6539c7a6c6f6a7c03f3cc5e605a2d6d29f44 Mon Sep 17 00:00:00 2001 From: SIMAILA Djalim Date: Tue, 3 Jan 2023 09:32:01 +0100 Subject: [PATCH 03/42] changed imports --- data_persistance/classes.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/data_persistance/classes.py b/data_persistance/classes.py index f4362bd..4cd49a3 100644 --- a/data_persistance/classes.py +++ b/data_persistance/classes.py @@ -1,5 +1,6 @@ from sqlalchemy import Column, Integer, Text, ForeignKey -from sqlalchemy.ext.declarative import declarative_base, relationship +from sqlalchemy.orm import declarative_base, relationship + Base = declarative_base() @@ -86,8 +87,8 @@ class Reaction(Base): __tablename__ = "T_REACTION" REACTION_ID = Column(Integer, primary_key=True) DESC_LID = Column(Integer, ForeignKey("T_LOCALE.TEXT_ID")) - NPC_ID = Column(Integer, ForeignKey("T_LOCALE.NPC_ID")) - TRAIT_ID = Column(Integer, ForeignKey("T_LOCALE.TRAIT_ID")) + NPC_ID = Column(Integer, ForeignKey("T_NPC.NPC_ID")) + TRAIT_ID = Column(Integer, ForeignKey("T_TRAIT.TRAIT_ID")) def __init__(self, REACTION_ID, DESC_LID, NPC_ID, TRAIT_ID): self.REACTION_ID = REACTION_ID From 8d9886fc8df1def8262a8f6e51bc32b77a074dfa Mon Sep 17 00:00:00 2001 From: SIMAILA Djalim Date: Tue, 3 Jan 2023 09:32:43 +0100 Subject: [PATCH 04/42] renamed file --- data_persistance/{classes.py => tables.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename data_persistance/{classes.py => tables.py} (100%) diff --git a/data_persistance/classes.py b/data_persistance/tables.py similarity index 100% rename from data_persistance/classes.py rename to data_persistance/tables.py From 38c3ed3f649d732afe2e0ec5724d5acc6a9795b7 Mon Sep 17 00:00:00 2001 From: SIMAILA Djalim Date: Tue, 3 Jan 2023 13:41:05 +0100 Subject: [PATCH 05/42] added data presets --- data_persistance/answer.py | 5 +++++ data_persistance/data.py | 25 +++++++++++++++++++++++++ data_persistance/locales.py | 5 +++++ data_persistance/npc.py | 5 +++++ data_persistance/places.py | 5 +++++ data_persistance/questions.py | 5 +++++ data_persistance/reactions.py | 5 +++++ data_persistance/traits.py | 5 +++++ 8 files changed, 60 insertions(+) create mode 100644 data_persistance/answer.py create mode 100644 data_persistance/data.py create mode 100644 data_persistance/locales.py create mode 100644 data_persistance/npc.py create mode 100644 data_persistance/places.py create mode 100644 data_persistance/questions.py create mode 100644 data_persistance/reactions.py create mode 100644 data_persistance/traits.py diff --git a/data_persistance/answer.py b/data_persistance/answer.py new file mode 100644 index 0000000..feeea97 --- /dev/null +++ b/data_persistance/answer.py @@ -0,0 +1,5 @@ +from tables import Answer + +ANSWER = [ + +] \ No newline at end of file diff --git a/data_persistance/data.py b/data_persistance/data.py new file mode 100644 index 0000000..4790677 --- /dev/null +++ b/data_persistance/data.py @@ -0,0 +1,25 @@ +from sqlalchemy import create_engine +from sqlalchemy.orm import Session +from tables import * + +from answer import ANSWER +from locales import LOCALES +from npc import NPCS +from places import PLACES +from questions import QUESTIONS +from reactions import REACTIONS +from traits import TRAITS + + +# Create Engine and tables +engine = create_engine("sqlite://", echo=True, future=True) +Base.metadata.create_all(engine) + +with Session(engine) as session: + session.add_all(ANSWER) + session.add_all(LOCALES) + session.add_all(NPCS) + session.add_all(PLACES) + session.add_all(QUESTIONS) + session.add_all(REACTIONS) + session.add_all(TRAITS) diff --git a/data_persistance/locales.py b/data_persistance/locales.py new file mode 100644 index 0000000..1ca2da1 --- /dev/null +++ b/data_persistance/locales.py @@ -0,0 +1,5 @@ +from tables import Locale + +LOCALES = [ + Locale(0,"EN", "Hello World"), +] diff --git a/data_persistance/npc.py b/data_persistance/npc.py new file mode 100644 index 0000000..e2ce341 --- /dev/null +++ b/data_persistance/npc.py @@ -0,0 +1,5 @@ +from tables import Npc + +NPCS = [ + +] \ No newline at end of file diff --git a/data_persistance/places.py b/data_persistance/places.py new file mode 100644 index 0000000..57b77fe --- /dev/null +++ b/data_persistance/places.py @@ -0,0 +1,5 @@ +from tables import Place + +PLACES = [ + +] \ No newline at end of file diff --git a/data_persistance/questions.py b/data_persistance/questions.py new file mode 100644 index 0000000..c6bfebc --- /dev/null +++ b/data_persistance/questions.py @@ -0,0 +1,5 @@ +from tables import Question + +QUESTIONS = [ + +] \ No newline at end of file diff --git a/data_persistance/reactions.py b/data_persistance/reactions.py new file mode 100644 index 0000000..534be94 --- /dev/null +++ b/data_persistance/reactions.py @@ -0,0 +1,5 @@ +from tables import Reaction + +REACTIONS = [ + +] \ No newline at end of file diff --git a/data_persistance/traits.py b/data_persistance/traits.py new file mode 100644 index 0000000..0eed597 --- /dev/null +++ b/data_persistance/traits.py @@ -0,0 +1,5 @@ +from tables import Trait + +TRAITS = [ + +] \ No newline at end of file From 54d719065ba6b6bb7a5e579ad7222bb6674085c0 Mon Sep 17 00:00:00 2001 From: SIMAILA Djalim Date: Tue, 3 Jan 2023 14:39:25 +0100 Subject: [PATCH 06/42] added remote connection --- .gitignore | 2 ++ data_persistance/data.py | 30 +++++++++++++++++++++++++----- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 1a7fde3..9b9b034 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ **/__pycache__ instance/ +data_persistance/secret.py +**/.vscode \ No newline at end of file diff --git a/data_persistance/data.py b/data_persistance/data.py index 4790677..cc80318 100644 --- a/data_persistance/data.py +++ b/data_persistance/data.py @@ -1,5 +1,7 @@ from sqlalchemy import create_engine from sqlalchemy.orm import Session +from sqlalchemy import engine as eg + from tables import * from answer import ANSWER @@ -10,16 +12,34 @@ from questions import QUESTIONS from reactions import REACTIONS from traits import TRAITS +from secret import HOST, USER, PASS + +url_object = eg.URL.create( + "mariadb+pymysql", + username=USER, + password=PASS, + host=HOST, + port=6776, + database="truthInquiry", +) # Create Engine and tables -engine = create_engine("sqlite://", echo=True, future=True) +engine = create_engine(url_object) Base.metadata.create_all(engine) with Session(engine) as session: - session.add_all(ANSWER) + print("adding locales") session.add_all(LOCALES) - session.add_all(NPCS) + print("adding places") session.add_all(PLACES) - session.add_all(QUESTIONS) - session.add_all(REACTIONS) + print("adding NPCS") + session.add_all(NPCS) + print("adding trait") session.add_all(TRAITS) + print("adding questions") + session.add_all(QUESTIONS) + print("adding answers") + session.add_all(ANSWER) + print("adding reaction") + session.add_all(REACTIONS) + session.commit() From 829d092e55ee9ad0b3d7251d09b340cabd6f2c75 Mon Sep 17 00:00:00 2001 From: Thomas Rubini <74205383+ThomasRubini@users.noreply.github.com> Date: Fri, 2 Dec 2022 09:47:38 +0100 Subject: [PATCH 07/42] create instance directory --- truthseeker/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/truthseeker/__init__.py b/truthseeker/__init__.py index 81ea82f..0500b4e 100644 --- a/truthseeker/__init__.py +++ b/truthseeker/__init__.py @@ -15,6 +15,7 @@ def set_secret(app): else: import secrets app.config["SECRET_KEY"] = secrets.token_hex() + os.makedirs("instance", exist_ok=True) f = open("instance/secret.txt", "w") f.write(app.config["SECRET_KEY"]) f.close() From 42f655d7fd2f2c567f3ae714d4944046e4320867 Mon Sep 17 00:00:00 2001 From: SIMAILA Djalim Date: Wed, 7 Dec 2022 12:09:03 +0100 Subject: [PATCH 08/42] refractor: changed test api --- tests/test_api.py | 70 ++++++++++++++++++----------------------------- 1 file changed, 27 insertions(+), 43 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index bad0a9a..97f25d1 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,25 +1,8 @@ -import requests import json import pytest +from truthseeker import app -############################################################################### -# # -# # -# Constantes # -# # -# # -############################################################################### - -# "scheme" : le protocol a utiliser pour les requette -scheme = "http://" - -port = "5000" -# "baseUrl" : url racine du serveur web -baseUrl = "localhost" - -# -url= scheme+baseUrl+":"+port - +test_app = app.test_client() ############################################################################### # # @@ -32,16 +15,16 @@ url= scheme+baseUrl+":"+port class User: def __init__(self,username): self.username = username - self.jwt ="" + self.jwt = "" self.isAdmin = False def createGame(user:User): data = {"username":user.username} - response = requests.post(url+"/api/v1/createGame",data=data) - if response.status_code != 200: + responseObject = test_app.post("/api/v1/createGame",data=data) + if responseObject.status_code != 200: print("status code is not 200") raise Exception("status code is not 200") - content = json.loads(response.content.decode("utf-8")) + content = responseObject.json if content is None: print("content is none") raise Exception("Response is null") @@ -55,10 +38,11 @@ def createGame(user:User): def joinGame(user:User,game_id:str): data = {"username":user.username,"game_id":game_id} - response = requests.post(url+"/api/v1/joinGame",data=data) - if response.status_code != 200: + responseObject = test_app.post("/api/v1/joinGame",data=data) + if responseObject.status_code != 200: + print("status code is not 200") raise Exception("status code is not 200") - content = json.loads(response.content.decode("utf-8")) + content = responseObject.json if content is None: raise Exception("Response is null") if content["status"] != "ok": @@ -69,10 +53,11 @@ def joinGame(user:User,game_id:str): def startGame(user:User): data = {"jwt":user.jwt} - response = requests.post(url+"/api/v1/startGame",data=data) - if response.status_code != 200: + responseObject = test_app.post("/api/v1/startGame",data=data) + if responseObject.status_code != 200: + print("status code is not 200") raise Exception("status code is not 200") - content = json.loads(response.content.decode("utf-8")) + content = responseObject.json if content is None: raise Exception("Response is null") if content["status"] != "ok": @@ -113,10 +98,10 @@ def test_that_two_person_having_the_same_pseudo_creating_two_games_results_in_tw def test_that_not_sending_a_username_results_in_an_error(): - response = requests.post(url+"/api/v1/createGame") - assert response.status_code == 200 - content = json.loads(response.content.decode("utf-8")) - #assert content["status"] != "ok" + responseObject = test_app.post("/api/v1/createGame") + assert responseObject.status_code == 200 + assert responseObject.json["status"] != "ok" + def test_that_sending_a_empty_username_results_in_an_error(): user = User("") @@ -162,22 +147,22 @@ def test_that_people_cant_join_if_the_username_is_already_used(): def test_that_people_joining_without_sending_any_data_results_in_an_error(): game_id = createGame(User("neoxyde")) - response = requests.post(url+"/api/v1/joinGame") - assert response.status_code == 200 - assert json.loads(response.content.decode("utf-8"))["status"] != "ok" + responseObject = test_app.post("/api/v1/joinGame") + assert responseObject.status_code == 200 + assert responseObject.json["status"] != "ok" def test_that_people_joining_without_sending_a_game_id_results_in_an_error(): data={"username":"neomblic"} - response = requests.post(url+"/api/v1/joinGame",data=data) - assert response.status_code == 200 - assert json.loads(response.content.decode("utf-8"))["status"] != "ok" + responseObject = test_app.post("/api/v1/joinGame",data=data) + assert responseObject.status_code == 200 + assert responseObject.json["status"] != "ok" def test_that_people_joining_without_sending_an_username_still_results_in_an_error(): game_id = createGame(User("neonyx")) data={"game_id":game_id} - response = requests.post(url+"/api/v1/joinGame",data=data) - assert response.status_code == 200 - assert json.loads(response.content.decode("utf-8"))["status"] != "ok" + responseObject = test_app.post("/api/v1/joinGame",data=data) + assert responseObject.status_code == 200 + assert responseObject.json["status"] != "ok" def test_that_people_joining_with_an_empty_username_still_results_in_an_error(): game_id = createGame(User("neodeur")) @@ -229,4 +214,3 @@ def test_that_non_owners_cant_start_a_game(): joinGame(notOwner,game_id) assert startGame(notOwner) == False assert "Status is not ok" in str(e.value) - From 536b28ff90fdf50908368ec87a84aa394e7c08cb Mon Sep 17 00:00:00 2001 From: Thomas Rubini <74205383+ThomasRubini@users.noreply.github.com> Date: Mon, 12 Dec 2022 14:36:38 +0100 Subject: [PATCH 09/42] updated tests README --- tests/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/README.md b/tests/README.md index adc6b75..568a603 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,4 +1,4 @@ # TESTS UNITAIRES -Pour lancer les test unitaires lancez pytest dans le dossier actuel ou bien manuellement -```pytest test_api.py --verbose``` \ No newline at end of file +Pour lancer les test unitaires lancez pytest dans le dossier racine du dépot : +```python -m pytest --verbose``` \ No newline at end of file From 8ecb3a6f3ab1ac58556fa1d663d461a1deb2b073 Mon Sep 17 00:00:00 2001 From: AudricV <74829229+AudricV@users.noreply.github.com> Date: Mon, 2 Jan 2023 12:12:45 +0100 Subject: [PATCH 10/42] [Client] Add transitions for theme switcher button and footer links The following are also included in this commit: - add missing swap property for Titan One font; - move links decoration and visited properties into a specific class, link Co-authored-by: Cazals Mathias --- truthseeker/static/css/game_ui.css | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/truthseeker/static/css/game_ui.css b/truthseeker/static/css/game_ui.css index d8ad5fb..95013fa 100644 --- a/truthseeker/static/css/game_ui.css +++ b/truthseeker/static/css/game_ui.css @@ -1,6 +1,6 @@ /* Custom fonts */ @font-face { - font-family: 'Spicy Rice'; + font-family: "Spicy Rice"; font-style: normal; font-weight: 400; font-display: swap; @@ -9,9 +9,10 @@ } @font-face { - font-family: 'Titan One'; + font-family: "Titan One"; font-style: normal; font-weight: 400; + font-display: swap; src: url("../fonts/titan_one_v13/titan_one_v13.woff2") format('woff2'), url("../fonts/titan_one_v13/titan_one_v13.woff") format('woff'), url("../fonts/titan_one_v13/titan_one_v13.ttf") format('truetype'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } @@ -46,6 +47,15 @@ body { display: none; } +/* Links */ +.link { + text-decoration: none; +} + +.link:visited { + color: unset; +} + /* Footer */ .footer_link { color: white; @@ -53,11 +63,11 @@ body { font-size: 1em; font-weight: bold; margin: 0.5em; - text-decoration: none; + transition: color 0.25s; } -.footer_link:visited { - color: unset; +.footer_link:hover { + color: #939393; } .footer_links { @@ -121,4 +131,12 @@ body { height: 100vh; width: 100vw; z-index: 1; -} \ No newline at end of file +} + +.theme_switcher_btn { + transition: fill 0.5s; +} + +.theme_switcher_btn:hover { + fill: #939393; +} From dac91b422f325cc66e21df092886dcc1ed48c7d6 Mon Sep 17 00:00:00 2001 From: AudricV <74829229+AudricV@users.noreply.github.com> Date: Mon, 2 Jan 2023 12:15:42 +0100 Subject: [PATCH 11/42] [Client] Add transition on the action button Also remove game title text effect due to different rendering between browsers. Co-authored-by: Cazals Mathias --- truthseeker/static/css/game_ui_start.css | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/truthseeker/static/css/game_ui_start.css b/truthseeker/static/css/game_ui_start.css index fb6e398..6f46adf 100644 --- a/truthseeker/static/css/game_ui_start.css +++ b/truthseeker/static/css/game_ui_start.css @@ -35,8 +35,6 @@ margin-top: 0.5em; margin-bottom: 0.5em; text-align: center; - -webkit-text-stroke: 1px black; - text-shadow: 1px 0 0 red, 0 1px 0 red, -1px 0 0 red, 0 -1px 0 red; } .top_button { @@ -69,4 +67,11 @@ padding-left: 1em; padding-right: 1em; text-transform: uppercase; -} \ No newline at end of file + overflow: hidden; + transition: box-shadow 0.5s, transform 0.5s; +} + +.action_button:hover { + transform: translate(0.1em, 0.1em); + box-shadow: 10px -10px 25px 0px black, -10px 10px 25px 0px black; +} From 84813790a6321d6250b16d8bf59dda6e1c6b0822 Mon Sep 17 00:00:00 2001 From: AudricV <74829229+AudricV@users.noreply.github.com> Date: Mon, 2 Jan 2023 12:18:44 +0100 Subject: [PATCH 12/42] [Client] Use absolute path for resources in the home page and fix some issues - Fix game name in the game title - Remove fill property from the theme switcher button to allow changing its color when hovering it --- truthseeker/templates/index.html | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/truthseeker/templates/index.html b/truthseeker/templates/index.html index 961add6..6802828 100644 --- a/truthseeker/templates/index.html +++ b/truthseeker/templates/index.html @@ -2,8 +2,8 @@ Truth Inquiry - - + + @@ -13,13 +13,13 @@
-

Thruth Inquiry

+

Truth Inquiry

@@ -40,11 +40,11 @@ - + From d6220c06000dd352fc31aa4002bd53543d954c9f Mon Sep 17 00:00:00 2001 From: AudricV <74829229+AudricV@users.noreply.github.com> Date: Mon, 2 Jan 2023 12:25:35 +0100 Subject: [PATCH 13/42] [Client] Add Roboto Mono font This font will be used on the legal pages and could be used as the font of some text blocks of the game. Version 22 of the font has been added. Roboto Mono is made by Christian Robertson, licensed under the Apache 2.0 License. --- truthseeker/static/css/game_ui.css | 9 +++++++++ .../fonts/roboto_mono_v22/roboto_mono_v22.ttf | Bin 0 -> 78996 bytes .../fonts/roboto_mono_v22/roboto_mono_v22.woff | Bin 0 -> 47300 bytes .../fonts/roboto_mono_v22/roboto_mono_v22.woff2 | Bin 0 -> 12312 bytes 4 files changed, 9 insertions(+) create mode 100644 truthseeker/static/fonts/roboto_mono_v22/roboto_mono_v22.ttf create mode 100644 truthseeker/static/fonts/roboto_mono_v22/roboto_mono_v22.woff create mode 100644 truthseeker/static/fonts/roboto_mono_v22/roboto_mono_v22.woff2 diff --git a/truthseeker/static/css/game_ui.css b/truthseeker/static/css/game_ui.css index 95013fa..fca2a65 100644 --- a/truthseeker/static/css/game_ui.css +++ b/truthseeker/static/css/game_ui.css @@ -17,6 +17,15 @@ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } +@font-face { + font-family: "Roboto Mono"; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url("../fonts/roboto_mono_v22/roboto_mono_v22.woff2") format('woff2'), url("../fonts/roboto_mono_v22/roboto_mono_v22.woff") format('woff'), url("../fonts/roboto_mono_v22/roboto_mono_v22.ttf") format('truetype'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + /* Base elements */ :root { color-scheme: dark; diff --git a/truthseeker/static/fonts/roboto_mono_v22/roboto_mono_v22.ttf b/truthseeker/static/fonts/roboto_mono_v22/roboto_mono_v22.ttf new file mode 100644 index 0000000000000000000000000000000000000000..2ab8f34a5bb5fdd2e87ba931bbafc8792fc92bb5 GIT binary patch literal 78996 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 literal 0 HcmV?d00001 diff --git a/truthseeker/static/fonts/roboto_mono_v22/roboto_mono_v22.woff b/truthseeker/static/fonts/roboto_mono_v22/roboto_mono_v22.woff new file mode 100644 index 0000000000000000000000000000000000000000..068df6180762c52c6144f12cd49065ce15cef0bc GIT binary patch literal 47300 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 Date: Mon, 2 Jan 2023 18:36:35 +0100 Subject: [PATCH 14/42] [Client] Add legal style --- truthseeker/static/css/game_ui_legal.css | 45 ++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 truthseeker/static/css/game_ui_legal.css diff --git a/truthseeker/static/css/game_ui_legal.css b/truthseeker/static/css/game_ui_legal.css new file mode 100644 index 0000000..f991a03 --- /dev/null +++ b/truthseeker/static/css/game_ui_legal.css @@ -0,0 +1,45 @@ +html { + background-color: black; +} + +body { + font-family: "Roboto Mono", "sans-serif"; +} + +.legal_link { + color: #0096FF; +} + +.legal_text_title { + font-family: "Spicy Rice", sans-serif; + font-size: 2.5em; + text-align: center; + margin-block-start: 0; + margin-block-end: 0; + margin-top: 0.5em; + margin-bottom: 0.5em; +} + +.legal_text_last_update { + font-family: "Titan One", serif; + font-size: 1.5em; + margin-block-start: 0; + margin-block-end: 0; + margin-top: 0.5em; + margin-bottom: 0.5em; +} + +.legal_text_description { + font-size: 1.25em; + margin-block-start: 0; + margin-block-end: 0; + margin-top: 0.5em; + margin-bottom: 0.5em; +} + +.legal_text_unordered_list { + font-size: 1em; + /* Disable list item padding */ + list-style-position: inside; + padding-inline-start: 0; +} From c4b6807c8e828959db02b314661e2a17d9ff44ef Mon Sep 17 00:00:00 2001 From: AudricV <74829229+AudricV@users.noreply.github.com> Date: Mon, 2 Jan 2023 19:06:52 +0100 Subject: [PATCH 15/42] [Client] Add legal mentions HTML page The legal mentions themselves will have to be added by Flask using the html_legal_mentions variable. --- truthseeker/templates/legal.html | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 truthseeker/templates/legal.html diff --git a/truthseeker/templates/legal.html b/truthseeker/templates/legal.html new file mode 100644 index 0000000..9e5495e --- /dev/null +++ b/truthseeker/templates/legal.html @@ -0,0 +1,22 @@ + + + + Truth Inquiry - Mentions légales + + + + + + + +

Mentions légales

+ + + + From 63fdac4f39ca7553982dc57f7c19b017ab7d2c43 Mon Sep 17 00:00:00 2001 From: AudricV <74829229+AudricV@users.noreply.github.com> Date: Mon, 2 Jan 2023 19:12:28 +0100 Subject: [PATCH 16/42] [Client] Add privacy policy HTML page The privacy policy by itself will have to be added by Flask using the html_privacy_policy variable and its last updated date using the privacy_policy_last_updated_date variable. --- truthseeker/templates/privacy.html | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 truthseeker/templates/privacy.html diff --git a/truthseeker/templates/privacy.html b/truthseeker/templates/privacy.html new file mode 100644 index 0000000..d9bec53 --- /dev/null +++ b/truthseeker/templates/privacy.html @@ -0,0 +1,24 @@ + + + + Truth Inquiry - Politique de confidentialité + + + + + + + +

Politique de confidentialité de Truth Inquiry

+ + + + + + From 78766ca72835b19ef1ba110c1addbfe180dc619f Mon Sep 17 00:00:00 2001 From: AudricV <74829229+AudricV@users.noreply.github.com> Date: Mon, 2 Jan 2023 19:13:00 +0100 Subject: [PATCH 17/42] [Client] Add licenses HTML page --- truthseeker/templates/licenses.html | 42 +++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 truthseeker/templates/licenses.html diff --git a/truthseeker/templates/licenses.html b/truthseeker/templates/licenses.html new file mode 100644 index 0000000..d73f61f --- /dev/null +++ b/truthseeker/templates/licenses.html @@ -0,0 +1,42 @@ + + + + Truth Inquiry - Licenses + + + + + + + +

Licenses et crédits de Truth Inquiry

+ + + + + From 6d960222cccb7613791310c790c751d8a3f71649 Mon Sep 17 00:00:00 2001 From: AudricV <74829229+AudricV@users.noreply.github.com> Date: Mon, 2 Jan 2023 19:14:09 +0100 Subject: [PATCH 18/42] [Server] Add legal pages to existing routes --- truthseeker/routes/routes_ui.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/truthseeker/routes/routes_ui.py b/truthseeker/routes/routes_ui.py index 70ce902..9342c17 100644 --- a/truthseeker/routes/routes_ui.py +++ b/truthseeker/routes/routes_ui.py @@ -9,6 +9,18 @@ routes_ui = flask.Blueprint("ui", __name__) def index(): return flask.render_template("index.html") +@routes_ui.route("/privacy") +def privacy(): + return flask.render_template("privacy.html") + +@routes_ui.route("/licenses") +def licenses(): + return flask.render_template("licenses.html") + +@routes_ui.route("/legal") +def legal(): + return flask.render_template("legal.html") + @routes_ui.route("/lobby/") def lobby(game_id): # rendered by the javascript client-side From 271bf3775864277c8323736ff78885df2972b711 Mon Sep 17 00:00:00 2001 From: Thomas Rubini <74205383+ThomasRubini@users.noreply.github.com> Date: Fri, 2 Dec 2022 09:29:42 +0100 Subject: [PATCH 19/42] add requirements.txt --- requirements.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d0f5f12 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +Flask==2.2.2 +pyjwt==2.6.0 From c554ed13ad1b24f25f0517fcec26f1957a18ffd1 Mon Sep 17 00:00:00 2001 From: Thomas Rubini <74205383+ThomasRubini@users.noreply.github.com> Date: Mon, 12 Dec 2022 14:49:53 +0100 Subject: [PATCH 20/42] add dev-requirements.txt --- dev-requirements.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 dev-requirements.txt diff --git a/dev-requirements.txt b/dev-requirements.txt new file mode 100644 index 0000000..49780e0 --- /dev/null +++ b/dev-requirements.txt @@ -0,0 +1 @@ +pytest==7.2.0 From 6710e72cd1985c4ea7c9c5f71656734b295b3123 Mon Sep 17 00:00:00 2001 From: Thomas Rubini Date: Fri, 2 Dec 2022 09:32:13 +0100 Subject: [PATCH 21/42] Create test_api.yml --- .github/workflows/tests.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..cde5719 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,20 @@ +name: Tests + +on: [push] + +jobs: + api: + name: Test API + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Install dependencies + run: | + pip install -r requirements.txt + pip install -r dev-requirements.txt + + - name: Run tests + run: | + python -m pytest --verbose From 4c5f56edcfae8b5b979dd615e795dd12d8343dd5 Mon Sep 17 00:00:00 2001 From: AudricV <74829229+AudricV@users.noreply.github.com> Date: Tue, 3 Jan 2023 16:04:58 +0100 Subject: [PATCH 22/42] [Client] Add game selection view to start page Some changes in the existing code have been also made to avoid code duplication and fix some issues. Co-authored-by: Cazals Mathias --- truthseeker/static/css/game_ui.css | 2 +- truthseeker/static/css/game_ui_start.css | 117 ++++++++++++++++++++--- truthseeker/static/js/game_start_page.js | 33 +++++++ truthseeker/templates/index.html | 29 +++++- 4 files changed, 168 insertions(+), 13 deletions(-) create mode 100644 truthseeker/static/js/game_start_page.js diff --git a/truthseeker/static/css/game_ui.css b/truthseeker/static/css/game_ui.css index fca2a65..31e0096 100644 --- a/truthseeker/static/css/game_ui.css +++ b/truthseeker/static/css/game_ui.css @@ -53,7 +53,7 @@ body { /* Utility classes */ .hidden { - display: none; + display: none !important; } /* Links */ diff --git a/truthseeker/static/css/game_ui_start.css b/truthseeker/static/css/game_ui_start.css index 6f46adf..8142ec9 100644 --- a/truthseeker/static/css/game_ui_start.css +++ b/truthseeker/static/css/game_ui_start.css @@ -3,6 +3,32 @@ --header-actions-height: 3em; } +button { + background-color: transparent; + border: none; + cursor: pointer; + padding: 0; +} + +input { + background-color: white; + border: none; + border-radius: 1em; + font-family: "Titan One", sans-serif; + font-size: 1em; + padding: 0.5em; + color: black; +} + +input::placeholder { + color: black; + opacity: 0.5; +} + +.back_btn { + fill: #BD1E1E; +} + .game_begin { align-items: center; border-radius: 1.5em; @@ -30,19 +56,12 @@ font-family: "Spicy Rice", serif; font-weight: bold; font-size: 5em; - margin-block-start: 0; - margin-block-end: 0; - margin-top: 0.5em; - margin-bottom: 0.5em; + margin: 0.25em; text-align: center; } -.top_button { - background-color: transparent; - border: none; - fill: white; - cursor: pointer; - padding: 0; +.ingame { + margin: 0; } .header_actions { @@ -59,7 +78,6 @@ border-radius: var(--button-and-dialog-border-radius); cursor: pointer; font-family: "Titan One", sans-serif; - font-size: 3em; margin-left: auto; margin-right: auto; padding-top: 0.5em; @@ -75,3 +93,80 @@ transform: translate(0.1em, 0.1em); box-shadow: 10px -10px 25px 0px black, -10px 10px 25px 0px black; } + +.game_mode_selection { + align-items: center; + background-image: url("../images/start_background.png"); + background-position: center; + background-repeat: no-repeat; + background-size: cover; + display: flex; + flex-direction: column; + flex-wrap: nowrap; + height: 100vh; + justify-content: center; +} + +.game_mode_item { + margin: 0.5em; + display: flex; + flex-direction: column; + align-content: center; + justify-content: center; + align-items: center; +} + +.game_mode_item_title { + font-family: "Titan One", sans-serif; + font-size: 2em; + margin: 0.25em; +} + +.game_mode_item_input_text_single_line { + align-items: center; + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: center; + margin-top: 0.5em; + margin-bottom: 0.5em; +} + +.game_mode_items { + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: center; + align-items: center; +} + +.game_mode_choice_selector { + background-color: black; + border-color: white; + border-style: solid; + border-radius: 1.5em; + padding: 1em; +} + +.theme_switcher { + fill: white; +} + +#back_button { + position: absolute; + left: 0; + top: 0; +} + +#create_room_button, #join_room_button, #start_solo_game_button { + font-size: 1.5em; +} + +#game_username { + margin: 0.5em; + width: calc(100% - 1.5em); +} + +#play_button { + font-size: 3em; +} diff --git a/truthseeker/static/js/game_start_page.js b/truthseeker/static/js/game_start_page.js new file mode 100644 index 0000000..3fc1586 --- /dev/null +++ b/truthseeker/static/js/game_start_page.js @@ -0,0 +1,33 @@ +/** + * Show the game selection view. + * + *

+ * The "hidden" class is added on the footer, the game_start elements and the game_mode_selection + * elements are show. The body margin is also set to 0 by adding the ingame class. + *

+ */ +function showGameModeSelection() { + document.getElementsByTagName("body")[0].classList.add("ingame"); + document.getElementsByTagName("footer")[0].classList.add("hidden"); + document.getElementsByClassName("game_start")[0].classList.add("hidden"); + document.getElementsByClassName("game_mode_selection")[0].classList.remove("hidden"); +} + +/** + * Show the game selection view. + * + *

+ * The "hidden" class is removed on the footer, the game_start elements and the game_mode_selection + * elements are hidden. The body margin is also set to its normal value by removing the ingame + * class. + *

+ */ +function hideGameModeSelection() { + document.getElementsByTagName("body")[0].classList.remove("ingame"); + document.getElementsByTagName("footer")[0].classList.remove("hidden"); + document.getElementsByClassName("game_start")[0].classList.remove("hidden"); + document.getElementsByClassName("game_mode_selection")[0].classList.add("hidden"); +} + +document.getElementById("play_button").addEventListener("click", showGameModeSelection); +document.getElementById("back_button").addEventListener("click", hideGameModeSelection); diff --git a/truthseeker/templates/index.html b/truthseeker/templates/index.html index 6802828..b594fe5 100644 --- a/truthseeker/templates/index.html +++ b/truthseeker/templates/index.html @@ -12,7 +12,7 @@
-
+

Navigateur non supporté

@@ -46,5 +72,6 @@
+ From 74f5c82a5de87af86a7005246c4c07b373c9aacb Mon Sep 17 00:00:00 2001 From: AudricV <74829229+AudricV@users.noreply.github.com> Date: Wed, 4 Jan 2023 10:03:27 +0100 Subject: [PATCH 23/42] [Client] Add initial interactions on the game selection view These interactions are checks of nickname and room code validity, where it is relevant. Also set a maximum room code length to 20 characters, at least for now. --- truthseeker/static/css/game_ui_start.css | 12 +++ truthseeker/static/js/game_start_page.js | 112 +++++++++++++++++++++++ truthseeker/templates/index.html | 3 +- 3 files changed, 126 insertions(+), 1 deletion(-) diff --git a/truthseeker/static/css/game_ui_start.css b/truthseeker/static/css/game_ui_start.css index 8142ec9..a160f98 100644 --- a/truthseeker/static/css/game_ui_start.css +++ b/truthseeker/static/css/game_ui_start.css @@ -148,6 +148,18 @@ input::placeholder { padding: 1em; } +.game_start_failed { + color: #BD1E1E; + font-family: "Roboto Mono", sans-serif; + font-size: 1em; + font-size: 1.5em; + font-weight: bold; + margin-bottom: 0.25em; + margin-left: 0.5em; + margin-top: 0.25em; + margin-right: 0.5em; +} + .theme_switcher { fill: white; } diff --git a/truthseeker/static/js/game_start_page.js b/truthseeker/static/js/game_start_page.js index 3fc1586..4df0062 100644 --- a/truthseeker/static/js/game_start_page.js +++ b/truthseeker/static/js/game_start_page.js @@ -29,5 +29,117 @@ function hideGameModeSelection() { document.getElementsByClassName("game_mode_selection")[0].classList.add("hidden"); } +/** + * Hide an error message on the first game_start_failed CSS element. + * + *

+ * The element will be hidden by removing the hidden CSS class on the element. + *

+ */ +function hideInvalidInputErrorMessage() { + document.getElementsByClassName("game_start_failed")[0].classList.add("hidden"); +} + +/** + * Show an error message on the first game_start_failed CSS element. + * + *

+ * The current error message text will be replaced by the given message and the element will be + * shown, by removing the hidden CSS class on the element. + *

+ * + * @param {boolean} errorMessage the error message to show + */ +function showInvalidInputErrorMessage(errorMessage) { + let gameStartFailedElement = document.getElementsByClassName("game_start_failed")[0]; + gameStartFailedElement.textContent = errorMessage; + gameStartFailedElement.classList.remove("hidden"); +} + +/** + * Determine whether a nickname is invalid. + * + *

+ * A nickname is invalid when it only contains spaces characters or is empty. + *

+ * + * @returns whether a nickname is invalid + */ +function isNickNameInvalid() { + return document.getElementById("game_username").value.trim() == ""; +} + +/** + * Determine whether a room code is invalid. + * + *

+ * A room code is invalid when it only contains spaces characters or is empty. + *

+ * + * @returns whether a room code is invalid + */ +function isRoomCodeInvalid() { + return document.getElementById("game_room_code").value.trim() == ""; +} + +/** + * Determine whether nickname and/or room code inputs are valid. + * + *

+ * The nickname validity is always checked, while the room code validity is checked only when + * checkRoomCode is true (because when creating a room or playing on a solo match, the room code is + * not used so checking it would be useless and would require a valid room code). + *

+ * + * @param {boolean} checkRoomCode whether the room code input should be checked + * @returns whether the checked inputs are valid + */ +function areInputsValid(checkRoomCode) { + if (isNickNameInvalid()) { + showInvalidInputErrorMessage("Le nom saisi n'est pas valide."); + return false; + } + + if (checkRoomCode && isRoomCodeInvalid()) { + showInvalidInputErrorMessage("Le code de salon saisi n'est pas valide."); + return false; + } + + return true; +} + +function startSoloGame() { + if (!areInputsValid(false)) { + return; + } + + hideInvalidInputErrorMessage(); + + //TODO: code to start solo game +} + +function createMultiPlayerRoom() { + if (!areInputsValid(false)) { + return; + } + + hideInvalidInputErrorMessage(); + + //TODO: code to create multi player game +} + +function joinMultiPlayerRoom() { + if (!areInputsValid(true)) { + return; + } + + hideInvalidInputErrorMessage(); + + //TODO: code to join multi player game +} + document.getElementById("play_button").addEventListener("click", showGameModeSelection); document.getElementById("back_button").addEventListener("click", hideGameModeSelection); +document.getElementById("start_solo_game_button").addEventListener("click", startSoloGame); +document.getElementById("create_room_button").addEventListener("click", createMultiPlayerRoom); +document.getElementById("join_room_button").addEventListener("click", joinMultiPlayerRoom); diff --git a/truthseeker/templates/index.html b/truthseeker/templates/index.html index b594fe5..c5cb68b 100644 --- a/truthseeker/templates/index.html +++ b/truthseeker/templates/index.html @@ -31,6 +31,7 @@

Thruth Inquiry

+
@@ -42,7 +43,7 @@

Code :

- +
From 99a7ddf66e19b1a383effc847b05d4267aae3e6f Mon Sep 17 00:00:00 2001 From: Thomas Rubini <74205383+ThomasRubini@users.noreply.github.com> Date: Thu, 5 Jan 2023 14:25:31 +0100 Subject: [PATCH 24/42] use flask sessions instead of jwt --- truthseeker/logic/game_logic.py | 23 ++++----------- truthseeker/routes/routes_api.py | 50 +++++++++++--------------------- 2 files changed, 22 insertions(+), 51 deletions(-) diff --git a/truthseeker/logic/game_logic.py b/truthseeker/logic/game_logic.py index 6c20f12..b4aeaf7 100644 --- a/truthseeker/logic/game_logic.py +++ b/truthseeker/logic/game_logic.py @@ -1,6 +1,5 @@ import string import random -import jwt from datetime import datetime, timedelta import truthseeker @@ -52,28 +51,15 @@ class Game: self.owner = None self.members = [] - def _gen_jwt(self, username, owner): - return jwt.encode( - payload={ - "game_type": "multi", - "game_id": self.game_id, - "username": username, - "owner": owner, - "exp": datetime.utcnow() + timedelta(hours = 1) # handled automatically on jwt.decode - }, - key=truthseeker.app.config["SECRET_KEY"], - algorithm="HS256" - ) - def set_owner(self, username): self.owner = Member(username) self.members.append(self.owner) - return self.owner, self._gen_jwt(username, owner=True) + return self.owner def add_member(self, username): member = Member(username) self.members.append(member) - return member, self._gen_jwt(username, owner=False) + return member def __str__(self) -> str: return "Game[game_id={}, owner={}, members={}]".format(self.game_id, self.owner, self.members) @@ -81,7 +67,7 @@ class Game: def __repr__(self) -> str: return self.__str__() -def create_game(): +def create_game(owner): """ This function creates a new game by creating a Game object and stores it into the games_list dictionnary @@ -90,8 +76,9 @@ def create_game(): : return type : Game """ game = Game() + game.owner = owner + game.members.append(Member(owner)) game.game_id = random_string(6) - game.start_token = random_string(64) games_list[game.game_id] = game #TODO ADD A WEBSOCKET IF THE GAME IS KNOWN TO BE MULTIPLAYER return game diff --git a/truthseeker/routes/routes_api.py b/truthseeker/routes/routes_api.py index 6b6742f..e74b833 100644 --- a/truthseeker/routes/routes_api.py +++ b/truthseeker/routes/routes_api.py @@ -1,34 +1,11 @@ import flask -import jwt import truthseeker from truthseeker.logic import game_logic -from functools import wraps routes_api = flask.Blueprint("api", __name__) -# Auth decorator -def jwt_required(f): - @wraps(f) - def decorator(*args, **kwargs): - jwt_str = flask.request.values.get("jwt") - if not jwt_str: - return {"status": "Error, JWT token missing"} - - try: - claims = jwt.decode(jwt_str, truthseeker.app.config['SECRET_KEY'], algorithms=['HS256']) - except jwt.exceptions.InvalidTokenError as e: - print("Caught exception while decoding JWT token :", e) - return {"status": "Error, invalid JWT"} - - return f(claims, *args, **kwargs) - return decorator - - - - - @routes_api.route("/createGame", methods=["GET", "POST"]) def create_game(): username = flask.request.values.get("username") @@ -38,10 +15,13 @@ def create_game(): response = {} response["status"] = "ok" - game = game_logic.create_game() + game = game_logic.create_game(owner=username) response["game_id"] = game.game_id - owner, owner_jwt = game.set_owner(username=username) - response["jwt"] = owner_jwt + + flask.session["game_id"] = game.game_id + flask.session["is_owner"] = True + flask.session["username"] = username + return response @routes_api.route("/joinGame", methods=["GET", "POST"]) @@ -55,11 +35,15 @@ def join_game(): if game == None: return {"status": "error, game does not exist"} - member, member_jwt = game.add_member(username) + + game.add_member(username) + + flask.session["game_id"] = game.game_id + flask.session["is_owner"] = False + flask.session["username"] = username response = {} response["status"] = "ok" - response["jwt"] = member_jwt return response @routes_api.route("/getGameInfo", methods=["GET", "POST"]) @@ -80,12 +64,12 @@ def get_game_info(): # DEPRECATED, SHOULD BE REMOVED return response @routes_api.route("/startGame", methods=["GET", "POST"]) -@jwt_required -def start_game(claims): - if not claims["owner"]: +def start_game(): + if not flask.session: + return {"status": "No session"} + if not flask.session["is_owner"]: return {"status": "Error, you are not the owner of this game"} - - if game_logic.get_game(claims["game_id"]) == None: + if game_logic.get_game(flask.session["game_id"]) == None: return {"status": "Error, this game doesn't exist"} return {"status": "ok"} From 370619814996964d9de299d85a10f32e60b78aa0 Mon Sep 17 00:00:00 2001 From: Thomas Rubini <74205383+ThomasRubini@users.noreply.github.com> Date: Thu, 5 Jan 2023 14:26:09 +0100 Subject: [PATCH 25/42] modify tests to not use jwt --- tests/test_api.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index 97f25d1..52b6546 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -15,7 +15,6 @@ test_app = app.test_client() class User: def __init__(self,username): self.username = username - self.jwt = "" self.isAdmin = False def createGame(user:User): @@ -31,7 +30,6 @@ def createGame(user:User): if content["status"] != "ok": print(content["status"]) raise Exception("Status is not ok") - user.jwt = content["jwt"] user.isAdmin = True return content["game_id"] @@ -48,12 +46,10 @@ def joinGame(user:User,game_id:str): if content["status"] != "ok": print(content["status"]) raise Exception("Status is not ok") - user.jwt = content["jwt"] return True def startGame(user:User): - data = {"jwt":user.jwt} - responseObject = test_app.post("/api/v1/startGame",data=data) + responseObject = test_app.post("/api/v1/startGame") if responseObject.status_code != 200: print("status code is not 200") raise Exception("status code is not 200") @@ -76,7 +72,7 @@ def startGame(user:User): # # Cette requete api crée une salle de jeu multijoueur dans le serveur, elle # octroie ensuite les droit de creation de la salle a l'utilisateur dont le -# pseudo est donné en parametre post et lui retourne son token jwt" +# pseudo est donné en parametre post def test_that_people_can_create_a_game(): user = User("neotaku") @@ -127,8 +123,7 @@ def test_that_username_that_contains_non_alphanumerics_results_in_an_error(): ############################################################################### # # Cette requete ajoute dans la partie identifié par l'identifiant de jeu -# (game_id) l'utilisateur indentifié par son pseudo (username) et lui retourne -# son token jwt +# (game_id) l'utilisateur indentifié par son pseudo (username) def test_that_people_can_join_a_game(): game_id = createGame(User("neoracle")) From 2548cab78eae7e50e9064b97e5313abc5feef514 Mon Sep 17 00:00:00 2001 From: Thomas Rubini <74205383+ThomasRubini@users.noreply.github.com> Date: Thu, 5 Jan 2023 14:38:03 +0100 Subject: [PATCH 26/42] remove getGameInfo endpoint --- truthseeker/routes/routes_api.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/truthseeker/routes/routes_api.py b/truthseeker/routes/routes_api.py index e74b833..ae0a416 100644 --- a/truthseeker/routes/routes_api.py +++ b/truthseeker/routes/routes_api.py @@ -45,23 +45,6 @@ def join_game(): response = {} response["status"] = "ok" return response - -@routes_api.route("/getGameInfo", methods=["GET", "POST"]) -def get_game_info(): # DEPRECATED, SHOULD BE REMOVED - response = {} - game_id = flask.request.values.get("game_id") - if game_id == None: - response["status"] = "No 'game_id' argument" - return response - game = game_logic.get_game_info(game_id) - if game == None: - response["status"] = "Game {} does not exist".format(game_id) - return response - else: - response["status"] = "ok" - response["game_id"] = game_id - response["token"] = game.start_token - return response @routes_api.route("/startGame", methods=["GET", "POST"]) def start_game(): From 1ccabac77ee0cb117547ec36cedc7668df6aa33b Mon Sep 17 00:00:00 2001 From: Thomas Rubini <74205383+ThomasRubini@users.noreply.github.com> Date: Thu, 5 Jan 2023 14:48:05 +0100 Subject: [PATCH 27/42] replace "status" with "error" and "msg" in error codes --- tests/test_api.py | 20 ++++++++++---------- truthseeker/routes/routes_api.py | 20 +++++++++----------- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index 52b6546..6b23d13 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -27,8 +27,8 @@ def createGame(user:User): if content is None: print("content is none") raise Exception("Response is null") - if content["status"] != "ok": - print(content["status"]) + if content["error"] != 0: + print(content["msg"]) raise Exception("Status is not ok") user.isAdmin = True return content["game_id"] @@ -43,8 +43,8 @@ def joinGame(user:User,game_id:str): content = responseObject.json if content is None: raise Exception("Response is null") - if content["status"] != "ok": - print(content["status"]) + if content["error"] != 0: + print(content["msg"]) raise Exception("Status is not ok") return True @@ -56,8 +56,8 @@ def startGame(user:User): content = responseObject.json if content is None: raise Exception("Response is null") - if content["status"] != "ok": - print(content["status"]) + if content["error"] != 0: + print(content["msg"]) raise Exception("Status is not ok") return True @@ -96,7 +96,7 @@ def test_that_two_person_having_the_same_pseudo_creating_two_games_results_in_tw def test_that_not_sending_a_username_results_in_an_error(): responseObject = test_app.post("/api/v1/createGame") assert responseObject.status_code == 200 - assert responseObject.json["status"] != "ok" + assert responseObject.json["error"] != 0 def test_that_sending_a_empty_username_results_in_an_error(): @@ -144,20 +144,20 @@ def test_that_people_joining_without_sending_any_data_results_in_an_error(): game_id = createGame(User("neoxyde")) responseObject = test_app.post("/api/v1/joinGame") assert responseObject.status_code == 200 - assert responseObject.json["status"] != "ok" + assert responseObject.json["error"] != 0 def test_that_people_joining_without_sending_a_game_id_results_in_an_error(): data={"username":"neomblic"} responseObject = test_app.post("/api/v1/joinGame",data=data) assert responseObject.status_code == 200 - assert responseObject.json["status"] != "ok" + assert responseObject.json["error"] != 0 def test_that_people_joining_without_sending_an_username_still_results_in_an_error(): game_id = createGame(User("neonyx")) data={"game_id":game_id} responseObject = test_app.post("/api/v1/joinGame",data=data) assert responseObject.status_code == 200 - assert responseObject.json["status"] != "ok" + assert responseObject.json["error"] != 0 def test_that_people_joining_with_an_empty_username_still_results_in_an_error(): game_id = createGame(User("neodeur")) diff --git a/truthseeker/routes/routes_api.py b/truthseeker/routes/routes_api.py index ae0a416..647b3a1 100644 --- a/truthseeker/routes/routes_api.py +++ b/truthseeker/routes/routes_api.py @@ -10,11 +10,11 @@ routes_api = flask.Blueprint("api", __name__) def create_game(): username = flask.request.values.get("username") if username==None: - return {"status": "error, username not set"} + return {"error": 1, "msg": "username not set"} response = {} - response["status"] = "ok" + response["error"] = 0 game = game_logic.create_game(owner=username) response["game_id"] = game.game_id @@ -29,11 +29,11 @@ def join_game(): game_id = flask.request.values.get("game_id") username = flask.request.values.get("username") if game_id==None or username==None: - return {"status": "error, username or game id not set"} + return {"error": 1, "msg": "username or game id not set"} game = game_logic.get_game(game_id) if game == None: - return {"status": "error, game does not exist"} + return {"error": 1, "msg": "game does not exist"} game.add_member(username) @@ -42,17 +42,15 @@ def join_game(): flask.session["is_owner"] = False flask.session["username"] = username - response = {} - response["status"] = "ok" - return response + return {"error": 0} @routes_api.route("/startGame", methods=["GET", "POST"]) def start_game(): if not flask.session: - return {"status": "No session"} + return {"error": 1, "msg": "No session"} if not flask.session["is_owner"]: - return {"status": "Error, you are not the owner of this game"} + return {"error": 1, "msg": "you are not the owner of this game"} if game_logic.get_game(flask.session["game_id"]) == None: - return {"status": "Error, this game doesn't exist"} + return {"error": 1, "msg": "this game doesn't exist"} - return {"status": "ok"} + return {"error": 0} From 0efba544bcc2d96c46c43cefe3a31153bda71463 Mon Sep 17 00:00:00 2001 From: Thomas Rubini <74205383+ThomasRubini@users.noreply.github.com> Date: Thu, 5 Jan 2023 15:00:36 +0100 Subject: [PATCH 28/42] remove prints in tests --- tests/test_api.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index 6b23d13..b57f196 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -21,15 +21,12 @@ def createGame(user:User): data = {"username":user.username} responseObject = test_app.post("/api/v1/createGame",data=data) if responseObject.status_code != 200: - print("status code is not 200") raise Exception("status code is not 200") content = responseObject.json if content is None: - print("content is none") raise Exception("Response is null") if content["error"] != 0: - print(content["msg"]) - raise Exception("Status is not ok") + raise Exception("backend returned an error: "+content["msg"]) user.isAdmin = True return content["game_id"] @@ -38,27 +35,23 @@ def joinGame(user:User,game_id:str): data = {"username":user.username,"game_id":game_id} responseObject = test_app.post("/api/v1/joinGame",data=data) if responseObject.status_code != 200: - print("status code is not 200") raise Exception("status code is not 200") content = responseObject.json if content is None: raise Exception("Response is null") if content["error"] != 0: - print(content["msg"]) - raise Exception("Status is not ok") + raise Exception("backend returned an error: "+content["msg"]) return True def startGame(user:User): responseObject = test_app.post("/api/v1/startGame") if responseObject.status_code != 200: - print("status code is not 200") raise Exception("status code is not 200") content = responseObject.json if content is None: raise Exception("Response is null") if content["error"] != 0: - print(content["msg"]) - raise Exception("Status is not ok") + raise Exception("backend returned an error: "+content["msg"]) return True From b9f6b3245eb5c9083c3ad796b00804e580484ef3 Mon Sep 17 00:00:00 2001 From: Thomas Rubini <74205383+ThomasRubini@users.noreply.github.com> Date: Thu, 5 Jan 2023 15:03:04 +0100 Subject: [PATCH 29/42] check if username is valid --- truthseeker/routes/routes_api.py | 6 +++++- truthseeker/utils.py | 9 +++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 truthseeker/utils.py diff --git a/truthseeker/routes/routes_api.py b/truthseeker/routes/routes_api.py index 647b3a1..def7d41 100644 --- a/truthseeker/routes/routes_api.py +++ b/truthseeker/routes/routes_api.py @@ -2,6 +2,7 @@ import flask import truthseeker from truthseeker.logic import game_logic +from truthseeker.utils import check_username routes_api = flask.Blueprint("api", __name__) @@ -11,7 +12,8 @@ def create_game(): username = flask.request.values.get("username") if username==None: return {"error": 1, "msg": "username not set"} - + if not check_username(username): + return {"error": 1, "msg": "invalid username"} response = {} response["error"] = 0 @@ -30,6 +32,8 @@ def join_game(): username = flask.request.values.get("username") if game_id==None or username==None: return {"error": 1, "msg": "username or game id not set"} + if not check_username(username): + return {"error": 1, "msg": "invalid username"} game = game_logic.get_game(game_id) if game == None: diff --git a/truthseeker/utils.py b/truthseeker/utils.py new file mode 100644 index 0000000..7cce76e --- /dev/null +++ b/truthseeker/utils.py @@ -0,0 +1,9 @@ +def check_username(username): + if not username: + return False + if not username == username.strip(): + return False + if not len(username) < 16: + return False + + return True From 94ffdb10a148a8c9462633a70185dc2ee37dd7a3 Mon Sep 17 00:00:00 2001 From: Thomas Rubini <74205383+ThomasRubini@users.noreply.github.com> Date: Thu, 5 Jan 2023 15:03:57 +0100 Subject: [PATCH 30/42] update tests to detect exceptions --- tests/test_api.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index b57f196..efb3404 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -97,15 +97,15 @@ def test_that_sending_a_empty_username_results_in_an_error(): with pytest.raises(Exception) as e: createGame(user) - assert "Status is not ok" in str(e.value) - def test_that_a_too_long_username_results_in_an_error(): user = User("Le test unitaire est un moyen de vérifier qu’un extrait de code fonctionne correctement. C’est l’une des procédures mises en oeuvre dans le cadre d’une méthodologie de travail agile. ") - assert createGame(user) == None + with pytest.raises(Exception) as e: + createGame(user) def test_that_username_that_contains_non_alphanumerics_results_in_an_error(): user = User("я русский пират") - assert createGame(user) == None + with pytest.raises(Exception) as e: + createGame(user) ############################################################################### # # From 178894da0612bbe8c3188244640a02c02a8dfce7 Mon Sep 17 00:00:00 2001 From: Thomas Rubini <74205383+ThomasRubini@users.noreply.github.com> Date: Thu, 5 Jan 2023 15:05:31 +0100 Subject: [PATCH 31/42] use a custom error class in tests --- tests/test_api.py | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index efb3404..6dcb596 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -4,6 +4,14 @@ from truthseeker import app test_app = app.test_client() +class TestException(Exception): + + def __init__(self, message): + self.message = message + + def __str__(self): + return self.message + ############################################################################### # # # # @@ -21,12 +29,12 @@ def createGame(user:User): data = {"username":user.username} responseObject = test_app.post("/api/v1/createGame",data=data) if responseObject.status_code != 200: - raise Exception("status code is not 200") + raise TestException("status code is not 200") content = responseObject.json if content is None: - raise Exception("Response is null") + raise TestException("Response is null") if content["error"] != 0: - raise Exception("backend returned an error: "+content["msg"]) + raise TestException("backend returned an error: "+content["msg"]) user.isAdmin = True return content["game_id"] @@ -35,23 +43,23 @@ def joinGame(user:User,game_id:str): data = {"username":user.username,"game_id":game_id} responseObject = test_app.post("/api/v1/joinGame",data=data) if responseObject.status_code != 200: - raise Exception("status code is not 200") + raise TestException("status code is not 200") content = responseObject.json if content is None: - raise Exception("Response is null") + raise TestException("Response is null") if content["error"] != 0: - raise Exception("backend returned an error: "+content["msg"]) + raise TestException("backend returned an error: "+content["msg"]) return True def startGame(user:User): responseObject = test_app.post("/api/v1/startGame") if responseObject.status_code != 200: - raise Exception("status code is not 200") + raise TestException("status code is not 200") content = responseObject.json if content is None: - raise Exception("Response is null") + raise TestException("Response is null") if content["error"] != 0: - raise Exception("backend returned an error: "+content["msg"]) + raise TestException("backend returned an error: "+content["msg"]) return True @@ -94,17 +102,17 @@ def test_that_not_sending_a_username_results_in_an_error(): def test_that_sending_a_empty_username_results_in_an_error(): user = User("") - with pytest.raises(Exception) as e: + with pytest.raises(TestException) as e: createGame(user) def test_that_a_too_long_username_results_in_an_error(): user = User("Le test unitaire est un moyen de vérifier qu’un extrait de code fonctionne correctement. C’est l’une des procédures mises en oeuvre dans le cadre d’une méthodologie de travail agile. ") - with pytest.raises(Exception) as e: + with pytest.raises(TestException) as e: createGame(user) def test_that_username_that_contains_non_alphanumerics_results_in_an_error(): user = User("я русский пират") - with pytest.raises(Exception) as e: + with pytest.raises(TestException) as e: createGame(user) ############################################################################### @@ -188,14 +196,14 @@ def test_that_people_can_start_a_game(): def test_that_a_started_game_cannot_be_started_again(): - with pytest.raises(Exception) as e: + with pytest.raises(TestException) as e: owner = User("neosteopathie") game_id = createGame(owner) startGame(owner) assert "Status is not ok" in str(e.value) def test_that_non_owners_cant_start_a_game(): - with pytest.raises(Exception) as e: + with pytest.raises(TestException) as e: owner = User("neosteopathie") notOwner = User("neorphelin") game_id = createGame(owner) From c670cd36e19b27dd5b6bcfcf9e263fbffd30645b Mon Sep 17 00:00:00 2001 From: Thomas Rubini <74205383+ThomasRubini@users.noreply.github.com> Date: Thu, 5 Jan 2023 15:07:34 +0100 Subject: [PATCH 32/42] check if username is alphanumeric --- truthseeker/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/truthseeker/utils.py b/truthseeker/utils.py index 7cce76e..048dd29 100644 --- a/truthseeker/utils.py +++ b/truthseeker/utils.py @@ -1,6 +1,8 @@ def check_username(username): if not username: return False + if not username.isalnum(): + return False if not username == username.strip(): return False if not len(username) < 16: From 85500413450d18b7a2531732561b3a5aa671a863 Mon Sep 17 00:00:00 2001 From: Thomas Rubini <74205383+ThomasRubini@users.noreply.github.com> Date: Thu, 5 Jan 2023 15:10:45 +0100 Subject: [PATCH 33/42] check if username is already added to the game --- truthseeker/logic/game_logic.py | 7 +++++++ truthseeker/routes/routes_api.py | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/truthseeker/logic/game_logic.py b/truthseeker/logic/game_logic.py index b4aeaf7..e0ed3a1 100644 --- a/truthseeker/logic/game_logic.py +++ b/truthseeker/logic/game_logic.py @@ -56,7 +56,14 @@ class Game: self.members.append(self.owner) return self.owner + def get_member(self, username): + for member in self.members: + if member.username == username: + return member + def add_member(self, username): + if self.get_member(username): + return None member = Member(username) self.members.append(member) return member diff --git a/truthseeker/routes/routes_api.py b/truthseeker/routes/routes_api.py index def7d41..76180d4 100644 --- a/truthseeker/routes/routes_api.py +++ b/truthseeker/routes/routes_api.py @@ -39,8 +39,8 @@ def join_game(): if game == None: return {"error": 1, "msg": "game does not exist"} - - game.add_member(username) + if not game.add_member(username): + return {"error": 1, "msg": f"Username '{username}' already used in game {game.game_id}"} flask.session["game_id"] = game.game_id flask.session["is_owner"] = False From d624ed9918de5ebdb4d0ccc51ffcec8e2b35fdba Mon Sep 17 00:00:00 2001 From: Thomas Rubini <74205383+ThomasRubini@users.noreply.github.com> Date: Thu, 5 Jan 2023 15:32:39 +0100 Subject: [PATCH 34/42] replace more asserts with exception checks in tests --- tests/test_api.py | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index 6dcb596..6500e37 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -139,7 +139,8 @@ def test_that_two_person_can_join_a_game(): def test_that_people_cant_join_if_the_username_is_already_used(): game_id = createGame(User("neoreille")) joinGame(User("neosomse"),game_id) - assert joinGame(User("neosomse"),game_id) == False + with pytest.raises(TestException) as e: + joinGame(User("neosomse"),game_id) def test_that_people_joining_without_sending_any_data_results_in_an_error(): game_id = createGame(User("neoxyde")) @@ -163,17 +164,23 @@ def test_that_people_joining_without_sending_an_username_still_results_in_an_err def test_that_people_joining_with_an_empty_username_still_results_in_an_error(): game_id = createGame(User("neodeur")) user = User("") - assert joinGame(user,game_id) == False + + with pytest.raises(TestException) as e: + joinGame(user,game_id) def test_that_people_joining_aving_an_username_that_contains_non_alphanumerics_still_results_in_an_error(): game_id = createGame(User("neobservateur")) user = User("Я брат русского пирата") - assert joinGame(user,game_id) == False + + with pytest.raises(TestException) as e: + joinGame(user,game_id) def test_that_people_joining_aving_a_too_long_username_still_results_in_an_error(): game_id = createGame(User("neordre")) user = User("Les tests unitaires sont généralement effectués pendant la phase de développement des applications mobiles ou logicielles. Ces tests sont normalement effectués par les développeurs, bien qu’à toutes fins pratiques, ils puissent également être effectués par les responsables en assurance QA.") - assert joinGame(user,game_id) == False + + with pytest.raises(TestException) as e: + joinGame(user,game_id) ############################################################################### @@ -192,21 +199,18 @@ def test_that_people_joining_aving_a_too_long_username_still_results_in_an_error def test_that_people_can_start_a_game(): owner = User("neAUBERGINE") game_id = createGame(owner) - assert startGame(owner) == True - + startGame(owner) def test_that_a_started_game_cannot_be_started_again(): + owner = User("neosteopathie") + game_id = createGame(owner) with pytest.raises(TestException) as e: - owner = User("neosteopathie") - game_id = createGame(owner) startGame(owner) - assert "Status is not ok" in str(e.value) def test_that_non_owners_cant_start_a_game(): - with pytest.raises(TestException) as e: - owner = User("neosteopathie") - notOwner = User("neorphelin") - game_id = createGame(owner) - joinGame(notOwner,game_id) - assert startGame(notOwner) == False - assert "Status is not ok" in str(e.value) + owner = User("neosteopathie") + notOwner = User("neorphelin") + game_id = createGame(owner) + joinGame(notOwner,game_id) + with pytest.raises(TestException) as e: + startGame(notOwner) From ca18e58e9d6c15a6be6848949fff39986b0e89ac Mon Sep 17 00:00:00 2001 From: Thomas Rubini <74205383+ThomasRubini@users.noreply.github.com> Date: Thu, 5 Jan 2023 15:34:20 +0100 Subject: [PATCH 35/42] do not collect exception class --- tests/test_api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_api.py b/tests/test_api.py index 6500e37..fe47ab8 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -5,6 +5,7 @@ from truthseeker import app test_app = app.test_client() class TestException(Exception): + __test__ = False def __init__(self, message): self.message = message From 8c6128d45e55bc98846435338a492a83299a2cfa Mon Sep 17 00:00:00 2001 From: Thomas Rubini <74205383+ThomasRubini@users.noreply.github.com> Date: Thu, 5 Jan 2023 15:37:07 +0100 Subject: [PATCH 36/42] prevent start from starting twice --- truthseeker/logic/game_logic.py | 1 + truthseeker/routes/routes_api.py | 11 ++++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/truthseeker/logic/game_logic.py b/truthseeker/logic/game_logic.py index e0ed3a1..c443bf3 100644 --- a/truthseeker/logic/game_logic.py +++ b/truthseeker/logic/game_logic.py @@ -50,6 +50,7 @@ class Game: self.game_id = None self.owner = None self.members = [] + self.has_started = True def set_owner(self, username): self.owner = Member(username) diff --git a/truthseeker/routes/routes_api.py b/truthseeker/routes/routes_api.py index 76180d4..5a1f3ee 100644 --- a/truthseeker/routes/routes_api.py +++ b/truthseeker/routes/routes_api.py @@ -54,7 +54,16 @@ def start_game(): return {"error": 1, "msg": "No session"} if not flask.session["is_owner"]: return {"error": 1, "msg": "you are not the owner of this game"} - if game_logic.get_game(flask.session["game_id"]) == None: + + game = game_logic.get_game(flask.session["game_id"]) + + if game == None: return {"error": 1, "msg": "this game doesn't exist"} + if game.has_started: + return {"error": 1, "msg": "this game is already started"} + + game.has_started = None + + return {"error": 0} From 2459d74df0b658e24920cb33e6053145ff6f03b5 Mon Sep 17 00:00:00 2001 From: Thomas Rubini <74205383+ThomasRubini@users.noreply.github.com> Date: Thu, 5 Jan 2023 15:38:24 +0100 Subject: [PATCH 37/42] fix test that game cannot be started twice --- tests/test_api.py | 1 + truthseeker/logic/game_logic.py | 2 +- truthseeker/routes/routes_api.py | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index fe47ab8..4235864 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -205,6 +205,7 @@ def test_that_people_can_start_a_game(): def test_that_a_started_game_cannot_be_started_again(): owner = User("neosteopathie") game_id = createGame(owner) + startGame(owner) with pytest.raises(TestException) as e: startGame(owner) diff --git a/truthseeker/logic/game_logic.py b/truthseeker/logic/game_logic.py index c443bf3..b46f2f4 100644 --- a/truthseeker/logic/game_logic.py +++ b/truthseeker/logic/game_logic.py @@ -50,7 +50,7 @@ class Game: self.game_id = None self.owner = None self.members = [] - self.has_started = True + self.has_started = False def set_owner(self, username): self.owner = Member(username) diff --git a/truthseeker/routes/routes_api.py b/truthseeker/routes/routes_api.py index 5a1f3ee..c18b6e1 100644 --- a/truthseeker/routes/routes_api.py +++ b/truthseeker/routes/routes_api.py @@ -59,10 +59,11 @@ def start_game(): if game == None: return {"error": 1, "msg": "this game doesn't exist"} + print(game.has_started) if game.has_started: return {"error": 1, "msg": "this game is already started"} - game.has_started = None + game.has_started = True From 853dafca92b732ec9847d413a7161a1f3088c5ff Mon Sep 17 00:00:00 2001 From: SIMAILA Djalim Date: Fri, 6 Jan 2023 08:26:14 +0100 Subject: [PATCH 38/42] renamed file --- data_persistance/{data.py => remote.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename data_persistance/{data.py => remote.py} (100%) diff --git a/data_persistance/data.py b/data_persistance/remote.py similarity index 100% rename from data_persistance/data.py rename to data_persistance/remote.py From 8033002eea9c72b79f32fa1894df4780a044f67c Mon Sep 17 00:00:00 2001 From: SIMAILA Djalim Date: Fri, 6 Jan 2023 08:33:36 +0100 Subject: [PATCH 39/42] changed architecture --- data_persistance/{ => data}/answer.py | 0 data_persistance/{ => data}/locales.py | 0 data_persistance/{ => data}/npc.py | 0 data_persistance/{ => data}/places.py | 0 data_persistance/{ => data}/questions.py | 0 data_persistance/{ => data}/reactions.py | 0 data_persistance/{ => data}/traits.py | 0 data_persistance/remote.py | 14 +++++++------- 8 files changed, 7 insertions(+), 7 deletions(-) rename data_persistance/{ => data}/answer.py (100%) rename data_persistance/{ => data}/locales.py (100%) rename data_persistance/{ => data}/npc.py (100%) rename data_persistance/{ => data}/places.py (100%) rename data_persistance/{ => data}/questions.py (100%) rename data_persistance/{ => data}/reactions.py (100%) rename data_persistance/{ => data}/traits.py (100%) diff --git a/data_persistance/answer.py b/data_persistance/data/answer.py similarity index 100% rename from data_persistance/answer.py rename to data_persistance/data/answer.py diff --git a/data_persistance/locales.py b/data_persistance/data/locales.py similarity index 100% rename from data_persistance/locales.py rename to data_persistance/data/locales.py diff --git a/data_persistance/npc.py b/data_persistance/data/npc.py similarity index 100% rename from data_persistance/npc.py rename to data_persistance/data/npc.py diff --git a/data_persistance/places.py b/data_persistance/data/places.py similarity index 100% rename from data_persistance/places.py rename to data_persistance/data/places.py diff --git a/data_persistance/questions.py b/data_persistance/data/questions.py similarity index 100% rename from data_persistance/questions.py rename to data_persistance/data/questions.py diff --git a/data_persistance/reactions.py b/data_persistance/data/reactions.py similarity index 100% rename from data_persistance/reactions.py rename to data_persistance/data/reactions.py diff --git a/data_persistance/traits.py b/data_persistance/data/traits.py similarity index 100% rename from data_persistance/traits.py rename to data_persistance/data/traits.py diff --git a/data_persistance/remote.py b/data_persistance/remote.py index cc80318..1289cb0 100644 --- a/data_persistance/remote.py +++ b/data_persistance/remote.py @@ -4,13 +4,13 @@ from sqlalchemy import engine as eg from tables import * -from answer import ANSWER -from locales import LOCALES -from npc import NPCS -from places import PLACES -from questions import QUESTIONS -from reactions import REACTIONS -from traits import TRAITS +from data.answer import ANSWER +from data.locales import LOCALES +from data.npc import NPCS +from data.places import PLACES +from data.questions import QUESTIONS +from data.reactions import REACTIONS +from data.traits import TRAITS from secret import HOST, USER, PASS From 4e43ba853e6bfd06d51baa2cbc844b1708aa2b3a Mon Sep 17 00:00:00 2001 From: SIMAILA Djalim Date: Fri, 6 Jan 2023 08:52:24 +0100 Subject: [PATCH 40/42] fixed typo + data table drop at script launch --- data_persistance/data/answer.py | 2 +- data_persistance/remote.py | 43 ++++++++++++++++++++++++++++----- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/data_persistance/data/answer.py b/data_persistance/data/answer.py index feeea97..4a7c58b 100644 --- a/data_persistance/data/answer.py +++ b/data_persistance/data/answer.py @@ -1,5 +1,5 @@ from tables import Answer -ANSWER = [ +ANSWERS = [ ] \ No newline at end of file diff --git a/data_persistance/remote.py b/data_persistance/remote.py index 1289cb0..54ee469 100644 --- a/data_persistance/remote.py +++ b/data_persistance/remote.py @@ -4,7 +4,7 @@ from sqlalchemy import engine as eg from tables import * -from data.answer import ANSWER +from data.answer import ANSWERS from data.locales import LOCALES from data.npc import NPCS from data.places import PLACES @@ -22,24 +22,55 @@ url_object = eg.URL.create( port=6776, database="truthInquiry", ) - -# Create Engine and tables engine = create_engine(url_object) -Base.metadata.create_all(engine) + +# Reset data tables with Session(engine) as session: + + + session.execute("SELECT CONCAT('DROP TABLE IF EXISTS `', TABLE_SCHEMA, '`.`', TABLE_NAME, '`;') FROM information_schema.TABLES WHERE TABLE_SCHEMA = 'mydb'") + Base.metadata.create_all(engine) + print("adding locales") + for locale in LOCALES: + print(locale) session.add_all(LOCALES) + + print("adding places") + for place in PLACES: + print(place) session.add_all(PLACES) + + print("adding NPCS") + for npc in NPCS: + print(npc) session.add_all(NPCS) + + print("adding trait") + for trait in TRAITS: + print(trait) session.add_all(TRAITS) + + print("adding questions") + for question in QUESTIONS: + print(question) session.add_all(QUESTIONS) + + print("adding answers") - session.add_all(ANSWER) - print("adding reaction") + for answer in ANSWERS: + print(answer) + session.add_all(ANSWERS) + + + print("adding reactions") + for reactions in REACTIONS: + print(reactions) session.add_all(REACTIONS) + session.commit() From cb38bf940872608d7147e2e3b0d6ce80ff33088f Mon Sep 17 00:00:00 2001 From: SIMAILA Djalim Date: Fri, 6 Jan 2023 09:20:08 +0100 Subject: [PATCH 41/42] added to string to missing classes --- data_persistance/tables.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/data_persistance/tables.py b/data_persistance/tables.py index 4cd49a3..a8ac5aa 100644 --- a/data_persistance/tables.py +++ b/data_persistance/tables.py @@ -16,7 +16,7 @@ class Locale(Base): self.TEXT = TEXT def __str__(self): - return self.PLACE_ID + " " + self.LANG + " " + self.TEXT + return f"{self.PLACE_ID} {self.LANG} {self.TEXT}" class Place(Base): @@ -30,7 +30,7 @@ class Place(Base): self.NAME_LID = NAME_LID def __str__(self): - return self.PLACE_ID + " " + self.NAME_LID + return f"{self.PLACE_ID} {self.NAME_LID}" class Question(Base): @@ -46,7 +46,7 @@ class Question(Base): self.TEXT_LID = TEXT_LID def __str__(self): - return self.QUESTION_ID + " " + self.QUESTION_TYPE + " " + self.TEXT_LID + return f"{self.QUESTION_ID} {self.QUESTION_TYPE} {self.TEXT_LID}" class Answer(Base): @@ -62,6 +62,8 @@ class Answer(Base): self.NPC_ID = NPC_ID self.TEXT_LID = TEXT_LID + def __str__(self): + return f"{self.ANSWER_ID} {self.QA_TYPE} {self.NPC_ID} {self.TEXT_LID}" class Npc(Base): __tablename__ = "T_NPC" @@ -72,6 +74,8 @@ class Npc(Base): self.NPC_ID = NPC_ID self.NAME_LID = NAME_LID + def __str__(self) -> str: + return f"{self.NPC_ID} {self.NAME_LID}" class Trait(Base): __tablename__ = "T_TRAIT" @@ -82,6 +86,8 @@ class Trait(Base): self.TRAIT_ID = TRAIT_ID self.NAME_LID = NAME_LID + def __str__(self) -> str: + return f"{self.TRAIT_ID} {self.NAME_LID}" class Reaction(Base): __tablename__ = "T_REACTION" @@ -95,3 +101,6 @@ class Reaction(Base): self.DESC_LID = DESC_LID self.NPC_ID = NPC_ID self.TRAIT_ID = TRAIT_ID + + def __str__(self) -> str: + return f"{self.REACTION_ID} {self.DESC_LID} {self.NPC_ID} {self.TRAIT_ID}" \ No newline at end of file From a07e8270e3f64fac4fadc1c7a96a097d5dbca938 Mon Sep 17 00:00:00 2001 From: SIMAILA Djalim Date: Sat, 7 Jan 2023 17:09:08 +0100 Subject: [PATCH 42/42] slight data tables refactor + initial db data --- data_persistance/data/answer.py | 41 +++++++++++++- data_persistance/data/locales.py | 79 +++++++++++++++++++++++++- data_persistance/data/npc.py | 11 +++- data_persistance/data/places.py | 7 ++- data_persistance/data/questions.py | 5 +- data_persistance/data/reactions.py | 91 +++++++++++++++++++++++++++++- data_persistance/data/traits.py | 10 +++- data_persistance/remote.py | 31 +++++----- data_persistance/tables.py | 28 ++++----- 9 files changed, 269 insertions(+), 34 deletions(-) diff --git a/data_persistance/data/answer.py b/data_persistance/data/answer.py index 4a7c58b..864373f 100644 --- a/data_persistance/data/answer.py +++ b/data_persistance/data/answer.py @@ -1,5 +1,44 @@ from tables import Answer ANSWERS = [ - + Answer(1,0,1,1), + Answer(2,0,1,2), + Answer(3,1,1,3), + Answer(4,1,1,4), + Answer(5,0,2,6), + Answer(6,0,2,7), + Answer(7,1,2,8), + Answer(8,1,2,9), + Answer(9,0,3,11), + Answer(10,0,3,12), + Answer(11,1,3,13), + Answer(12,1,3,14), + Answer(13,0,4,16), + Answer(14,0,4,17), + Answer(15,1,4,18), + Answer(16,1,4,19), + Answer(17,0,5,21), + Answer(18,0,5,22), + Answer(19,1,5,23), + Answer(20,1,5,24), + Answer(21,0,6,26), + Answer(22,0,6,27), + Answer(23,1,6,28), + Answer(24,1,6,29), + Answer(25,0,7,31), + Answer(26,0,7,32), + Answer(27,1,7,33), + Answer(28,1,7,34), + Answer(29,0,8,36), + Answer(30,0,8,37), + Answer(31,1,8,38), + Answer(32,1,8,39), + Answer(33,0,9,41), + Answer(34,0,9,42), + Answer(35,1,9,43), + Answer(36,1,9,44), + Answer(37,0,10,46), + Answer(38,0,10,47), + Answer(39,1,10,48), + Answer(40,1,10,49) ] \ No newline at end of file diff --git a/data_persistance/data/locales.py b/data_persistance/data/locales.py index 1ca2da1..7d72462 100644 --- a/data_persistance/data/locales.py +++ b/data_persistance/data/locales.py @@ -1,5 +1,82 @@ from tables import Locale LOCALES = [ - Locale(0,"EN", "Hello World"), + Locale(0,"FR","Le Médecin"), + Locale(1,"FR","Il y avait {SALLE} ça m'a intrigué."), + Locale(2,"FR","{SALLE} avait l'air sympa donc j'y suis allé."), + Locale(3,"FR","Il me semble qu'il y avait {NPC}."), + Locale(4,"FR","Je suis pratiquement sûr que j'étais avec {NPC}."), + Locale(5,"FR","Le Baron"), + Locale(6,"FR","{SALLE}"), + Locale(7,"FR","{SALLE}"), + Locale(8,"FR","{NPC}"), + Locale(9,"FR","{NPC}"), + Locale(10,"FR","Le Diplomate"), + Locale(11,"FR","Je profitais d'une collation dans {SALLE}."), + Locale(12,"FR","J'admirais la décoration subtile de {SALLE} ... je m'en inspirerais pour chez moi."), + Locale(13,"FR","Je m'instruisais auprès de {NPC}."), + Locale(14,"FR","Avec {NPC} pour exposer nos différents points de vus sur divers sujets."), + Locale(15,"FR","Le Combattant"), + Locale(16,"FR","{SALLE} nous a servi de salle de duel."), + Locale(17,"FR","J'ai festoillé dans {SALLE}."), + Locale(18,"FR","On faisait un bras de fer avec {NPC}."), + Locale(19,"FR","{NPC} et moi nous sommes engagés dans une joute verbale des plus palpitante."), + Locale(20,"FR","L'Artiste"), + Locale(21,"FR","J'ai surement piqué un somme dans {SALLE}."), + Locale(22,"FR","{SALLE} ... definitivement {SALLE}."), + Locale(23,"FR","Avec {NPC}, c'est vraiment une personne désopilante."), + Locale(24,"FR","{NPC} est assez souple vous savez ?"), + Locale(25,"FR","La Duchesse"), + Locale(26,"FR","Pour votre gouverne je me trouvais dans {SALLE}."), + Locale(27,"FR","s'il vous faut savoir ... j'étais en train de me reposer dans {SALLE}."), + Locale(28,"FR","{NPC} me tenait compagnie."), + Locale(29,"FR","J'était avec {NPC}."), + Locale(30,"FR","La Diva"), + Locale(31,"FR","{SALLE} me semblait être la plus belle pièce de la maison."), + Locale(32,"FR","Je buvais un verre dans {SALLE}."), + Locale(33,"FR","Je profitais de la compagnie de {NPC}."), + Locale(34,"FR","J'étais avec {NPC} à partager une délicieuse conversation ainsi qu'une coupe de champagne."), + Locale(35,"FR","La Parieuse"), + Locale(36,"FR","J'avis monté une table de jeu dans {SALLE}."), + Locale(37,"FR","{SALLE} est tout de même plus agréable une fois changé en casino."), + Locale(38,"FR","Vous saviez que {NPC} était incroyable avec des cartes à la mains?"), + Locale(39,"FR","Si vous tenez à votre argent ne jouez jamisa au poker avec {NPC}."), + Locale(40,"FR","L'Agent"), + Locale(41,"FR","On pouvait me retrouver dans {SALLE}."), + Locale(42,"FR","{SALLE}"), + Locale(43,"FR","J'étais avec {NPC} au moment des faits."), + Locale(44,"FR","{NPC}"), + Locale(45,"FR","La Voyageuse"), + Locale(46,"FR","{SALLE}"), + Locale(47,"FR","{SALLE}"), + Locale(48,"FR","{NPC}"), + Locale(49,"FR","{NPC}"), + Locale(100,"FR","Ce manoir est plutôt grand ... vous pouvez me dire où vous étiez?"), + Locale(101,"FR","Vous étiez où au moment des faits?"), + Locale(102,"FR","Dans quelle salle étiez vous?"), + Locale(105,"FR","-- Avec qui etiez vous?"), + Locale(120,"FR","méfiant"), + Locale(110,"FR","deffinir méfiant"), + Locale(121,"FR","surpris"), + Locale(111,"FR","deffinir surpris"), + Locale(122,"FR","chagrin"), + Locale(112,"FR","deffinir chagrin"), + Locale(123,"FR","rejouie"), + Locale(113,"FR","deffinir rejouie"), + Locale(124,"FR","menteur"), + Locale(114,"FR","deffinir menteur"), + Locale(125,"FR","honnête"), + Locale(115,"FR","deffinir honnête"), + Locale(126,"FR","géné"), + Locale(116,"FR","deffinir géné"), + Locale(127,"FR","défencif"), + Locale(117,"FR","deffinir défencif"), + Locale(128,"FR","ennuyé"), + Locale(118,"FR","deffinir ennuyé"), + Locale(130,"FR","Le salon"), + Locale(131,"FR","La salle de reception"), + Locale(132,"FR","Le hall d'entrée"), + Locale(133,"FR","La cuisine"), + Locale(134,"FR","La chambre du maitre"), + Locale(135,"FR","Le jarin") ] diff --git a/data_persistance/data/npc.py b/data_persistance/data/npc.py index e2ce341..e91958a 100644 --- a/data_persistance/data/npc.py +++ b/data_persistance/data/npc.py @@ -1,5 +1,14 @@ from tables import Npc NPCS = [ - + Npc(1,0), + Npc(2,5), + Npc(3,10), + Npc(4,15), + Npc(5,20), + Npc(6,25), + Npc(7,30), + Npc(8,35), + Npc(9,40), + Npc(10,45), ] \ No newline at end of file diff --git a/data_persistance/data/places.py b/data_persistance/data/places.py index 57b77fe..044e746 100644 --- a/data_persistance/data/places.py +++ b/data_persistance/data/places.py @@ -1,5 +1,10 @@ from tables import Place PLACES = [ - + Place(1,130), + Place(2,131), + Place(3,132), + Place(4,133), + Place(5,134), + Place(6,135) ] \ No newline at end of file diff --git a/data_persistance/data/questions.py b/data_persistance/data/questions.py index c6bfebc..6a40cd2 100644 --- a/data_persistance/data/questions.py +++ b/data_persistance/data/questions.py @@ -1,5 +1,8 @@ from tables import Question QUESTIONS = [ - + Question(1,0,100), + Question(2,0,101), + Question(3,0,102), + Question(4,1,105) ] \ No newline at end of file diff --git a/data_persistance/data/reactions.py b/data_persistance/data/reactions.py index 534be94..f518b76 100644 --- a/data_persistance/data/reactions.py +++ b/data_persistance/data/reactions.py @@ -1,5 +1,94 @@ from tables import Reaction REACTIONS = [ - + Reaction(1,110,1,1), + Reaction(2,110,2,1), + Reaction(3,110,3,1), + Reaction(4,110,4,1), + Reaction(5,110,5,1), + Reaction(6,110,6,1), + Reaction(7,110,7,1), + Reaction(8,110,8,1), + Reaction(9,110,9,1), + Reaction(10,110,10,1), + Reaction(11,111,1,2), + Reaction(12,111,2,2), + Reaction(13,111,3,2), + Reaction(14,111,4,2), + Reaction(15,111,5,2), + Reaction(16,111,6,2), + Reaction(17,111,7,2), + Reaction(18,111,8,2), + Reaction(19,111,9,2), + Reaction(20,111,10,2), + Reaction(21,112,1,3), + Reaction(22,112,2,3), + Reaction(23,112,3,3), + Reaction(24,112,4,3), + Reaction(25,112,5,3), + Reaction(26,112,6,3), + Reaction(27,112,7,3), + Reaction(28,112,8,3), + Reaction(29,112,9,3), + Reaction(30,112,10,3), + Reaction(31,113,1,4), + Reaction(32,113,2,4), + Reaction(33,113,3,4), + Reaction(34,113,4,4), + Reaction(35,113,5,4), + Reaction(36,113,6,4), + Reaction(37,113,7,4), + Reaction(38,113,8,4), + Reaction(39,113,9,4), + Reaction(40,113,10,4), + Reaction(41,114,1,5), + Reaction(42,114,2,5), + Reaction(43,114,3,5), + Reaction(44,114,4,5), + Reaction(45,114,5,5), + Reaction(46,114,6,5), + Reaction(47,114,7,5), + Reaction(48,114,8,5), + Reaction(49,114,9,5), + Reaction(50,114,10,5), + Reaction(51,115,1,6), + Reaction(52,115,2,6), + Reaction(53,115,3,6), + Reaction(54,115,4,6), + Reaction(55,115,5,6), + Reaction(56,115,6,6), + Reaction(57,115,7,6), + Reaction(58,115,8,6), + Reaction(59,115,9,6), + Reaction(60,115,10,6), + Reaction(61,116,1,7), + Reaction(62,116,2,7), + Reaction(63,116,3,7), + Reaction(64,116,4,7), + Reaction(65,116,5,7), + Reaction(66,116,6,7), + Reaction(67,116,7,7), + Reaction(68,116,8,7), + Reaction(69,116,9,7), + Reaction(70,116,10,7), + Reaction(71,117,1,8), + Reaction(72,117,2,8), + Reaction(73,117,3,8), + Reaction(74,117,4,8), + Reaction(75,117,5,8), + Reaction(76,117,6,8), + Reaction(77,117,7,8), + Reaction(78,117,8,8), + Reaction(79,117,9,8), + Reaction(80,117,10,8), + Reaction(81,118,1,9), + Reaction(82,118,2,9), + Reaction(83,118,3,9), + Reaction(84,118,4,9), + Reaction(85,118,5,9), + Reaction(86,118,6,9), + Reaction(87,118,7,9), + Reaction(88,118,8,9), + Reaction(89,118,9,9), + Reaction(90,118,10,9) ] \ No newline at end of file diff --git a/data_persistance/data/traits.py b/data_persistance/data/traits.py index 0eed597..2514932 100644 --- a/data_persistance/data/traits.py +++ b/data_persistance/data/traits.py @@ -1,5 +1,13 @@ from tables import Trait TRAITS = [ - + Trait(1,120), + Trait(2,121), + Trait(3,122), + Trait(4,123), + Trait(5,124), + Trait(6,125), + Trait(7,126), + Trait(8,127), + Trait(9,128) ] \ No newline at end of file diff --git a/data_persistance/remote.py b/data_persistance/remote.py index 54ee469..acf5fd4 100644 --- a/data_persistance/remote.py +++ b/data_persistance/remote.py @@ -27,50 +27,53 @@ engine = create_engine(url_object) # Reset data tables with Session(engine) as session: - - - session.execute("SELECT CONCAT('DROP TABLE IF EXISTS `', TABLE_SCHEMA, '`.`', TABLE_NAME, '`;') FROM information_schema.TABLES WHERE TABLE_SCHEMA = 'mydb'") + Base.metadata.drop_all(engine) Base.metadata.create_all(engine) print("adding locales") for locale in LOCALES: print(locale) - session.add_all(LOCALES) + session.add(locale) + session.commit() print("adding places") for place in PLACES: print(place) - session.add_all(PLACES) + session.add(place) + session.commit() print("adding NPCS") for npc in NPCS: print(npc) - session.add_all(NPCS) + session.add(npc) + session.commit() print("adding trait") for trait in TRAITS: print(trait) - session.add_all(TRAITS) + session.add(trait) + session.commit() print("adding questions") for question in QUESTIONS: print(question) - session.add_all(QUESTIONS) + session.add(question) + session.commit() print("adding answers") for answer in ANSWERS: print(answer) - session.add_all(ANSWERS) + session.add(answer) + session.commit() print("adding reactions") - for reactions in REACTIONS: - print(reactions) - session.add_all(REACTIONS) - - session.commit() + for reaction in REACTIONS: + print(reaction) + session.add(reaction) + session.commit() \ No newline at end of file diff --git a/data_persistance/tables.py b/data_persistance/tables.py index a8ac5aa..26bebca 100644 --- a/data_persistance/tables.py +++ b/data_persistance/tables.py @@ -1,4 +1,4 @@ -from sqlalchemy import Column, Integer, Text, ForeignKey +from sqlalchemy import Column, Integer, Text, ForeignKey, VARCHAR from sqlalchemy.orm import declarative_base, relationship Base = declarative_base() @@ -7,17 +7,16 @@ Base = declarative_base() class Locale(Base): __tablename__ = 'T_LOCALE' TEXT_ID = Column(Integer, primary_key=True) - LANG = Column(Text) + LANG = Column(VARCHAR(2), primary_key=True) TEXT = Column(Text) def __init__(self, TEXT_ID, LANG, TEXT): - self.PLACE_ID = TEXT_ID + self.TEXT_ID = TEXT_ID self.LANG = LANG self.TEXT = TEXT def __str__(self): - return f"{self.PLACE_ID} {self.LANG} {self.TEXT}" - + return f"{self.TEXT_ID} {self.LANG} {self.TEXT}" class Place(Base): __tablename__ = 'T_PLACE' @@ -32,7 +31,6 @@ class Place(Base): def __str__(self): return f"{self.PLACE_ID} {self.NAME_LID}" - class Question(Base): __tablename__ = "T_QUESTION" QUESTION_ID = Column(Integer, primary_key=True) @@ -48,16 +46,17 @@ class Question(Base): def __str__(self): return f"{self.QUESTION_ID} {self.QUESTION_TYPE} {self.TEXT_LID}" - class Answer(Base): __tablename__ = "T_ANSWER" ANSWER_ID = Column(Integer, primary_key=True) QA_TYPE = Column(Integer) NPC_ID = Column(Integer, ForeignKey("T_NPC.NPC_ID")) TEXT_LID = Column(Integer, ForeignKey("T_LOCALE.TEXT_ID")) + LOCALE = relationship("Locale") + NPC = relationship("Npc") - def __init__(self, ANSWSER_ID, QA_TYPE, NPC_ID, TEXT_LID): - self.ANSWSER_ID = ANSWSER_ID + def __init__(self, ANSWER_ID, QA_TYPE, NPC_ID, TEXT_LID): + self.ANSWER_ID = ANSWER_ID self.QA_TYPE = QA_TYPE self.NPC_ID = NPC_ID self.TEXT_LID = TEXT_LID @@ -75,7 +74,7 @@ class Npc(Base): self.NAME_LID = NAME_LID def __str__(self) -> str: - return f"{self.NPC_ID} {self.NAME_LID}" + return f"{self.NPC_ID} {self.NAME_LID}" class Trait(Base): __tablename__ = "T_TRAIT" @@ -92,9 +91,12 @@ class Trait(Base): class Reaction(Base): __tablename__ = "T_REACTION" REACTION_ID = Column(Integer, primary_key=True) + NPC_ID = Column(Integer, ForeignKey("T_NPC.NPC_ID"),primary_key=True) + TRAIT_ID = Column(Integer, ForeignKey("T_TRAIT.TRAIT_ID"),primary_key=True) DESC_LID = Column(Integer, ForeignKey("T_LOCALE.TEXT_ID")) - NPC_ID = Column(Integer, ForeignKey("T_NPC.NPC_ID")) - TRAIT_ID = Column(Integer, ForeignKey("T_TRAIT.TRAIT_ID")) + LOCALE = relationship("Locale") + NPC = relationship("Npc") + TRAIT = relationship("Trait") def __init__(self, REACTION_ID, DESC_LID, NPC_ID, TRAIT_ID): self.REACTION_ID = REACTION_ID @@ -103,4 +105,4 @@ class Reaction(Base): self.TRAIT_ID = TRAIT_ID def __str__(self) -> str: - return f"{self.REACTION_ID} {self.DESC_LID} {self.NPC_ID} {self.TRAIT_ID}" \ No newline at end of file + return f"{self.REACTION_ID} {self.DESC_LID} {self.NPC_ID} {self.TRAIT_ID}"