quizzy 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- quizzy-0.1.0/.github/workflows/ci.yaml +19 -0
- quizzy-0.1.0/.github/workflows/publish-to-pypi.yaml +26 -0
- quizzy-0.1.0/.gitignore +138 -0
- quizzy-0.1.0/.pre-commit-config.yaml +20 -0
- quizzy-0.1.0/LICENSE +21 -0
- quizzy-0.1.0/PKG-INFO +69 -0
- quizzy-0.1.0/README.md +56 -0
- quizzy-0.1.0/assets/question-board.png +0 -0
- quizzy-0.1.0/examples/quizzy.yaml +21 -0
- quizzy-0.1.0/pyproject.toml +55 -0
- quizzy-0.1.0/quizzy/__init__.py +3 -0
- quizzy-0.1.0/quizzy/__main__.py +9 -0
- quizzy-0.1.0/quizzy/app.py +208 -0
- quizzy-0.1.0/quizzy/models.py +59 -0
- quizzy-0.1.0/quizzy/quizzy.tcss +101 -0
- quizzy-0.1.0/uv.lock +1107 -0
@@ -0,0 +1,19 @@
|
|
1
|
+
name: test
|
2
|
+
|
3
|
+
on: [push]
|
4
|
+
jobs:
|
5
|
+
lint:
|
6
|
+
runs-on: ubuntu-latest
|
7
|
+
strategy:
|
8
|
+
matrix:
|
9
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
10
|
+
|
11
|
+
steps:
|
12
|
+
- uses: actions/checkout@v4
|
13
|
+
- uses: astral-sh/setup-uv@v4
|
14
|
+
with:
|
15
|
+
version: "0.5.9"
|
16
|
+
- name: Ruff lint
|
17
|
+
run: uv run ruff check
|
18
|
+
- name: Ruff format
|
19
|
+
run: uv run ruff format --diff --check
|
@@ -0,0 +1,26 @@
|
|
1
|
+
name: Publish Python distributions to PyPI
|
2
|
+
on:
|
3
|
+
push:
|
4
|
+
tags:
|
5
|
+
- v[0-9]+.[0-9]+.[0-9]+
|
6
|
+
jobs:
|
7
|
+
pypi-publish:
|
8
|
+
name: Upload to PyPI
|
9
|
+
runs-on: ubuntu-latest
|
10
|
+
permissions:
|
11
|
+
# For PyPI's trusted publishing.
|
12
|
+
id-token: write
|
13
|
+
steps:
|
14
|
+
- name: "Install uv"
|
15
|
+
uses: astral-sh/setup-uv@v4
|
16
|
+
with:
|
17
|
+
# Install a specific version of uv.
|
18
|
+
version: "0.5.9"
|
19
|
+
- name: Checkout the repository
|
20
|
+
uses: actions/checkout@v3
|
21
|
+
- name: Lint with ruff
|
22
|
+
run: uv run ruff check
|
23
|
+
- name: Build Python distributions
|
24
|
+
run: uv build
|
25
|
+
- name: Publish to PyPI
|
26
|
+
run: uv publish -v --trusted-publishing always
|
quizzy-0.1.0/.gitignore
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
# Virtual environments
|
2
|
+
.venv
|
3
|
+
# IDE directories
|
4
|
+
.vscode/
|
5
|
+
.idea/
|
6
|
+
*.code-workspace
|
7
|
+
|
8
|
+
# Byte-compiled / optimized / DLL files
|
9
|
+
__pycache__/
|
10
|
+
*.py[cod]
|
11
|
+
*$py.class
|
12
|
+
|
13
|
+
# C extensions
|
14
|
+
*.so
|
15
|
+
|
16
|
+
# Distribution / packaging
|
17
|
+
.Python
|
18
|
+
build/
|
19
|
+
develop-eggs/
|
20
|
+
dist/
|
21
|
+
downloads/
|
22
|
+
eggs/
|
23
|
+
.eggs/
|
24
|
+
lib/
|
25
|
+
lib64/
|
26
|
+
parts/
|
27
|
+
sdist/
|
28
|
+
var/
|
29
|
+
wheels/
|
30
|
+
pip-wheel-metadata/
|
31
|
+
share/python-wheels/
|
32
|
+
*.egg-info/
|
33
|
+
.installed.cfg
|
34
|
+
*.egg
|
35
|
+
MANIFEST
|
36
|
+
|
37
|
+
# PyInstaller
|
38
|
+
# Usually these files are written by a python script from a template
|
39
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
40
|
+
*.manifest
|
41
|
+
*.spec
|
42
|
+
|
43
|
+
# Installer logs
|
44
|
+
pip-log.txt
|
45
|
+
pip-delete-this-directory.txt
|
46
|
+
|
47
|
+
# Unit test / coverage reports
|
48
|
+
htmlcov/
|
49
|
+
.tox/
|
50
|
+
.nox/
|
51
|
+
.coverage
|
52
|
+
.coverage.*
|
53
|
+
.cache
|
54
|
+
nosetests.xml
|
55
|
+
coverage.xml
|
56
|
+
*.cover
|
57
|
+
*.py,cover
|
58
|
+
.hypothesis/
|
59
|
+
.pytest_cache/
|
60
|
+
|
61
|
+
# Translations
|
62
|
+
*.mo
|
63
|
+
*.pot
|
64
|
+
|
65
|
+
# Django stuff:
|
66
|
+
*.log
|
67
|
+
local_settings.py
|
68
|
+
db.sqlite3
|
69
|
+
db.sqlite3-journal
|
70
|
+
|
71
|
+
# Flask stuff:
|
72
|
+
instance/
|
73
|
+
.webassets-cache
|
74
|
+
|
75
|
+
# Scrapy stuff:
|
76
|
+
.scrapy
|
77
|
+
|
78
|
+
# Sphinx documentation
|
79
|
+
docs/_build/
|
80
|
+
|
81
|
+
# PyBuilder
|
82
|
+
target/
|
83
|
+
|
84
|
+
# Jupyter Notebook
|
85
|
+
.ipynb_checkpoints
|
86
|
+
|
87
|
+
# IPython
|
88
|
+
profile_default/
|
89
|
+
ipython_config.py
|
90
|
+
|
91
|
+
# pyenv
|
92
|
+
.python-version
|
93
|
+
|
94
|
+
# pipenv
|
95
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
96
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
97
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
98
|
+
# install all needed dependencies.
|
99
|
+
#Pipfile.lock
|
100
|
+
|
101
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
102
|
+
__pypackages__/
|
103
|
+
|
104
|
+
# Celery stuff
|
105
|
+
celerybeat-schedule
|
106
|
+
celerybeat.pid
|
107
|
+
|
108
|
+
# SageMath parsed files
|
109
|
+
*.sage.py
|
110
|
+
|
111
|
+
# Environments
|
112
|
+
.env
|
113
|
+
.venv
|
114
|
+
env/
|
115
|
+
venv/
|
116
|
+
ENV/
|
117
|
+
env.bak/
|
118
|
+
venv.bak/
|
119
|
+
|
120
|
+
# Spyder project settings
|
121
|
+
.spyderproject
|
122
|
+
.spyproject
|
123
|
+
|
124
|
+
# Rope project settings
|
125
|
+
.ropeproject
|
126
|
+
|
127
|
+
# mkdocs documentation
|
128
|
+
/site
|
129
|
+
|
130
|
+
# mypy
|
131
|
+
.mypy_cache/
|
132
|
+
.dmypy.json
|
133
|
+
dmypy.json
|
134
|
+
|
135
|
+
# Pyre type checker
|
136
|
+
.pyre/
|
137
|
+
|
138
|
+
playground/
|
@@ -0,0 +1,20 @@
|
|
1
|
+
repos:
|
2
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
3
|
+
rev: v5.0.0
|
4
|
+
hooks:
|
5
|
+
- id: check-yaml
|
6
|
+
- id: end-of-file-fixer
|
7
|
+
- id: trailing-whitespace
|
8
|
+
- repo: https://github.com/psf/black
|
9
|
+
rev: "24.10.0"
|
10
|
+
hooks:
|
11
|
+
- id: black
|
12
|
+
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
13
|
+
# Ruff version.
|
14
|
+
rev: "v0.8.3"
|
15
|
+
hooks:
|
16
|
+
- id: ruff
|
17
|
+
- repo: https://github.com/pycqa/isort
|
18
|
+
rev: "5.13.2"
|
19
|
+
hooks:
|
20
|
+
- id: isort
|
quizzy-0.1.0/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2024 Jonas Ehrlich
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
quizzy-0.1.0/PKG-INFO
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: quizzy
|
3
|
+
Version: 0.1.0
|
4
|
+
Summary: A Python TUI quiz app
|
5
|
+
Author-email: Jonas Ehrlich <jonas.ehrlich@gmail.com>
|
6
|
+
License-Expression: MIT
|
7
|
+
License-File: LICENSE
|
8
|
+
Requires-Python: >=3.10
|
9
|
+
Requires-Dist: pydantic>=2.10.3
|
10
|
+
Requires-Dist: pyyaml>=6.0.2
|
11
|
+
Requires-Dist: textual>=1.0.0
|
12
|
+
Description-Content-Type: text/markdown
|
13
|
+
|
14
|
+
# Quizzy
|
15
|
+
|
16
|
+
A quiz app using [textual](https://textual.textualize.io/).
|
17
|
+
|
18
|
+

|
19
|
+
|
20
|
+
|
21
|
+
## Configuration and Questions
|
22
|
+
|
23
|
+
Create a YAML file to define the teams participating, the questions and their answers.
|
24
|
+
|
25
|
+
```yaml
|
26
|
+
teams:
|
27
|
+
- name: "Team 1"
|
28
|
+
- name: "Team 2"
|
29
|
+
categories:
|
30
|
+
- name: "General Knowledge"
|
31
|
+
questions:
|
32
|
+
- question: "What is the capital of France?"
|
33
|
+
answer: "Paris"
|
34
|
+
value: 100
|
35
|
+
- question: "What is the capital of Germany?"
|
36
|
+
answer: "Berlin"
|
37
|
+
value: 200
|
38
|
+
- name: "Science"
|
39
|
+
questions:
|
40
|
+
- question: "What is the chemical symbol for gold?"
|
41
|
+
answer: "Au"
|
42
|
+
value: 100
|
43
|
+
- question: "What is the chemical symbol for silver?"
|
44
|
+
answer: "Ag"
|
45
|
+
value: 200
|
46
|
+
```
|
47
|
+
|
48
|
+
See [examples/quizzy.yaml](examples/quizzy.yaml) for an example.
|
49
|
+
|
50
|
+
|
51
|
+
## Running it
|
52
|
+
|
53
|
+
Run normally using *uv*:
|
54
|
+
|
55
|
+
``` sh
|
56
|
+
uv run quizzy examples/quizzy.yaml
|
57
|
+
```
|
58
|
+
|
59
|
+
Serve using textual:
|
60
|
+
|
61
|
+
``` sh
|
62
|
+
uv run textual serve "uv run quizzy examples/quizzy.yaml"
|
63
|
+
```
|
64
|
+
|
65
|
+
Run in development mode:
|
66
|
+
|
67
|
+
``` sh
|
68
|
+
uv run textual run --dev quizzy.app:QuizzyApp examples/quizzy.yaml
|
69
|
+
```
|
quizzy-0.1.0/README.md
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
# Quizzy
|
2
|
+
|
3
|
+
A quiz app using [textual](https://textual.textualize.io/).
|
4
|
+
|
5
|
+

|
6
|
+
|
7
|
+
|
8
|
+
## Configuration and Questions
|
9
|
+
|
10
|
+
Create a YAML file to define the teams participating, the questions and their answers.
|
11
|
+
|
12
|
+
```yaml
|
13
|
+
teams:
|
14
|
+
- name: "Team 1"
|
15
|
+
- name: "Team 2"
|
16
|
+
categories:
|
17
|
+
- name: "General Knowledge"
|
18
|
+
questions:
|
19
|
+
- question: "What is the capital of France?"
|
20
|
+
answer: "Paris"
|
21
|
+
value: 100
|
22
|
+
- question: "What is the capital of Germany?"
|
23
|
+
answer: "Berlin"
|
24
|
+
value: 200
|
25
|
+
- name: "Science"
|
26
|
+
questions:
|
27
|
+
- question: "What is the chemical symbol for gold?"
|
28
|
+
answer: "Au"
|
29
|
+
value: 100
|
30
|
+
- question: "What is the chemical symbol for silver?"
|
31
|
+
answer: "Ag"
|
32
|
+
value: 200
|
33
|
+
```
|
34
|
+
|
35
|
+
See [examples/quizzy.yaml](examples/quizzy.yaml) for an example.
|
36
|
+
|
37
|
+
|
38
|
+
## Running it
|
39
|
+
|
40
|
+
Run normally using *uv*:
|
41
|
+
|
42
|
+
``` sh
|
43
|
+
uv run quizzy examples/quizzy.yaml
|
44
|
+
```
|
45
|
+
|
46
|
+
Serve using textual:
|
47
|
+
|
48
|
+
``` sh
|
49
|
+
uv run textual serve "uv run quizzy examples/quizzy.yaml"
|
50
|
+
```
|
51
|
+
|
52
|
+
Run in development mode:
|
53
|
+
|
54
|
+
``` sh
|
55
|
+
uv run textual run --dev quizzy.app:QuizzyApp examples/quizzy.yaml
|
56
|
+
```
|
Binary file
|
@@ -0,0 +1,21 @@
|
|
1
|
+
teams:
|
2
|
+
- name: "Team 1"
|
3
|
+
- name: "Team 2"
|
4
|
+
categories:
|
5
|
+
- name: "General Knowledge"
|
6
|
+
questions:
|
7
|
+
- question: "What is the capital of France?"
|
8
|
+
answer: "Paris"
|
9
|
+
value: 100
|
10
|
+
- question: "What is the capital of Germany?"
|
11
|
+
answer: "Berlin"
|
12
|
+
value: 200
|
13
|
+
- name: "Science"
|
14
|
+
questions:
|
15
|
+
- question: "What is the chemical symbol for gold?"
|
16
|
+
answer: "Au"
|
17
|
+
value: 100
|
18
|
+
- question: "What is the chemical symbol for silver?"
|
19
|
+
answer: "Ag"
|
20
|
+
value: 200
|
21
|
+
|
@@ -0,0 +1,55 @@
|
|
1
|
+
|
2
|
+
[project]
|
3
|
+
name = "quizzy"
|
4
|
+
version = "0.1.0"
|
5
|
+
description = "A Python TUI quiz app"
|
6
|
+
authors = [{ name = "Jonas Ehrlich", email = "jonas.ehrlich@gmail.com" }]
|
7
|
+
readme = "README.md"
|
8
|
+
requires-python = ">=3.10"
|
9
|
+
dependencies = ["pydantic>=2.10.3", "pyyaml>=6.0.2", "textual>=1.0.0"]
|
10
|
+
license = "MIT"
|
11
|
+
|
12
|
+
[project.scripts]
|
13
|
+
quizzy = "quizzy.__main__:main"
|
14
|
+
|
15
|
+
[build-system]
|
16
|
+
requires = ["hatchling"]
|
17
|
+
build-backend = "hatchling.build"
|
18
|
+
|
19
|
+
|
20
|
+
[dependency-groups]
|
21
|
+
dev = [
|
22
|
+
"black>=24.10.0",
|
23
|
+
"isort>=5.13.2",
|
24
|
+
"pytest>=8.3.4",
|
25
|
+
"pytest-sugar>=1.0.0",
|
26
|
+
"ruff>=0.8.3",
|
27
|
+
"textual-dev>=1.7.0",
|
28
|
+
]
|
29
|
+
|
30
|
+
[tool.isort]
|
31
|
+
profile = "black"
|
32
|
+
|
33
|
+
[tool.black]
|
34
|
+
line-length = 120
|
35
|
+
|
36
|
+
[tool.mypy]
|
37
|
+
strict = true
|
38
|
+
|
39
|
+
[tool.ruff]
|
40
|
+
line-length = 120
|
41
|
+
src = ["quizzy"]
|
42
|
+
|
43
|
+
[tool.ruff.lint]
|
44
|
+
select = [
|
45
|
+
"E", # pycodestyle error rules
|
46
|
+
"F", # pycodestyle warning rules
|
47
|
+
"B", # flake8-bugbear rules
|
48
|
+
"S", # flake8-bandit rules
|
49
|
+
"PTH", # flake8-use-pathlib
|
50
|
+
"PLC", # pylint convention rules
|
51
|
+
"PLR", # pylint refactor rules
|
52
|
+
"PLE", # pylint error rules
|
53
|
+
"PLW", # pylint warning rules
|
54
|
+
"C90", # mccabe complexity rules
|
55
|
+
]
|
@@ -0,0 +1,208 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import argparse
|
4
|
+
import pathlib
|
5
|
+
|
6
|
+
from textual import app, containers, log, message, reactive, screen, widgets
|
7
|
+
|
8
|
+
from quizzy import __version__, models
|
9
|
+
|
10
|
+
|
11
|
+
def get_arg_parser() -> argparse.ArgumentParser:
|
12
|
+
parser = argparse.ArgumentParser(prog=__name__.split(".")[0], description="A terminal quiz app")
|
13
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
14
|
+
parser.add_argument("quizfile", type=pathlib.Path, help="Quiz file")
|
15
|
+
return parser
|
16
|
+
|
17
|
+
|
18
|
+
class AnswerScreen(screen.ModalScreen[models.Team | None]):
|
19
|
+
NOONE_ANSWERED_ID = "__noone-answered"
|
20
|
+
|
21
|
+
def __init__(self, category: str, question: models.Question, teams: list[models.Team]) -> None:
|
22
|
+
super().__init__(classes="question-answer-screen")
|
23
|
+
self.category = category
|
24
|
+
self.question = question
|
25
|
+
self.teams = {team.id: team for team in teams}
|
26
|
+
self.border_title = f"{category} - {question.value} points"
|
27
|
+
|
28
|
+
def compose(self) -> app.ComposeResult:
|
29
|
+
question_widget = widgets.Static(self.question.question, id="question")
|
30
|
+
question_widget.border_title = "Question"
|
31
|
+
|
32
|
+
answer_widget = widgets.Static(self.question.answer, id="answer")
|
33
|
+
answer_widget.border_title = "Answer"
|
34
|
+
|
35
|
+
whoanswered = containers.HorizontalGroup(
|
36
|
+
*[
|
37
|
+
containers.Vertical(widgets.Button(team.name, id=team_id, variant="primary"))
|
38
|
+
for team_id, team in self.teams.items()
|
39
|
+
],
|
40
|
+
id="who-answered",
|
41
|
+
classes="horizontal-100",
|
42
|
+
)
|
43
|
+
whoanswered.border_title = "Who Answered Correctly?"
|
44
|
+
|
45
|
+
container = containers.Grid(
|
46
|
+
question_widget,
|
47
|
+
answer_widget,
|
48
|
+
whoanswered,
|
49
|
+
containers.Horizontal(
|
50
|
+
widgets.Button(
|
51
|
+
"😭 No one answered correctly 😭", id=self.NOONE_ANSWERED_ID, variant="error", classes="button-100"
|
52
|
+
),
|
53
|
+
classes="horizontal-100",
|
54
|
+
),
|
55
|
+
classes="question-answer-dialog",
|
56
|
+
id="dialog",
|
57
|
+
)
|
58
|
+
|
59
|
+
container.border_title = f"{self.category} - {self.question.value} points"
|
60
|
+
yield container
|
61
|
+
|
62
|
+
def on_button_pressed(self, event: widgets.Button.Pressed) -> None:
|
63
|
+
if event.button.id == self.NOONE_ANSWERED_ID:
|
64
|
+
self.dismiss(None)
|
65
|
+
elif event.button.id in self.teams:
|
66
|
+
team = self.teams[event.button.id]
|
67
|
+
self.dismiss(team)
|
68
|
+
|
69
|
+
|
70
|
+
class QuestionScreen(screen.ModalScreen[models.Team | None]):
|
71
|
+
SHOW_ANSWER_ID = "show-answer"
|
72
|
+
|
73
|
+
def __init__(self, category: str, question: models.Question, teams: list[models.Team]) -> None:
|
74
|
+
super().__init__(classes="question-answer-screen")
|
75
|
+
self.category = category
|
76
|
+
self.question = question
|
77
|
+
self.teams = teams
|
78
|
+
|
79
|
+
def compose(self) -> app.ComposeResult:
|
80
|
+
question_widget = widgets.Static(self.question.question, id="question")
|
81
|
+
question_widget.border_title = "Question"
|
82
|
+
|
83
|
+
container = containers.Grid(
|
84
|
+
question_widget,
|
85
|
+
containers.Horizontal(
|
86
|
+
widgets.Button("Show Answer", id=self.SHOW_ANSWER_ID, variant="primary", classes="button-100"),
|
87
|
+
classes="horizontal-100",
|
88
|
+
),
|
89
|
+
classes="question-answer-dialog",
|
90
|
+
id="dialog",
|
91
|
+
)
|
92
|
+
|
93
|
+
container.border_title = f"{self.category} - {self.question.value} points"
|
94
|
+
yield container
|
95
|
+
|
96
|
+
def on_button_pressed(self, event: widgets.Button.Pressed) -> None:
|
97
|
+
def dismiss(team: models.Team | None) -> None:
|
98
|
+
self.dismiss(team)
|
99
|
+
|
100
|
+
if event.button.id == self.SHOW_ANSWER_ID:
|
101
|
+
self.app.push_screen(AnswerScreen(self.category, self.question, self.teams), dismiss)
|
102
|
+
|
103
|
+
|
104
|
+
class TeamScore(containers.Vertical):
|
105
|
+
score = reactive.reactive(0, recompose=True)
|
106
|
+
|
107
|
+
def __init__(self, team: models.Team) -> None:
|
108
|
+
self.team = team
|
109
|
+
super().__init__()
|
110
|
+
self.border_title = self.team.name
|
111
|
+
self.score = team.score
|
112
|
+
|
113
|
+
def compose(self) -> app.ComposeResult:
|
114
|
+
yield widgets.Static(str(self.score))
|
115
|
+
|
116
|
+
def watch_score(self, score: int) -> None:
|
117
|
+
"""
|
118
|
+
Watch the reactive score and update the team's score in the data object. This allows dumping the quiz state
|
119
|
+
to YAML.
|
120
|
+
"""
|
121
|
+
self.team.score = score
|
122
|
+
|
123
|
+
|
124
|
+
class Scoreboard(containers.Vertical):
|
125
|
+
def __init__(self, teams: list[models.Team]) -> None:
|
126
|
+
super().__init__()
|
127
|
+
self.teams = teams
|
128
|
+
self.team_score_widgets = {team.id: TeamScore(team) for team in teams}
|
129
|
+
|
130
|
+
def compose(self) -> app.ComposeResult:
|
131
|
+
yield containers.HorizontalGroup(*self.team_score_widgets.values())
|
132
|
+
|
133
|
+
def update_team_score(self, team_id: str, value: int) -> None:
|
134
|
+
log(f"scoreboard: Updating team score {team_id} to {value}")
|
135
|
+
self.team_score_widgets[team_id].score += value
|
136
|
+
|
137
|
+
|
138
|
+
class QuestionButton(widgets.Button):
|
139
|
+
class Answered(message.Message):
|
140
|
+
def __init__(self, team: models.Team, question_value: int) -> None:
|
141
|
+
self.team = team
|
142
|
+
self.value = question_value
|
143
|
+
super().__init__()
|
144
|
+
|
145
|
+
def __init__(self, category: str, question: models.Question, teams: list[models.Team]) -> None:
|
146
|
+
self.question = question
|
147
|
+
self.teams = teams
|
148
|
+
self.category = category
|
149
|
+
super().__init__(str(question.value), variant="warning", classes="button-100")
|
150
|
+
# Disable the button if the question has already been answered on init. This might be useful when starting with
|
151
|
+
# a state
|
152
|
+
self.disabled = question.answered
|
153
|
+
|
154
|
+
def on_click(self) -> None:
|
155
|
+
# First, disable the button to prevent multiple clicks
|
156
|
+
self.disabled = True
|
157
|
+
self.question.answered = True
|
158
|
+
|
159
|
+
def wait_for_result(team: models.Team | None) -> None:
|
160
|
+
if team is None:
|
161
|
+
log("question-button: No-one answered the question")
|
162
|
+
else:
|
163
|
+
log(f"question-button: {team.id} answered the question")
|
164
|
+
self.post_message(self.Answered(team, self.question.value))
|
165
|
+
|
166
|
+
self.app.push_screen(QuestionScreen(self.category, self.question, self.teams), wait_for_result)
|
167
|
+
|
168
|
+
|
169
|
+
class CategoryColumn(containers.VerticalGroup):
|
170
|
+
def __init__(self, category: models.Category, teams: list[models.Team]) -> None:
|
171
|
+
self.category = category
|
172
|
+
self.teams = teams
|
173
|
+
super().__init__()
|
174
|
+
|
175
|
+
def compose(self) -> app.ComposeResult:
|
176
|
+
yield widgets.Static(self.category.name)
|
177
|
+
for question in self.category.questions:
|
178
|
+
yield QuestionButton(self.category.name, question, self.teams)
|
179
|
+
|
180
|
+
|
181
|
+
class QuestionBoard(containers.HorizontalGroup):
|
182
|
+
def __init__(self, config: models.Config) -> None:
|
183
|
+
self.config = config
|
184
|
+
super().__init__()
|
185
|
+
|
186
|
+
def compose(self) -> app.ComposeResult:
|
187
|
+
for category in self.config.categories:
|
188
|
+
yield CategoryColumn(category, self.config.teams)
|
189
|
+
|
190
|
+
|
191
|
+
class QuizzyApp(app.App[None]):
|
192
|
+
CSS_PATH = "quizzy.tcss"
|
193
|
+
|
194
|
+
def __init__(self) -> None:
|
195
|
+
super().__init__()
|
196
|
+
parser = get_arg_parser()
|
197
|
+
namespace = parser.parse_args()
|
198
|
+
|
199
|
+
self.config = models.load_config(namespace.quizfile)
|
200
|
+
self.scoreboard_widget = Scoreboard(self.config.teams)
|
201
|
+
|
202
|
+
def compose(self) -> app.ComposeResult:
|
203
|
+
yield widgets.Header()
|
204
|
+
yield widgets.Footer()
|
205
|
+
yield containers.Grid(QuestionBoard(self.config), self.scoreboard_widget, id="app-grid")
|
206
|
+
|
207
|
+
def on_question_button_answered(self, event: QuestionButton.Answered) -> None:
|
208
|
+
self.scoreboard_widget.update_team_score(event.team.id, event.value)
|