diff options
| -rw-r--r-- | app.py | 73 | ||||
| -rw-r--r-- | config.py | 16 | ||||
| -rw-r--r-- | dummy_data_generation.py | 28 | ||||
| -rw-r--r-- | index.html | 0 | ||||
| -rw-r--r-- | model.py | 50 | ||||
| -rw-r--r-- | pyproject.toml | 35 | ||||
| -rw-r--r-- | requirements-dev.txt | 1 | ||||
| -rw-r--r-- | requirements.txt | 3 | ||||
| -rw-r--r-- | src/match_up/__init__.py | 60 | ||||
| -rw-r--r-- | src/match_up/__main__.py | 4 | ||||
| -rw-r--r-- | src/match_up/model.py | 153 | ||||
| -rw-r--r-- | src/match_up/templates/index.html | 15 | ||||
| -rw-r--r-- | src/match_up/tests/__init__.py | 44 | ||||
| -rw-r--r-- | src/match_up/view.py | 96 | ||||
| -rw-r--r-- | templates/index.html | 51 | ||||
| -rw-r--r-- | templates/index.j2 | 45 | ||||
| -rw-r--r-- | templates/macro.j2 | 3 | ||||
| -rw-r--r-- | templates/player.j2 | 1 | ||||
| -rw-r--r-- | templates/players.j2 | 7 |
19 files changed, 184 insertions, 501 deletions
@@ -1,55 +1,42 @@ -from dash import Dash -from pathlib import PurePath -from importlib.resources import files -from match_up.model import PlaySession -from match_up.view import MainLayout -from match_up.tests import ( - TEST_DATABASE_LOCATION, -) -from dash_bootstrap_components.themes import ZEPHYR -from flask import Flask, render_template +from os import getenv +from model import PlayerList +from flask import Flask, abort, render_template +from dotenv import load_dotenv -DATABASE_LOCATION = "storage.json" +load_dotenv() -# class Application: -# def __init__(self, debug=False): -# self._debug = debug -# if debug: -# database_location = TEST_DATABASE_LOCATION -# else: -# database_location = DATABASE_LOCATION -# self._session = PlaySession(PurePath(database_location)) -# self._dashboard = Dash("Match Up!", external_stylesheets=[ZEPHYR]) -# self._layout = MainLayout( -# self._session.data.get_players(), self._session.get_matches() -# ) -# self._dashboard.layout = self._layout.get() +print("Importing player list...\n") +PLAYER_LIST = PlayerList(location="test_data.csv") +print("List of player:\n") +print(PLAYER_LIST) -# def run(self): -# self._dashboard.run(debug=self._debug) +app = Flask("Match Up! Backend") +env_config = getenv("PROD_APP_SETTINGS", "config.DevelopmentConfig") +app.config.from_object(env_config) -# def main(): -# a = Application(debug=True) -# a.run() - - -app = Flask("Match Up") -session = PlaySession(PurePath("test_storage.json")) -base_template_path = "match_up.templates" +@app.route("/", methods=["GET"]) +def home(): + return render_template("index.j2") -@app.route("/") -def home(): - print(files(base_template_path).joinpath("index.html").name) +@app.route("/player-toggle/<player_request>", methods=["POST"]) +def toggle_player(player_request): + selection = PLAYER_LIST.get_player(player_request) + if len(selection) != 1: + abort(404) + PLAYER_LIST.toggle_player(player_request) + selection = PLAYER_LIST.get_player(player_request) return render_template( - "index.html", - player_list=session.data.get_players(), - game_list=session.get_matches(), + "player.j2", + player=selection[0], ) -if __name__ == "__main__": - print("Starting app") - app.run(host="localhost", debug=True, port=80) +@app.route("/player-list", methods=["GET"]) +def get_list_of_players(): + return render_template( + "players.j2", + players=PLAYER_LIST.players, + ) diff --git a/config.py b/config.py new file mode 100644 index 0000000..7d01933 --- /dev/null +++ b/config.py @@ -0,0 +1,16 @@ +class Config: + DEBUG = False + DEVELOPMENT = False + CSRF_ENABLED = True + ASSETS_DEBUG = False + + +class ProductionConfig(Config): + pass + + +class DevelopmentConfig(Config): + DEBUG = True + DEVELOPMENT = True + TEMPLATES_AUTO_RELOAD = True + ASSETS_DEBUG = True diff --git a/dummy_data_generation.py b/dummy_data_generation.py new file mode 100644 index 0000000..3ee6e42 --- /dev/null +++ b/dummy_data_generation.py @@ -0,0 +1,28 @@ +from csv import writer +from model import Player +from names import get_full_name +from random import randint, getrandbits + + +def get_test_players(total_player: int = 20, min_skill: int = 1, max_skill: int = 10): + return [ + Player( + name=get_full_name(), + skill=randint(min_skill, max_skill), + status=bool(getrandbits(1)), + ) + for _ in range(total_player) + ] + + +def main(): + dummy_list = get_test_players() + with open("test_data.csv", "w", newline="") as file: + data_writer = writer(file) + data_writer.writerow(["name", "skill", "status"]) + for player in dummy_list: + data_writer.writerow([player.name, player.skill, player.status]) + + +if __name__ == "__main__": + main() diff --git a/index.html b/index.html deleted file mode 100644 index e69de29..0000000 --- a/index.html +++ /dev/null diff --git a/model.py b/model.py new file mode 100644 index 0000000..53119eb --- /dev/null +++ b/model.py @@ -0,0 +1,50 @@ +from csv import reader +from pathlib import Path +from dataclasses import dataclass + + +@dataclass +class Player: + name: str + skill: int + status: bool + + +class PlayerList: + def __init__(self, location: Path = "players.csv"): + with open(location, newline="") as file: + data_reader = reader(file) + next(data_reader, None) + self.players = [ + ( + Player( + name=row[0], + skill=row[1], + status=(row[2] == "True"), + ) + ) + for row in data_reader + ] + + def get_player(self, player_request: str): + return [player for player in self.players if player.name == player_request] + + def toggle_player(self, player_request: Player): + self.players = [ + ( + Player( + name=player.name, + skill=player.skill, + status=not player.status, + ) + if player.name == player_request + else player + ) + for player in self.players + ] + + def __str__(self): + display_string = "" + for player in self.players: + display_string += player.__str__() + "\n" + return display_string diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index a82318c..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,35 +0,0 @@ -[build-system] -requires = ["setuptools"] -build-backend = "setuptools.build_meta" - -[project] -name = "match_up" -version = "0.1" -license = {file = "LICENSE"} -authors = [ - { name="Karan Jayachandra", email="mail@karanjayachandra.com" }, -] -description = "Plotly dashboard for skill based matchmaking." -readme = "README.md" -requires-python = ">=3.10" -classifiers = [ - "Programming Language :: Python :: 3", - "Operating System :: OS Independent", -] -dependencies = [ - 'tinydb', - 'plotly', - 'dash', - 'dash_bootstrap_components', -] - -[project.scripts] -match_up = "match_up:main" - -[project.urls] -Repository = "https://gitlab.com/KaranJayachandra/match_up" - -[project.optional-dependencies] -tests = ['names', 'pytest', 'pytest-cov'] -docs = ['sphinx', 'sphinx-autodoc-typehints'] -ci = ['coverage2clover']
\ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..ee8bbca --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1 @@ +names
\ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..886c3db --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +flask +python-dotenv +gunicorn
\ No newline at end of file diff --git a/src/match_up/__init__.py b/src/match_up/__init__.py deleted file mode 100644 index 95f767e..0000000 --- a/src/match_up/__init__.py +++ /dev/null @@ -1,60 +0,0 @@ -from dash import Dash -from pathlib import PurePath -from importlib.resources import files -from match_up.model import PlaySession -from match_up.view import MainLayout -from match_up.tests import ( - TEST_DATABASE_LOCATION, -) -from dash_bootstrap_components.themes import ZEPHYR -from flask import Flask, render_template - -DATABASE_LOCATION = "storage.json" - - -# class Application: -# def __init__(self, debug=False): -# self._debug = debug -# if debug: -# database_location = TEST_DATABASE_LOCATION -# else: -# database_location = DATABASE_LOCATION -# self._session = PlaySession(PurePath(database_location)) -# self._dashboard = Dash("Match Up!", external_stylesheets=[ZEPHYR]) -# self._layout = MainLayout( -# self._session.data.get_players(), self._session.get_matches() -# ) -# self._dashboard.layout = self._layout.get() - -# def run(self): -# self._dashboard.run(debug=self._debug) - - -# def main(): -# a = Application(debug=True) -# a.run() - - -app = Flask("Match Up", template_folder="templates") -session = PlaySession(PurePath("test_storage.json")) -base_template_path = "match_up.templates" - - -@app.route("/") -def home(): - print(files(base_template_path).joinpath("index.html").name) - # return render_template( - # files(base_template_path).joinpath("index.html").name, - # title="My Generated Page", - # people=session.data.get_players(), - # ) - return render_template( - "index.html", - title="My Generated Page", - people=session.data.get_players(), - ) - - -if __name__ == "__main__": - print("Starting app") - app.run(host="localhost", debug=True, port=80) diff --git a/src/match_up/__main__.py b/src/match_up/__main__.py deleted file mode 100644 index 3500cdb..0000000 --- a/src/match_up/__main__.py +++ /dev/null @@ -1,4 +0,0 @@ -from match_up import main - -if __name__ == "__main__": - main() diff --git a/src/match_up/model.py b/src/match_up/model.py deleted file mode 100644 index d27893d..0000000 --- a/src/match_up/model.py +++ /dev/null @@ -1,153 +0,0 @@ -from pathlib import PurePath -from tinydb import TinyDB, where -from dataclasses import dataclass -from random import randint - -MIN_SKILL = 1 -MAX_SKILL = 10 - - -class PlaySession: - def __init__(self, database_location: PurePath): - self.data = PlayerDatabase(database_location) - - def get_matches(self, number_of_courts: int = 4): - player_list = self.data.get_players() - game_list = [] - for i in range(number_of_courts): - game_list.append( - Game( - court_number=i + 1, - team_1=Team( - player_1=player_list[randint(0, len(player_list) - 1)], - player_2=player_list[randint(0, len(player_list) - 1)], - ), - team_2=Team( - player_1=player_list[randint(0, len(player_list) - 1)], - player_2=player_list[randint(0, len(player_list) - 1)], - ), - ) - ) - return game_list - - def end(self): - self.data.close() - - def __str__(self): - display_string = "" - for player in self.data.get_players(): - display_string = display_string + player.__str__() + "\n" - return display_string.rstrip() - - def __del__(self): - self.end() - - -@dataclass -class Player: - first_name: str - last_name: str - skill: int = 1 - status: bool = False - wait_time: int = 0 - - def get_name(self): - return self.first_name + " " + self.last_name - - -@dataclass -class Team: - player_1: Player - player_2: Player - - def __str__(self): - return self.player_1.get_name() + " & " + self.player_2.get_name() - - -@dataclass -class Game: - court_number: int - team_1: Team - team_2: Team - - -class PlayerDatabase: - def __init__(self, database_location: PurePath = PurePath("storage.json")): - print(f"Using database location: {database_location.name}") - self._database = TinyDB(database_location.name) - - def get_players(self) -> list[Player]: - player_list = [] - for row in self._database: - player_list.append( - Player( - first_name=row["first_name"], - last_name=row["last_name"], - skill=row["skill"], - status=row["status"], - wait_time=row["wait_time"], - ) - ) - return player_list - - def activate_player(self, player: Player): - self._database.update( - {"status": True}, - where("first_name") == player.first_name - and where("last_name") == player.last_name, - ) - - def deactivate_player(self, player: Player): - self._database.update( - {"wait_time": False}, - where("first_name") == player.first_name - and where("last_name") == player.last_name, - ) - - def increment_player_wait(self, player: Player): - self._database.update( - {"wait_time": 1}, - where("first_name") == player.first_name - and where("last_name") == player.last_name, - ) - - def reset_player_wait(self, player: Player): - self._database.update( - {"wait_time": 0}, - where("first_name") == player.first_name - and where("last_name") == player.last_name, - ) - - def deactivate_all_players(self) -> bool: - for row in self._database: - row["status"] = False - - def add_player(self, player: Player) -> bool: - self._database.insert( - { - "first_name": player.first_name, - "last_name": player.last_name, - "skill": player.skill, - "status": player.status, - "wait_time": player.wait_time, - } - ) - - def remove_player(self, player: Player) -> bool: - self._database.remove( - where("first_name") == player.first_name - and where("last_name") == player.last_name - ) - - def update_player_skill(self, player: Player): - self._database.update( - {"skill": player.skill}, - where("first_name") == player.first_name - and where("last_name") == player.last_name, - ) - - def close(self) -> None: - self._database.close() - - def __del__(self): - self.close() diff --git a/src/match_up/templates/index.html b/src/match_up/templates/index.html deleted file mode 100644 index e292734..0000000 --- a/src/match_up/templates/index.html +++ /dev/null @@ -1,15 +0,0 @@ -<!doctype html> -<link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css"> -<html> - <head> - <title>{{ title }}</title> - </head> - <body> - <h1>Match Up!</h1> - <ul> - {% for person in people %} - <li>{{ person.first_name }}</li> - {% endfor %} - </ul> - </body> -</html>
\ No newline at end of file diff --git a/src/match_up/tests/__init__.py b/src/match_up/tests/__init__.py deleted file mode 100644 index 3166746..0000000 --- a/src/match_up/tests/__init__.py +++ /dev/null @@ -1,44 +0,0 @@ -from os import remove -from os.path import isfile -from random import randint -from pathlib import PurePath -from names import get_first_name, get_last_name -from match_up.model import Player, PlayerDatabase, MIN_SKILL, MAX_SKILL - -TEST_DATABASE_LOCATION = PurePath("test_storage.json") - - -def generate_test_database(total_players: int = 20) -> None: - try: - if isfile(TEST_DATABASE_LOCATION): - remove(TEST_DATABASE_LOCATION) - db = PlayerDatabase(TEST_DATABASE_LOCATION) - for _ in range(total_players): - db.add_player( - Player( - first_name=get_first_name(), - last_name=get_last_name(), - skill=randint(MIN_SKILL, MAX_SKILL), - ) - ) - finally: - db.close() - - -def activate_random_players(activate_players: int = 17) -> None: - try: - db = PlayerDatabase(TEST_DATABASE_LOCATION) - player_list = db.get_players() - for index, player in enumerate(player_list): - if index < activate_players: - db.activate_player(player) - finally: - db.close() - - -def main(): - pass - - -if __name__ == "__main__": - main() diff --git a/src/match_up/view.py b/src/match_up/view.py deleted file mode 100644 index 8fa1c54..0000000 --- a/src/match_up/view.py +++ /dev/null @@ -1,96 +0,0 @@ -from dash.html import Div, H4, Thead, Tbody, Tr, Td, Th -from match_up.model import Player, Game -from dash_bootstrap_components import ( - Row, - Col, - Container, - Input, - Select, - Table, - NavbarSimple, -) - - -class GameLayout: - def __init__(self, game_list: list[Game]): - table_header = [Thead(Tr([Th("Court"), Th("Team 1"), Th("Team 2")]))] - table_body = [ - Tbody( - [ - Tr( - [ - Td(children=game.court_number), - Td(children=game.team_1.__str__()), - Td(children=game.team_2.__str__()), - ] - ) - for game in game_list - ] - ) - ] - self._layout = Div( - [ - H4(children="Game List", style={"textAlign": "center"}), - Table(table_header + table_body, bordered=True), - ] - ) - - def get(self): - return self._layout - - -class PlayerLayout: - def __init__(self, player_list: list[Player]): - table_header = [Thead(Tr([Th("Player Name")]))] - table_body = [ - Tbody( - [ - Tr( - [ - Td( - children=player.first_name + " " + player.last_name, - style={"color": ("green" if player.status else "red")}, - ) - ] - ) - for player in player_list - ] - ) - ] - self._layout = Div( - [ - H4(children="Player List", style={"textAlign": "center"}), - Table(table_header + table_body, bordered=True), - ] - ) - - def get(self): - return self._layout - - -class MainLayout: - def __init__(self, player_list: list[Player], game_list: list[Game]): - self._player_layout = PlayerLayout(player_list) - self._game_layout = GameLayout(game_list) - - def refresh(): ... - - def get(self): - full_layout = Div( - [ - NavbarSimple(brand="Match Up!"), - Container( - [ - Row( - [ - Col([self._game_layout.get()], width=9), - Col([self._player_layout.get()], width=3), - ], - style={"margin": "15px"}, - ) - ] - ), - ] - ) - # full_layout = Div([Div(children="Hello World")]) - return full_layout diff --git a/templates/index.html b/templates/index.html deleted file mode 100644 index db4a991..0000000 --- a/templates/index.html +++ /dev/null @@ -1,51 +0,0 @@ -<!doctype html> -<link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css"> - -<html> - <head> - <title>Match Up!</title> - </head> - <body> - <h1>Match Up!</h1> - <div class="row"> - <div style=" float: left; width: 60%"> - <table> - <thead> - <tr> - <th> Court </th> - <th> Team 1 </th> - <th> Team 2 </th> - </tr> - </thead> - <tbody> - {% for game in game_list %} - <tr> - <td width="10%"> {{game.court_number}} </td> - <td width="40%"> {{game.team_1.player_1.get_name()}} <br> {{game.team_1.player_2.get_name()}} </td> - <td width="40%"> {{game.team_2.player_1.get_name()}} <br> {{game.team_2.player_2.get_name()}} </td> - </tr> - {% endfor %} - </tbody> - </table> - </div> - <div style=" float: left; width: 30%"> - <table> - <thead> - <tr> - <th> Player </th> - </tr> - </thead> - <tbody> - {% for player in player_list %} - <tr> - <td style={% if player.status %} "color : green" {% else %} "color : red" {% endif %}> - {{ player.get_name() }} - </td> - </tr> - {% endfor %} - </tbody> - </table> - </div> - </div> - </body> -</html>
\ No newline at end of file diff --git a/templates/index.j2 b/templates/index.j2 new file mode 100644 index 0000000..82c3c80 --- /dev/null +++ b/templates/index.j2 @@ -0,0 +1,45 @@ +<!doctype html> +<html lang="en"> + <head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>🏸Match Up!</title> + <link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css"> + <script src="https://unpkg.com/htmx.org@1.9.11"></script> + <style> + button { + height: 50px; + width: 200px; + } + .active { + background-color: #27ae60; + } + .dormant { + background-color: #a93226; + } + .grid-container { + display: grid; + grid-template-columns: auto auto auto; + row-gap: 20px; + column-gap: 20px; + } + .tagline { + text-align: center; + } + </style> + </head> + <body> + <header> + <h1>🏸Match Up!</h1> + <p class="tagline"> Come play with the <a href="https://www.bctsf.nl/">Smashing Fellows</a>! </p> + </header> + <hr> + <div class="players"> + <h2>Players</h2> + <div hx-get="/player-list" hx-swap="outerHTML" hx-trigger="revealed"></div> + </div> + <footer> + Made by <a href="https://karanjayachandra.com/">Karan Jayachandra</a> using <a href="https://htmx.org/">HTMX</a> and <a href="https://flask.palletsprojects.com">Flask</a> + </footer> + </body> +</html>
\ No newline at end of file diff --git a/templates/macro.j2 b/templates/macro.j2 new file mode 100644 index 0000000..e0089bc --- /dev/null +++ b/templates/macro.j2 @@ -0,0 +1,3 @@ +{% macro player_button(player) %} +<button class={% if player.status %} "active" {% else %} "dormant" {% endif %} hx-post="{{ "/player-toggle/" ~ player.name}}" hx-trigger="click" hx-swap="outerHTML">{{ player.name }}</button> +{%- endmacro %}
\ No newline at end of file diff --git a/templates/player.j2 b/templates/player.j2 new file mode 100644 index 0000000..1176d65 --- /dev/null +++ b/templates/player.j2 @@ -0,0 +1 @@ +<button class={% if player.status %} "active" {% else %} "dormant" {% endif %} hx-post="{{ "/player-toggle/" ~ player.name}}" hx-trigger="click" hx-swap="outerHTML">{{ player.name }}</button>
\ No newline at end of file diff --git a/templates/players.j2 b/templates/players.j2 new file mode 100644 index 0000000..3005fa4 --- /dev/null +++ b/templates/players.j2 @@ -0,0 +1,7 @@ +<div class="grid-container"> +{% for player in players %} + <div class="grid-item"> + <button class={% if player.status %} "active" {% else %} "dormant" {% endif %} hx-post="{{ "/player-toggle/" ~ player.name}}" hx-trigger="click" hx-swap="outerHTML">{{ player.name }}</button> + </div> +{% endfor %} +</div>
\ No newline at end of file |
