diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 2c1f402d..b47f5be7 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -61,3 +61,29 @@ jobs:
run: uv venv && uv pip install .[test]
- name: Test with pytest
run: source .venv/bin/activate && pytest
+ docs:
+ needs: [build]
+ runs-on: ubuntu-latest
+ continue-on-error: true
+ steps:
+ - uses: actions/checkout@v4
+ - name: Install uv
+ run: pipx install uv
+ - name: Install graphviz
+ run: sudo apt install -y graphviz
+ - name: Set up Python 3.11
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.11'
+ cache: "pip"
+ cache-dependency-path: pyproject.toml
+ - name: Install dependencies
+ run: uv venv && uv pip install .[test]
+ - name: Render Aiogram-dialogs transitions
+ run: source .venv/bin/activate && python -m shvatka.tgbot.dialogs.__init__
+ - name: Upload artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: transitions
+ path: out/shvatka-dialogs.png
+
diff --git a/.gitignore b/.gitignore
index 259335a3..6b1453d4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -150,6 +150,7 @@ dmypy.json
/config/
/files/
/local-storage/
+/out/
#pyro stuff
*.session
diff --git a/pyproject.toml b/pyproject.toml
index a360ebc1..9ccc2c27 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -62,6 +62,7 @@ test = [
"mypy>=1.1.1,<2.0",
"aiogram-tests @ git+https://github.com/bomzheg/aiogram_tests.git@fix/aiogram3rc",
"asgi-lifespan>=2.1.0,<3.0",
+ "aiogram_dialog[tools]>=2.1,<2.2",
]
[project.scripts]
diff --git a/shvatka/api/models/responses.py b/shvatka/api/models/responses.py
index 56af0342..6fe97975 100644
--- a/shvatka/api/models/responses.py
+++ b/shvatka/api/models/responses.py
@@ -3,12 +3,15 @@
from datetime import datetime
from typing import Sequence, Generic
+from adaptix import Retort, dumper
+
from shvatka.core.games.dto import CurrentHints
from shvatka.core.models import dto, enums
from shvatka.core.models.dto import scn
from shvatka.core.models.enums import GameStatus
T = typing.TypeVar("T")
+retort = Retort(recipe=[dumper(scn.HintsList, lambda x: x.hints)])
@dataclass
@@ -76,7 +79,7 @@ class Level:
db_id: int
name_id: str
author: Player
- scenario: scn.LevelScenario
+ scenario: dict[str, typing.Any]
game_id: int | None = None
number_in_game: int | None = None
@@ -88,7 +91,7 @@ def from_core(cls, core: dto.Level | None = None):
db_id=core.db_id,
name_id=core.name_id,
author=Player.from_core(core.author),
- scenario=core.scenario,
+ scenario=retort.dump(core.scenario),
game_id=core.game_id,
number_in_game=core.number_in_game,
)
diff --git a/shvatka/common/data_examples.py b/shvatka/common/data_examples.py
index 17122fdb..c0463f17 100644
--- a/shvatka/common/data_examples.py
+++ b/shvatka/common/data_examples.py
@@ -42,56 +42,58 @@
scenario=scn.LevelScenario(
id="level_100",
keys={"SH1"},
- time_hints=[
- scn.TimeHint(
- time=0,
- hint=[
- scn.TextHint(
- text="level_100_0",
- ),
- ],
- ),
- scn.TimeHint(
- time=10,
- hint=[
- scn.TextHint(
- text="level_100_10",
- ),
- ],
- ),
- scn.TimeHint(
- time=20,
- hint=[
- scn.TextHint(
- text="level_100_20",
- ),
- ],
- ),
- scn.TimeHint(
- time=30,
- hint=[
- scn.TextHint(
- text="level_100_20",
- ),
- ],
- ),
- scn.TimeHint(
- time=40,
- hint=[
- scn.TextHint(
- text="level_100_20",
- ),
- ],
- ),
- scn.TimeHint(
- time=60,
- hint=[
- scn.TextHint(
- text="level_100_20",
- ),
- ],
- ),
- ],
+ time_hints=scn.HintsList(
+ [
+ scn.TimeHint(
+ time=0,
+ hint=[
+ scn.TextHint(
+ text="level_100_0",
+ ),
+ ],
+ ),
+ scn.TimeHint(
+ time=10,
+ hint=[
+ scn.TextHint(
+ text="level_100_10",
+ ),
+ ],
+ ),
+ scn.TimeHint(
+ time=20,
+ hint=[
+ scn.TextHint(
+ text="level_100_20",
+ ),
+ ],
+ ),
+ scn.TimeHint(
+ time=30,
+ hint=[
+ scn.TextHint(
+ text="level_100_20",
+ ),
+ ],
+ ),
+ scn.TimeHint(
+ time=40,
+ hint=[
+ scn.TextHint(
+ text="level_100_20",
+ ),
+ ],
+ ),
+ scn.TimeHint(
+ time=60,
+ hint=[
+ scn.TextHint(
+ text="level_100_20",
+ ),
+ ],
+ ),
+ ]
+ ),
),
),
dto.Level(
@@ -103,56 +105,58 @@
scenario=scn.LevelScenario(
id="level_101",
keys={"SH2"},
- time_hints=[
- scn.TimeHint(
- time=0,
- hint=[
- scn.TextHint(
- text="level_101_0",
- ),
- ],
- ),
- scn.TimeHint(
- time=10,
- hint=[
- scn.TextHint(
- text="level_101_10",
- ),
- ],
- ),
- scn.TimeHint(
- time=20,
- hint=[
- scn.TextHint(
- text="level_101_20",
- ),
- ],
- ),
- scn.TimeHint(
- time=30,
- hint=[
- scn.TextHint(
- text="level_101_20",
- ),
- ],
- ),
- scn.TimeHint(
- time=40,
- hint=[
- scn.TextHint(
- text="level_101_20",
- ),
- ],
- ),
- scn.TimeHint(
- time=60,
- hint=[
- scn.TextHint(
- text="level_101_20",
- ),
- ],
- ),
- ],
+ time_hints=scn.HintsList(
+ [
+ scn.TimeHint(
+ time=0,
+ hint=[
+ scn.TextHint(
+ text="level_101_0",
+ ),
+ ],
+ ),
+ scn.TimeHint(
+ time=10,
+ hint=[
+ scn.TextHint(
+ text="level_101_10",
+ ),
+ ],
+ ),
+ scn.TimeHint(
+ time=20,
+ hint=[
+ scn.TextHint(
+ text="level_101_20",
+ ),
+ ],
+ ),
+ scn.TimeHint(
+ time=30,
+ hint=[
+ scn.TextHint(
+ text="level_101_20",
+ ),
+ ],
+ ),
+ scn.TimeHint(
+ time=40,
+ hint=[
+ scn.TextHint(
+ text="level_101_20",
+ ),
+ ],
+ ),
+ scn.TimeHint(
+ time=60,
+ hint=[
+ scn.TextHint(
+ text="level_101_20",
+ ),
+ ],
+ ),
+ ]
+ ),
),
),
dto.Level(
@@ -164,56 +168,58 @@
scenario=scn.LevelScenario(
id="level_102",
keys={"SH3"},
- time_hints=[
- scn.TimeHint(
- time=0,
- hint=[
- scn.TextHint(
- text="level_102_0",
- ),
- ],
- ),
- scn.TimeHint(
- time=10,
- hint=[
- scn.TextHint(
- text="level_102_10",
- ),
- ],
- ),
- scn.TimeHint(
- time=20,
- hint=[
- scn.TextHint(
- text="level_102_20",
- ),
- ],
- ),
- scn.TimeHint(
- time=30,
- hint=[
- scn.TextHint(
- text="level_102_20",
- ),
- ],
- ),
- scn.TimeHint(
- time=40,
- hint=[
- scn.TextHint(
- text="level_102_20",
- ),
- ],
- ),
- scn.TimeHint(
- time=60,
- hint=[
- scn.TextHint(
- text="level_102_20",
- ),
- ],
- ),
- ],
+ time_hints=scn.HintsList(
+ [
+ scn.TimeHint(
+ time=0,
+ hint=[
+ scn.TextHint(
+ text="level_102_0",
+ ),
+ ],
+ ),
+ scn.TimeHint(
+ time=10,
+ hint=[
+ scn.TextHint(
+ text="level_102_10",
+ ),
+ ],
+ ),
+ scn.TimeHint(
+ time=20,
+ hint=[
+ scn.TextHint(
+ text="level_102_20",
+ ),
+ ],
+ ),
+ scn.TimeHint(
+ time=30,
+ hint=[
+ scn.TextHint(
+ text="level_102_20",
+ ),
+ ],
+ ),
+ scn.TimeHint(
+ time=40,
+ hint=[
+ scn.TextHint(
+ text="level_102_20",
+ ),
+ ],
+ ),
+ scn.TimeHint(
+ time=60,
+ hint=[
+ scn.TextHint(
+ text="level_102_20",
+ ),
+ ],
+ ),
+ ]
+ ),
),
),
dto.Level(
@@ -225,56 +231,58 @@
scenario=scn.LevelScenario(
id="level_103",
keys={"SH4"},
- time_hints=[
- scn.TimeHint(
- time=0,
- hint=[
- scn.TextHint(
- text="level_103_0",
- ),
- ],
- ),
- scn.TimeHint(
- time=10,
- hint=[
- scn.TextHint(
- text="level_103_10",
- ),
- ],
- ),
- scn.TimeHint(
- time=20,
- hint=[
- scn.TextHint(
- text="level_103_20",
- ),
- ],
- ),
- scn.TimeHint(
- time=30,
- hint=[
- scn.TextHint(
- text="level_103_20",
- ),
- ],
- ),
- scn.TimeHint(
- time=40,
- hint=[
- scn.TextHint(
- text="level_103_20",
- ),
- ],
- ),
- scn.TimeHint(
- time=60,
- hint=[
- scn.TextHint(
- text="level_103_20",
- ),
- ],
- ),
- ],
+ time_hints=scn.HintsList(
+ [
+ scn.TimeHint(
+ time=0,
+ hint=[
+ scn.TextHint(
+ text="level_103_0",
+ ),
+ ],
+ ),
+ scn.TimeHint(
+ time=10,
+ hint=[
+ scn.TextHint(
+ text="level_103_10",
+ ),
+ ],
+ ),
+ scn.TimeHint(
+ time=20,
+ hint=[
+ scn.TextHint(
+ text="level_103_20",
+ ),
+ ],
+ ),
+ scn.TimeHint(
+ time=30,
+ hint=[
+ scn.TextHint(
+ text="level_103_20",
+ ),
+ ],
+ ),
+ scn.TimeHint(
+ time=40,
+ hint=[
+ scn.TextHint(
+ text="level_103_20",
+ ),
+ ],
+ ),
+ scn.TimeHint(
+ time=60,
+ hint=[
+ scn.TextHint(
+ text="level_103_20",
+ ),
+ ],
+ ),
+ ]
+ ),
),
),
],
diff --git a/shvatka/common/factory.py b/shvatka/common/factory.py
index 73caea12..4ecc19d1 100644
--- a/shvatka/common/factory.py
+++ b/shvatka/common/factory.py
@@ -1,10 +1,29 @@
+import typing
+
+import adaptix
import dataclass_factory
+from adaptix import (
+ Retort,
+ validator,
+ P,
+ name_mapping,
+ loader,
+ Chain,
+ dumper,
+)
+from adaptix.load_error import LoadError
+from adaptix._internal.morphing.provider_template import ABCProxy
from dataclass_factory import Schema, NameStyle
from dishka import Provider, Scope, provide
from telegraph.aio import Telegraph
from shvatka.common.url_factory import UrlFactory
+from shvatka.core.models.dto import scn
+from shvatka.core.models.dto.scn import HintsList, TimeHint
from shvatka.core.models.schems import schemas
+from shvatka.core.utils import exceptions
+from shvatka.core.utils.input_validation import validate_level_id, is_multiple_keys_normal
+from shvatka.core.views.texts import INVALID_KEY_ERROR
from shvatka.tgbot.config.models.bot import BotConfig
@@ -17,6 +36,13 @@ def create_telegraph(self, bot_config: BotConfig) -> Telegraph:
return telegraph
+REQUIRED_GAME_RECIPES = [
+ loader(HintsList, lambda x: HintsList(x), Chain.LAST),
+ ABCProxy(HintsList, list[TimeHint]), # internal class, can be broken in next version adaptix
+ dumper(set, lambda x: tuple(x)),
+]
+
+
class DCFProvider(Provider):
scope = Scope.APP
@@ -28,6 +54,38 @@ def create_dataclass_factory(self) -> dataclass_factory.Factory:
)
return dcf
+ @provide
+ def create_retort(self) -> Retort:
+ retort = Retort(
+ recipe=[
+ name_mapping(
+ name_style=adaptix.NameStyle.LOWER_KEBAB,
+ ),
+ *REQUIRED_GAME_RECIPES,
+ validator(
+ pred=P[scn.LevelScenario].id,
+ func=lambda x: validate_level_id(x) is not None,
+ error=lambda x: typing.cast(
+ LoadError,
+ exceptions.ScenarioNotCorrect(
+ name_id=x, text=f"name_id ({x}) not correct"
+ ),
+ ),
+ ),
+ validator(
+ pred=P[scn.LevelScenario].keys,
+ func=is_multiple_keys_normal,
+ error=lambda x: typing.cast(
+ LoadError,
+ exceptions.ScenarioNotCorrect(
+ notify_user=INVALID_KEY_ERROR, text="invalid keys"
+ ),
+ ),
+ ),
+ ]
+ )
+ return retort
+
class UrlProvider(Provider):
scope = Scope.APP
diff --git a/shvatka/core/models/dto/scn/__init__.py b/shvatka/core/models/dto/scn/__init__.py
index bbd629bd..bc3bd059 100644
--- a/shvatka/core/models/dto/scn/__init__.py
+++ b/shvatka/core/models/dto/scn/__init__.py
@@ -25,6 +25,6 @@
PhotoHint,
ContactHint,
)
-from .level import LevelScenario, SHKey, BonusKey
+from .level import LevelScenario, SHKey, BonusKey, HintsList
from .parsed_zip import ParsedZip
from .time_hint import TimeHint
diff --git a/shvatka/core/models/dto/scn/level.py b/shvatka/core/models/dto/scn/level.py
index 7133b75b..30e9f4a4 100644
--- a/shvatka/core/models/dto/scn/level.py
+++ b/shvatka/core/models/dto/scn/level.py
@@ -1,7 +1,13 @@
import typing
+from collections.abc import Sequence
from dataclasses import dataclass, field
from datetime import timedelta
+from typing import overload
+from mypy.server.objgraph import Iterable
+
+from shvatka.core.utils import exceptions
+from .hint_part import AnyHint
from .time_hint import TimeHint, EnumeratedTimeHint
SHKey: typing.TypeAlias = str
@@ -21,10 +27,98 @@ def __hash__(self) -> int:
return hash(self.text)
+class HintsList(Sequence[TimeHint]):
+ def __init__(self, hints: list[TimeHint]):
+ self.verify(hints)
+ self.hints = hints
+
+ @classmethod
+ def parse(cls, hints: list[TimeHint]):
+ return cls(cls.normalize(hints))
+
+ @staticmethod
+ def normalize(hints: list[TimeHint]) -> list[TimeHint]:
+ hint_map: dict[int, list[AnyHint]] = {}
+ for hint in hints:
+ if not hint.hint:
+ continue
+ hint_map.setdefault(hint.time, []).extend(hint.hint)
+ return [TimeHint(k, v) for k, v in sorted(hint_map.items(), key=lambda x: x[0])]
+
+ @staticmethod
+ def verify(hints: Iterable[TimeHint]) -> None:
+ times: set[int] = set()
+ for hint in hints:
+ if hint.time in times:
+ raise exceptions.LevelError(
+ text=f"Contains multiple times hints for time {hint.time}"
+ )
+ times.add(hint.time)
+ if not hint.hint:
+ raise exceptions.LevelError(text=f"There is no hint for time {hint.time}")
+
+ def get_hint_by_time(self, time: timedelta) -> EnumeratedTimeHint:
+ hint = self.hints[0]
+ number = 0
+ for i, h in enumerate(self.hints):
+ if timedelta(minutes=h.time) < time:
+ hint = h
+ number = i
+ else:
+ break
+ return EnumeratedTimeHint(time=hint.time, hint=hint.hint, number=number)
+
+ def get_hints_for_timedelta(self, delta: timedelta) -> list[TimeHint]:
+ minutes = delta.total_seconds() // 60
+ return [th for th in self.hints if th.time <= minutes]
+
+ def replace(self, old: TimeHint, new: TimeHint) -> "HintsList":
+ for i, hint in enumerate(self.hints):
+ if hint.time == old.time:
+ old_index = i
+ break
+ else:
+ old_index = None
+ if old_index is None:
+ raise exceptions.LevelError(
+ text=f"can't replace, there is no hints for time {old.time}"
+ )
+ result = self.hints[0:old_index] + self.hints[old_index + 1 :] + [new]
+ return self.__class__(self.normalize(result))
+
+ @property
+ def hints_count(self) -> int:
+ return sum(time_hint.hints_count for time_hint in self.hints)
+
+ @overload
+ def __getitem__(self, index: int) -> TimeHint:
+ return self.hints[index]
+
+ @overload
+ def __getitem__(self, index: slice) -> Sequence[TimeHint]:
+ return self.hints[index]
+
+ def __getitem__(self, index):
+ return self.hints[index]
+
+ def __len__(self):
+ return len(self.hints)
+
+ def __eq__(self, other):
+ if isinstance(other, HintsList):
+ return self.hints == other.hints
+ if isinstance(other, list):
+ return self.hints == other
+ return NotImplemented
+
+ def __repr__(self):
+ return repr(self.hints)
+
+
@dataclass
class LevelScenario:
id: str
- time_hints: list[TimeHint]
+ time_hints: HintsList
keys: set[SHKey] = field(default_factory=set)
bonus_keys: set[BonusKey] = field(default_factory=set)
@@ -32,15 +126,7 @@ def get_hint(self, hint_number: int) -> TimeHint:
return self.time_hints[hint_number]
def get_hint_by_time(self, time: timedelta) -> EnumeratedTimeHint:
- hint = self.time_hints[0]
- number = 0
- for i, h in enumerate(self.time_hints):
- if timedelta(minutes=h.time) < time:
- hint = h
- number = i
- else:
- break
- return EnumeratedTimeHint(time=hint.time, hint=hint.hint, number=number)
+ return self.time_hints.get_hint_by_time(time)
def is_last_hint(self, hint_number: int) -> bool:
return len(self.time_hints) == hint_number + 1
@@ -59,8 +145,7 @@ def get_guids(self) -> list[str]:
@property
def hints_count(self) -> int:
- return sum(time_hint.hints_count for time_hint in self.time_hints)
+ return self.time_hints.hints_count
def get_hints_for_timedelta(self, delta: timedelta) -> list[TimeHint]:
- minutes = delta.total_seconds() // 60
- return [th for th in self.time_hints if th.time <= minutes]
+ return self.time_hints.get_hints_for_timedelta(delta)
diff --git a/shvatka/core/models/dto/scn/time_hint.py b/shvatka/core/models/dto/scn/time_hint.py
index 50ce427e..b6bf80d2 100644
--- a/shvatka/core/models/dto/scn/time_hint.py
+++ b/shvatka/core/models/dto/scn/time_hint.py
@@ -1,5 +1,6 @@
from dataclasses import dataclass
+from shvatka.core.utils import exceptions
from .hint_part import AnyHint
@@ -8,6 +9,9 @@ class TimeHint:
time: int
hint: list[AnyHint]
+ def __post_init__(self):
+ _check_hint_not_empty(self.hint)
+
def get_guids(self) -> list[str]:
guids = []
for hint in self.hint:
@@ -18,7 +22,28 @@ def get_guids(self) -> list[str]:
def hints_count(self) -> int:
return len(self.hint)
+ def update_time(self, new_time: int) -> None:
+ if not self.can_update_time():
+ raise exceptions.LevelError(text="Невозможно отредактировать загадку уровня")
+ if new_time == 0:
+ raise exceptions.LevelError(text="Нельзя заменить таким способом загадку уровня")
+ self.time = new_time
+
+ def update_hint(self, new_hint: list[AnyHint]) -> None:
+ _check_hint_not_empty(new_hint)
+ self.hint = new_hint
+
+ def can_update_time(self) -> bool:
+ return self.time != 0
+
@dataclass
class EnumeratedTimeHint(TimeHint):
number: int
+
+
+def _check_hint_not_empty(new_hint: list[AnyHint]) -> None:
+ if not new_hint:
+ raise exceptions.LevelError(
+ text="Нельзя установить пустой список подсказок. Вероятно нужно было удалить?"
+ )
diff --git a/shvatka/core/services/game.py b/shvatka/core/services/game.py
index 03542209..3c7eb7a9 100644
--- a/shvatka/core/services/game.py
+++ b/shvatka/core/services/game.py
@@ -1,6 +1,6 @@
from datetime import datetime
-from dataclass_factory import Factory
+from adaptix import Retort
from shvatka.core.interfaces.clients.file_storage import FileGateway
from shvatka.core.interfaces.dal.complex import GameCompleter, GamePackager
@@ -37,11 +37,11 @@ async def upsert_game(
raw_scn: scn.RawGameScenario,
author: dto.Player,
dao: GameUpserter,
- dcf: Factory,
+ retort: Retort,
file_gateway: FileGateway,
) -> dto.FullGame:
check_allow_be_author(author)
- game_scn = parse_uploaded_game(raw_scn, dcf)
+ game_scn = parse_uploaded_game(raw_scn, retort)
if not await dao.is_name_available(name=game_scn.name):
if not await dao.is_author_game_by_name(name=game_scn.name, author=author):
raise CantEditGame(
@@ -121,7 +121,7 @@ async def get_game_package(
id_: int,
author: dto.Player,
dao: GamePackager,
- dcf: Factory,
+ retort: Retort,
file_gateway: FileGateway,
) -> scn.RawGameScenario:
game = await dao.get_full(id_=id_)
@@ -151,7 +151,9 @@ async def get_game_package(
)
else:
game_stat = None
- return scn.RawGameScenario(scn=dcf.dump(scenario), files=contents, stat=dcf.dump(game_stat))
+ return scn.RawGameScenario(
+ scn=retort.dump(scenario), files=contents, stat=retort.dump(game_stat)
+ )
async def get_active(dao: ActiveGameFinder) -> dto.Game | None:
diff --git a/shvatka/core/services/level.py b/shvatka/core/services/level.py
index 71b75cd7..edaa11ff 100644
--- a/shvatka/core/services/level.py
+++ b/shvatka/core/services/level.py
@@ -1,4 +1,4 @@
-from dataclass_factory import Factory
+from adaptix import Retort
from shvatka.core.interfaces.dal.level import (
LevelUpserter,
@@ -15,10 +15,10 @@
async def upsert_raw_level(
- level_data: dict, author: dto.Player, dcf: Factory, dao: LevelUpserter
+ level_data: dict, author: dto.Player, retort: Retort, dao: LevelUpserter
) -> dto.Level:
check_allow_be_author(author)
- scenario = load_level(level_data, dcf)
+ scenario = load_level(level_data, retort)
return await upsert_level(author, scenario, dao)
diff --git a/shvatka/core/services/scenario/game_ops.py b/shvatka/core/services/scenario/game_ops.py
index e045d18d..b77d4778 100644
--- a/shvatka/core/services/scenario/game_ops.py
+++ b/shvatka/core/services/scenario/game_ops.py
@@ -1,4 +1,4 @@
-from dataclass_factory import Factory
+from adaptix import Retort
from shvatka.core.models.dto import scn
from shvatka.core.services.scenario.level_ops import (
@@ -6,16 +6,16 @@
)
-def parse_game(game: scn.RawGameScenario, dcf: Factory) -> scn.GameScenario:
- return dcf.load(game.scn, scn.GameScenario)
+def parse_game(game: scn.RawGameScenario, retort: Retort) -> scn.GameScenario:
+ return retort.load(game.scn, scn.GameScenario)
-def parse_uploaded_game(game: scn.RawGameScenario, dcf: Factory) -> scn.UploadedGameScenario:
- return dcf.load(game.scn, scn.UploadedGameScenario)
+def parse_uploaded_game(game: scn.RawGameScenario, retort: Retort) -> scn.UploadedGameScenario:
+ return retort.load(game.scn, scn.UploadedGameScenario)
-def serialize(game: scn.FullGameScenario, dcf: Factory) -> dict:
- return dcf.dump(game)
+def serialize(game: scn.FullGameScenario, retort: Retort) -> dict:
+ return retort.dump(game)
def check_all_files_saved(game: scn.GameScenario, guids: set[str]):
diff --git a/shvatka/core/services/scenario/level_ops.py b/shvatka/core/services/scenario/level_ops.py
index 57651d9d..b4363586 100644
--- a/shvatka/core/services/scenario/level_ops.py
+++ b/shvatka/core/services/scenario/level_ops.py
@@ -1,14 +1,14 @@
-import dataclass_factory
-from dataclass_factory import Factory
+from adaptix import Retort
+from adaptix.load_error import LoadError
from shvatka.core.models.dto import scn
from shvatka.core.utils import exceptions
-def load_level(dct: dict, dcf: Factory) -> scn.LevelScenario:
+def load_level(dct: dict, retort: Retort) -> scn.LevelScenario:
try:
- return dcf.load(dct, scn.LevelScenario)
- except dataclass_factory.PARSER_EXCEPTIONS as e:
+ return retort.load(dct, scn.LevelScenario)
+ except LoadError as e:
raise exceptions.ScenarioNotCorrect(notify_user="невалидный уровень") from e
diff --git a/shvatka/core/utils/exceptions.py b/shvatka/core/utils/exceptions.py
index 48bc27e6..02b26486 100644
--- a/shvatka/core/utils/exceptions.py
+++ b/shvatka/core/utils/exceptions.py
@@ -1,6 +1,8 @@
+import typing
from typing import Any
-from shvatka.core.models import dto
+if typing.TYPE_CHECKING:
+ from shvatka.core.models import dto
class SHError(Exception):
@@ -14,10 +16,10 @@ def __init__(
chat_id: int | None = None,
team_id: int | None = None,
game_id: int | None = None,
- user: dto.User | None = None,
- player: dto.Player | None = None,
- chat: dto.Chat | None = None,
- team: dto.Team | None = None,
+ user: "dto.User | None" = None,
+ player: "dto.Player | None" = None,
+ chat: "dto.Chat | None" = None,
+ team: "dto.Team | None" = None,
game: Any | None = None,
alarm: bool | None = False,
notify_user: str | None = None,
@@ -163,7 +165,7 @@ class GameNotFound(GameError, AttributeError):
class LevelError(SHError):
notify_user = "Ошибка связанная с уровнем"
- def __init__(self, level_id: int, *args, **kwargs) -> None:
+ def __init__(self, level_id: int | None = None, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.level_id = level_id
diff --git a/shvatka/infrastructure/crawler/game_scn/loader/load_scns.py b/shvatka/infrastructure/crawler/game_scn/loader/load_scns.py
index 2b5b7cc1..e0206bc1 100644
--- a/shvatka/infrastructure/crawler/game_scn/loader/load_scns.py
+++ b/shvatka/infrastructure/crawler/game_scn/loader/load_scns.py
@@ -6,7 +6,7 @@
from typing import BinaryIO, Any, Callable, Coroutine
from zipfile import Path as ZipPath
-from dataclass_factory import Factory
+from adaptix import Retort
from dishka import make_async_container
from shvatka.common.config.parser.logging_config import setup_logging
@@ -50,7 +50,7 @@ async def main():
bot_player=bot_player,
dao=dao,
file_gateway=file_gateway,
- dcf=await dishka.get(Factory),
+ retort=await dishka.get(Retort),
path=config.file_storage_config.path.parent / "scn",
)
finally:
@@ -61,7 +61,7 @@ async def load_scns(
bot_player: dto.Player,
dao: HolderDao,
file_gateway: FileGateway,
- dcf: Factory,
+ retort: Retort,
path: Path,
):
files = sorted(path.glob("*.zip"), key=lambda p: int(p.stem))
@@ -73,12 +73,12 @@ async def load_scns(
player=bot_player,
dao=dao,
file_gateway=file_gateway,
- dcf=dcf,
+ retort=retort,
zip_scn=game_zip_scn,
)
if not game:
continue
- results = load_results(game_zip_scn, dcf)
+ results = load_results(game_zip_scn, retort)
await dao.game.set_completed(game)
await set_results(game, results, dao)
await dao.commit()
@@ -192,7 +192,7 @@ async def transfer_ownership(game: dto.FullGame, bot_player: dto.Player, dao: Ho
await dao.game.transfer(game, bot_player)
-def load_results(game_zip_scn: BinaryIO, dcf: Factory) -> GameStat:
+def load_results(game_zip_scn: BinaryIO, retort: Retort) -> GameStat:
zip_path = ZipPath(game_zip_scn)
for unpacked_file in zip_path.iterdir():
if not unpacked_file.is_file():
@@ -200,7 +200,7 @@ def load_results(game_zip_scn: BinaryIO, dcf: Factory) -> GameStat:
if unpacked_file.name != "results.json":
continue
with unpacked_file.open("r", encoding="utf8") as results_file:
- results = dcf.load(json.load(results_file), GameStat)
+ results = retort.load(json.load(results_file), GameStat)
return results
raise ValueError("no results found")
@@ -209,12 +209,12 @@ async def load_scn(
player: dto.Player,
dao: HolderDao,
file_gateway: FileGateway,
- dcf: Factory,
+ retort: Retort,
zip_scn: BinaryIO,
) -> dto.FullGame | None:
try:
with unpack_scn(ZipPath(zip_scn)).open() as scenario: # type: scn.RawGameScenario
- game = await upsert_game(scenario, player, dao.game_upserter, dcf, file_gateway)
+ game = await upsert_game(scenario, player, dao.game_upserter, retort, file_gateway)
except exceptions.ScenarioNotCorrect as e:
logger.error("game scenario from player %s has problems", player.id, exc_info=e)
return None
diff --git a/shvatka/infrastructure/db/models/level.py b/shvatka/infrastructure/db/models/level.py
index ff781794..7d479d3c 100644
--- a/shvatka/infrastructure/db/models/level.py
+++ b/shvatka/infrastructure/db/models/level.py
@@ -1,13 +1,14 @@
import typing
from typing import Any
-from dataclass_factory import Factory
+from adaptix import Retort, dumper
from sqlalchemy import Integer, Text, ForeignKey, JSON, TypeDecorator, UniqueConstraint
from sqlalchemy.engine import Dialect
from sqlalchemy.orm import relationship, mapped_column, Mapped
+from shvatka.common.factory import REQUIRED_GAME_RECIPES
from shvatka.core.models import dto
-from shvatka.core.models.dto.scn.level import LevelScenario
+from shvatka.core.models.dto import scn
from shvatka.infrastructure.db.models import Base
if typing.TYPE_CHECKING:
@@ -18,20 +19,22 @@
class ScenarioField(TypeDecorator):
impl = JSON
cache_ok = True
- dcf = Factory()
+ retort = Retort(
+ recipe=[*REQUIRED_GAME_RECIPES, dumper(set, lambda x: list(x))],
+ )
def coerce_compared_value(self, op: Any, value: Any):
- if isinstance(value, LevelScenario):
+ if isinstance(value, scn.LevelScenario):
return self
return self.impl().coerce_compared_value(op=op, value=value)
- def process_bind_param(self, value: LevelScenario | None, dialect: Dialect):
- return self.dcf.dump(value, LevelScenario)
+ def process_bind_param(self, value: scn.LevelScenario | None, dialect: Dialect):
+ return self.retort.dump(value, scn.LevelScenario)
- def process_result_value(self, value: Any, dialect: Dialect) -> LevelScenario | None:
+ def process_result_value(self, value: Any, dialect: Dialect) -> scn.LevelScenario | None:
if value is None:
return None
- return self.dcf.load(value, LevelScenario)
+ return self.retort.load(value, scn.LevelScenario)
class Level(Base):
@@ -52,7 +55,7 @@ class Level(Base):
back_populates="my_levels",
)
number_in_game = mapped_column(Integer, nullable=True)
- scenario: Mapped[LevelScenario] = mapped_column(ScenarioField)
+ scenario: Mapped[scn.LevelScenario] = mapped_column(ScenarioField)
__table_args__ = (UniqueConstraint("author_id", "name_id"),)
diff --git a/shvatka/tgbot/dialogs/__init__.py b/shvatka/tgbot/dialogs/__init__.py
index 9e517dbe..f866df98 100644
--- a/shvatka/tgbot/dialogs/__init__.py
+++ b/shvatka/tgbot/dialogs/__init__.py
@@ -2,6 +2,8 @@
from aiogram.enums import ChatType
from aiogram_dialog import setup_dialogs
from aiogram_dialog.api.protocols import MessageManagerProtocol, BgManagerFactory
+from aiogram_dialog.manager.message_manager import MessageManager
+from aiogram_dialog.tools import render_transitions
from shvatka.tgbot.dialogs import (
game_orgs,
@@ -60,3 +62,13 @@ def setup_active_game_dialogs() -> Router:
router = Router(name=__name__ + ".game.running")
game_spy.setup(router)
return router
+
+
+def render_all():
+ router = Router(name="main")
+ setup(router, MessageManager())
+ render_transitions(router, title="Shvatka", filename="out/shvatka-dialogs")
+
+
+if __name__ == "__main__":
+ render_all()
diff --git a/shvatka/tgbot/dialogs/game_manage/dialogs.py b/shvatka/tgbot/dialogs/game_manage/dialogs.py
index b82cbb8f..fc3b0f62 100644
--- a/shvatka/tgbot/dialogs/game_manage/dialogs.py
+++ b/shvatka/tgbot/dialogs/game_manage/dialogs.py
@@ -47,8 +47,7 @@
to_publish_game_forum,
complete_game_handler,
)
-from shvatka.tgbot.dialogs.preview_data import PREVIEW_GAME
-
+from shvatka.tgbot.dialogs.preview_data import PREVIEW_GAME, PreviewSwitchTo, PreviewStart
games = Dialog(
Window(
@@ -67,8 +66,9 @@
),
Cancel(Const("🔙Назад")),
state=states.CompletedGamesPanelSG.list,
- preview_data={"games": [PREVIEW_GAME]},
getter=get_games,
+ preview_data={"games": [PREVIEW_GAME]},
+ preview_add_transitions=[PreviewSwitchTo(states.CompletedGamesPanelSG.game)],
),
Window(
Jinja(
@@ -117,8 +117,11 @@
state=states.CompletedGamesPanelSG.list,
),
state=states.CompletedGamesPanelSG.game,
- preview_data={"game": PREVIEW_GAME},
getter=get_completed_game,
+ preview_data={"game": PREVIEW_GAME},
+ preview_add_transitions=[
+ PreviewStart(states.GameOrgsSG.orgs_list),
+ ],
),
Window(
Jinja(
@@ -237,8 +240,9 @@
),
Cancel(Const("🔙Назад")),
state=states.MyGamesPanelSG.choose_game,
- preview_data={"games": [PREVIEW_GAME]},
getter=get_my_games,
+ preview_data={"games": [PREVIEW_GAME]},
+ preview_add_transitions=[PreviewSwitchTo(states.MyGamesPanelSG.game_menu)],
),
Window(
Jinja(
@@ -320,8 +324,16 @@
state=states.MyGamesPanelSG.choose_game,
),
state=states.MyGamesPanelSG.game_menu,
- preview_data={"game": PREVIEW_GAME},
getter=get_game,
+ preview_data={"game": PREVIEW_GAME},
+ preview_add_transitions=[
+ PreviewStart(states.GameEditSG.current_levels),
+ PreviewStart(states.GameOrgsSG.orgs_list),
+ PreviewStart(states.GamePublishSG.prepare),
+ PreviewStart(states.GamePublishSG.forum),
+ Cancel(),
+ PreviewStart(states.GameScheduleSG.date),
+ ],
),
Window(
Jinja("Чтобы переименовать игру {{game.name}} пришли новое имя"),
@@ -339,8 +351,9 @@
Calendar(id="select_game_play_date", on_click=select_date),
Cancel(Const("🔙Назад")),
state=states.GameScheduleSG.date,
- preview_data={"game": PREVIEW_GAME},
getter=get_game,
+ preview_data={"game": PREVIEW_GAME},
+ preview_add_transitions=[PreviewSwitchTo(states.GameScheduleSG.time)],
),
Window(
Case(
diff --git a/shvatka/tgbot/dialogs/game_manage/getters.py b/shvatka/tgbot/dialogs/game_manage/getters.py
index 26e497a3..e3b79f22 100644
--- a/shvatka/tgbot/dialogs/game_manage/getters.py
+++ b/shvatka/tgbot/dialogs/game_manage/getters.py
@@ -5,6 +5,7 @@
from aiogram_dialog import DialogManager
from aiogram_dialog.api.entities import MediaAttachment, MediaId
from dishka import AsyncContainer
+from dishka.integrations.aiogram import CONTAINER_NAME
from telegraph import Telegraph
from shvatka.common.url_factory import UrlFactory
@@ -30,7 +31,7 @@ async def get_completed_game(dao: HolderDao, dialog_manager: DialogManager, **_)
game_id = (
dialog_manager.dialog_data.get("game_id", None) or dialog_manager.start_data["game_id"]
)
- dishka: AsyncContainer = dialog_manager.middleware_data["dishka_container"]
+ dishka: AsyncContainer = dialog_manager.middleware_data[CONTAINER_NAME]
url_factory = await dishka.get(UrlFactory)
return {
"game": await game.get_game(
diff --git a/shvatka/tgbot/dialogs/game_manage/handlers.py b/shvatka/tgbot/dialogs/game_manage/handlers.py
index a724002b..a97a38f3 100644
--- a/shvatka/tgbot/dialogs/game_manage/handlers.py
+++ b/shvatka/tgbot/dialogs/game_manage/handlers.py
@@ -3,10 +3,10 @@
from io import BytesIO
from typing import Any
+from adaptix import Retort
from aiogram.types import CallbackQuery, Message, BufferedInputFile
from aiogram_dialog import DialogManager
from aiogram_dialog.widgets.kbd import Button
-from dataclass_factory import Factory
from shvatka.core.interfaces.clients.file_storage import FileGateway
from shvatka.core.interfaces.scheduler import Scheduler
@@ -80,9 +80,9 @@ async def show_my_zip_scn(c: CallbackQuery, widget: Button, manager: DialogManag
async def common_show_zip(c: CallbackQuery, game_id: int, manager: DialogManager):
player: dto.Player = manager.middleware_data["player"]
dao: HolderDao = manager.middleware_data["dao"]
- dcf: Factory = manager.middleware_data["dcf"]
+ retort: Retort = manager.middleware_data["retort"]
file_gateway: FileGateway = manager.middleware_data["file_gateway"]
- game_ = await game.get_game_package(game_id, player, dao.game_packager, dcf, file_gateway)
+ game_ = await game.get_game_package(game_id, player, dao.game_packager, retort, file_gateway)
zip_ = pack_scn(game_)
assert isinstance(c.message, Message)
await c.message.answer_document(BufferedInputFile(file=zip_.read(), filename="scenario.zip"))
diff --git a/shvatka/tgbot/dialogs/game_scn/dialogs.py b/shvatka/tgbot/dialogs/game_scn/dialogs.py
index 6fdff055..5bccf7af 100644
--- a/shvatka/tgbot/dialogs/game_scn/dialogs.py
+++ b/shvatka/tgbot/dialogs/game_scn/dialogs.py
@@ -8,12 +8,14 @@
Select,
Cancel,
SwitchTo,
+ Next,
)
from aiogram_dialog.widgets.text import Const, Format, Jinja
from shvatka.tgbot import states
from .getters import get_game_name, select_my_levels, select_full_game
from .handlers import process_name, save_game, edit_level, add_level_handler, process_zip_scn
+from shvatka.tgbot.dialogs.preview_data import PreviewStart
game_writer = Dialog(
Window(
@@ -33,6 +35,7 @@
SwitchTo(Const("Загрузить из zip"), id="game_from_zip", state=states.GameWriteSG.from_zip),
Cancel(Const("🔙Отменить")),
state=states.GameWriteSG.game_name,
+ preview_add_transitions=[Next()],
),
Window(
Jinja("Игра {{game_name}}\n\n"),
@@ -91,6 +94,9 @@
Cancel(Const("🔙Назад")),
state=states.GameEditSG.current_levels,
getter=select_full_game,
+ preview_add_transitions=[
+ PreviewStart(states.LevelTestSG.wait_key),
+ ],
),
Window(
Jinja("Игра {{game.name}}\n\n"),
diff --git a/shvatka/tgbot/dialogs/game_scn/handlers.py b/shvatka/tgbot/dialogs/game_scn/handlers.py
index 1a529e2b..4ffaf4a4 100644
--- a/shvatka/tgbot/dialogs/game_scn/handlers.py
+++ b/shvatka/tgbot/dialogs/game_scn/handlers.py
@@ -3,12 +3,12 @@
from typing import Any
from zipfile import Path as ZipPath
+from adaptix import Retort
from aiogram import Bot
from aiogram.types import Message, CallbackQuery
from aiogram.utils.text_decorations import html_decoration as hd
from aiogram_dialog import DialogManager
from aiogram_dialog.widgets.kbd import Button, ManagedMultiselect
-from dataclass_factory import Factory
from shvatka.core.interfaces.clients.file_storage import FileGateway
from shvatka.core.models import dto
@@ -57,12 +57,12 @@ async def process_zip_scn(m: Message, dialog_: Any, manager: DialogManager):
dao: HolderDao = manager.middleware_data["dao"]
bot: Bot = manager.middleware_data["bot"]
file_gateway: FileGateway = manager.middleware_data["file_gateway"]
- dcf: Factory = manager.middleware_data["dcf"]
+ retort: Retort = manager.middleware_data["retort"]
assert m.document
document = await bot.download(m.document.file_id)
try:
with unpack_scn(ZipPath(document)).open() as scenario: # type: scn.RawGameScenario
- game = await upsert_game(scenario, player, dao.game_upserter, dcf, file_gateway)
+ game = await upsert_game(scenario, player, dao.game_upserter, retort, file_gateway)
except ScenarioNotCorrect as e:
await m.reply(f"Ошибка {e}\n попробуйте исправить файл")
logger.error("game scenario from player %s has problems", player.id, exc_info=e)
diff --git a/shvatka/tgbot/dialogs/level_manage/dialogs.py b/shvatka/tgbot/dialogs/level_manage/dialogs.py
index 7b1c6e88..9cc1f2bc 100644
--- a/shvatka/tgbot/dialogs/level_manage/dialogs.py
+++ b/shvatka/tgbot/dialogs/level_manage/dialogs.py
@@ -19,6 +19,7 @@
unlink_level_handler,
delete_level_handler,
)
+from shvatka.tgbot.dialogs.preview_data import PreviewStart
levels_list = Dialog(
Window(
@@ -38,13 +39,21 @@
Cancel(Const("🔙Назад")),
state=states.LevelListSG.levels,
getter=get_levels,
+ preview_add_transitions=[PreviewStart(states.LevelManageSG.menu)],
),
)
level_manage = Dialog(
Window(
- Jinja("Уровень {{level.name_id}}\n{{rendered}}"),
+ Jinja(
+ "Уровень {{level.name_id}}\n"
+ "{% if time_hints %}"
+ "{{time_hints | time_hints}}"
+ "{% else %}"
+ "пока нет ни одной подсказки"
+ "{% endif %}"
+ ),
Button(
Const("✏Редактирование"),
id="level_edit",
@@ -82,6 +91,10 @@
Cancel(Const("🔙Назад")),
state=states.LevelManageSG.menu,
getter=get_level_id,
+ preview_add_transitions=[
+ PreviewStart(states.LevelEditSg.menu),
+ PreviewStart(states.LevelTestSG.wait_key),
+ ],
),
Window(
Jinja(
@@ -119,5 +132,6 @@
),
getter=get_level_id,
state=states.LevelTestSG.wait_key,
+ preview_add_transitions=[Cancel()],
),
)
diff --git a/shvatka/tgbot/dialogs/level_manage/getters.py b/shvatka/tgbot/dialogs/level_manage/getters.py
index 6037afcc..af225d6b 100644
--- a/shvatka/tgbot/dialogs/level_manage/getters.py
+++ b/shvatka/tgbot/dialogs/level_manage/getters.py
@@ -6,7 +6,6 @@
from shvatka.core.services.level import get_by_id, get_level_by_id_for_org, get_all_my_free_levels
from shvatka.core.services.organizers import get_org_by_id, get_by_player
from shvatka.infrastructure.db.dao.holder import HolderDao
-from shvatka.tgbot.views.utils import render_time_hints
async def get_level_id(dao: HolderDao, dialog_manager: DialogManager, **_):
@@ -15,8 +14,8 @@ async def get_level_id(dao: HolderDao, dialog_manager: DialogManager, **_):
hints = level.scenario.time_hints
return {
"level": level,
+ "time_hints": hints,
"org": org,
- "rendered": render_time_hints(hints) if hints else "пока нет ни одной подсказки",
}
@@ -38,15 +37,14 @@ async def get_level_and_org(
dao: HolderDao,
manager: DialogManager,
) -> tuple[dto.Level, dto.Organizer | None]:
- try:
- org_id = manager.start_data["org_id"]
- except KeyError:
- level = await get_by_id(manager.start_data["level_id"], author, dao.level)
- org = await get_org(author, level, dao)
- else:
- org = await get_org_by_id(org_id, dao.organizer)
+ if "org_id" in manager.start_data:
+ org = await get_org_by_id(manager.start_data["org_id"], dao.organizer)
level = await get_level_by_id_for_org(manager.start_data["level_id"], org, dao.level)
- return level, org
+ return level, org
+ else:
+ level = await get_by_id(manager.start_data["level_id"], author, dao.level)
+ org_ = await get_org(author, level, dao)
+ return level, org_
async def get_levels(
diff --git a/shvatka/tgbot/dialogs/level_scn/dialogs.py b/shvatka/tgbot/dialogs/level_scn/dialogs.py
index 4b62a8a0..954ae96f 100644
--- a/shvatka/tgbot/dialogs/level_scn/dialogs.py
+++ b/shvatka/tgbot/dialogs/level_scn/dialogs.py
@@ -1,7 +1,7 @@
from aiogram import F
from aiogram_dialog import Dialog, Window
from aiogram_dialog.widgets.input import TextInput
-from aiogram_dialog.widgets.kbd import Button, Cancel
+from aiogram_dialog.widgets.kbd import Button, Cancel, ScrollingGroup, Select, Next
from aiogram_dialog.widgets.text import Const, Jinja
from shvatka.tgbot import states
@@ -27,8 +27,9 @@
on_correct_bonus_keys,
not_correct_bonus_keys,
start_bonus_keys,
+ start_edit_time_hint,
)
-from shvatka.tgbot.dialogs.preview_data import RENDERED_HINTS_PREVIEW
+from shvatka.tgbot.dialogs.preview_data import PreviewStart
level = Dialog(
Window(
@@ -51,6 +52,7 @@
id="level_id",
),
state=states.LevelSG.level_id,
+ preview_add_transitions=[Next()],
),
Window(
Jinja(
@@ -64,7 +66,11 @@
"💰Бонусных ключей: {{bonus_keys | length}}\n"
"{% endif %}"
"\n💡Подсказки:\n"
- "{{rendered}}"
+ "{% if time_hints %}"
+ "{{time_hints | time_hints}}"
+ "{% else %}"
+ "пока нет ни одной"
+ "{% endif %}"
),
Button(Const("🔑Ключи"), id="keys", on_click=start_keys),
Button(Const("💰Бонусные ключи"), id="bonus_keys", on_click=start_bonus_keys),
@@ -80,6 +86,12 @@
preview_data={
"level_id": "Pinky Pie",
},
+ preview_add_transitions=[
+ PreviewStart(state=states.LevelKeysSG.keys),
+ PreviewStart(state=states.LevelBonusKeysSG.bonus_keys),
+ PreviewStart(state=states.LevelHintsSG.time_hints),
+ Cancel(),
+ ],
),
on_process_result=process_level_result,
)
@@ -97,7 +109,11 @@
"💰Бонусных ключей: {{bonus_keys | length}}\n"
"{% endif %}"
"\n💡Подсказки:\n"
- "{{rendered}}"
+ "{% if time_hints %}"
+ "{{time_hints | time_hints}}"
+ "{% else %}"
+ "пока нет ни одной"
+ "{% endif %}"
),
Button(Const("🔑Ключи"), id="keys", on_click=start_keys),
Button(Const("💰Бонусные ключи"), id="bonus_keys", on_click=start_bonus_keys),
@@ -114,6 +130,11 @@
preview_data={
"level_id": "Pinky Pie",
},
+ preview_add_transitions=[
+ PreviewStart(state=states.LevelKeysSG.keys),
+ PreviewStart(state=states.LevelBonusKeysSG.bonus_keys),
+ PreviewStart(state=states.LevelHintsSG.time_hints),
+ ],
),
on_process_result=process_level_result,
on_start=on_start_level_edit,
@@ -158,7 +179,25 @@
hints_dialog = Dialog(
Window(
Jinja("💡Подсказки уровня {{level_id}}:\n"),
- Jinja("{{rendered}}"),
+ Jinja(
+ "{% if time_hints %}"
+ "{{time_hints | time_hints}}"
+ "{% else %}"
+ "пока нет ни одной"
+ "{% endif %}"
+ ),
+ ScrollingGroup(
+ Select(
+ Jinja("{{item | time_hint}}"),
+ id="level_hints",
+ item_id_getter=lambda x: x.time,
+ items="time_hints",
+ on_click=start_edit_time_hint,
+ ),
+ id="level_hints_sg",
+ width=1,
+ height=10,
+ ),
Button(Const("➕Добавить подсказку"), id="add_time_hint", on_click=start_add_time_hint),
Button(
Const("👌Достаточно подсказок"),
@@ -177,9 +216,12 @@
getter=get_time_hints,
preview_data={
"time_hints": [],
- "rendered": RENDERED_HINTS_PREVIEW,
"level_id": "Pinky Pie",
},
+ preview_add_transitions=[
+ PreviewStart(state=states.TimeHintSG.time),
+ PreviewStart(state=states.TimeHintEditSG.details),
+ ],
),
on_process_result=process_time_hint_result,
on_start=on_start_hints_edit,
diff --git a/shvatka/tgbot/dialogs/level_scn/getters.py b/shvatka/tgbot/dialogs/level_scn/getters.py
index e3e42a9a..8057fd62 100644
--- a/shvatka/tgbot/dialogs/level_scn/getters.py
+++ b/shvatka/tgbot/dialogs/level_scn/getters.py
@@ -1,9 +1,8 @@
+from adaptix import Retort
from aiogram_dialog import DialogManager
-from dataclass_factory import Factory
from shvatka.core.models.dto import scn
from shvatka.core.models.dto.scn import TimeHint
-from shvatka.tgbot.views.utils import render_time_hints
async def get_level_id(dialog_manager: DialogManager, **_):
@@ -20,34 +19,32 @@ async def get_keys(dialog_manager: DialogManager, **_):
async def get_bonus_keys(dialog_manager: DialogManager, **_):
- dcf: Factory = dialog_manager.middleware_data["dcf"]
+ retort: Retort = dialog_manager.middleware_data["retort"]
keys_raw = dialog_manager.dialog_data.get(
"bonus_keys", dialog_manager.start_data.get("bonus_keys", [])
)
return {
- "bonus_keys": dcf.load(keys_raw, list[scn.BonusKey]),
+ "bonus_keys": retort.load(keys_raw, list[scn.BonusKey]),
}
async def get_level_data(dialog_manager: DialogManager, **_):
dialog_data = dialog_manager.dialog_data
- dcf: Factory = dialog_manager.middleware_data["dcf"]
- hints = dcf.load(dialog_data.get("time_hints", []), list[TimeHint])
+ retort: Retort = dialog_manager.middleware_data["retort"]
+ hints = retort.load(dialog_data.get("time_hints", []), list[TimeHint])
return {
"level_id": dialog_data["level_id"],
"keys": dialog_data.get("keys", []),
"bonus_keys": dialog_data.get("bonus_keys", []),
"time_hints": hints,
- "rendered": render_time_hints(hints) if hints else "пока нет ни одной",
}
async def get_time_hints(dialog_manager: DialogManager, **_):
dialog_data = dialog_manager.dialog_data
- dcf: Factory = dialog_manager.middleware_data["dcf"]
- hints = dcf.load(dialog_data.get("time_hints", []), list[TimeHint])
+ retort: Retort = dialog_manager.middleware_data["retort"]
+ hints = retort.load(dialog_data.get("time_hints", []), list[TimeHint])
return {
"level_id": dialog_manager.start_data["level_id"],
"time_hints": hints,
- "rendered": render_time_hints(hints) if hints else "пока нет ни одной",
}
diff --git a/shvatka/tgbot/dialogs/level_scn/handlers.py b/shvatka/tgbot/dialogs/level_scn/handlers.py
index 9c67da10..f82affad 100644
--- a/shvatka/tgbot/dialogs/level_scn/handlers.py
+++ b/shvatka/tgbot/dialogs/level_scn/handlers.py
@@ -1,9 +1,9 @@
from typing import Any
+from adaptix import Retort
from aiogram.types import CallbackQuery, Message
from aiogram_dialog import Data, DialogManager
from aiogram_dialog.widgets.kbd import Button
-from dataclass_factory import Factory
from shvatka.core.models import dto
from shvatka.core.models.dto import scn
@@ -96,15 +96,25 @@ async def not_correct_bonus_keys(
async def on_correct_bonus_keys(
m: Message, dialog_: Any, manager: DialogManager, keys: list[scn.BonusKey]
):
- dcf: Factory = manager.middleware_data["dcf"]
- await manager.done({"bonus_keys": dcf.dump(keys)})
+ retort: Retort = manager.middleware_data["retort"]
+ await manager.done({"bonus_keys": retort.dump(keys)})
async def process_time_hint_result(start_data: Data, result: Any, manager: DialogManager):
if not result:
return
- if new_hint := result["time_hint"]:
+ if new_hint := result.get("time_hint", None):
manager.dialog_data.setdefault("time_hints", []).append(new_hint)
+ elif (edited_hint := result.get("edited_time_hint")) and isinstance(start_data, dict):
+ old_hint = start_data["time_hint"]
+ if edited_hint == old_hint:
+ return
+ retort: Retort = manager.middleware_data["retort"]
+ hints_list = retort.load(manager.dialog_data.get("time_hints", []), scn.HintsList)
+ edited_list = hints_list.replace(
+ retort.load(old_hint, scn.TimeHint), retort.load(edited_hint, scn.TimeHint)
+ )
+ manager.dialog_data["time_hints"] = retort.dump(edited_list)
async def process_level_result(start_data: Data, result: Any, manager: DialogManager):
@@ -120,22 +130,33 @@ async def process_level_result(start_data: Data, result: Any, manager: DialogMan
async def on_start_level_edit(start_data: dict[str, Any], manager: DialogManager):
dao: HolderDao = manager.middleware_data["dao"]
- dcf: Factory = manager.middleware_data["dcf"]
+ retort: Retort = manager.middleware_data["retort"]
author: dto.Player = manager.middleware_data["player"]
level = await get_by_id(start_data["level_id"], author, dao.level)
manager.dialog_data["level_id"] = level.name_id
manager.dialog_data["keys"] = list(level.get_keys())
- manager.dialog_data["time_hints"] = dcf.dump(level.scenario.time_hints)
- manager.dialog_data["bonus_keys"] = dcf.dump(level.get_bonus_keys())
+ manager.dialog_data["time_hints"] = retort.dump(level.scenario.time_hints)
+ manager.dialog_data["bonus_keys"] = retort.dump(level.get_bonus_keys(), set[scn.BonusKey])
async def on_start_hints_edit(start_data: dict[str, Any], manager: DialogManager):
manager.dialog_data["time_hints"] = start_data["time_hints"]
+async def start_edit_time_hint(
+ c: CallbackQuery, widget: Any, manager: DialogManager, hint_time: str
+):
+ retort: Retort = manager.middleware_data["retort"]
+ hints = retort.load(manager.dialog_data.get("time_hints", []), list[scn.TimeHint])
+ await manager.start(
+ state=states.TimeHintEditSG.details,
+ data={"time_hint": retort.dump(next(filter(lambda x: x.time == int(hint_time), hints)))},
+ )
+
+
async def start_add_time_hint(c: CallbackQuery, button: Button, manager: DialogManager):
- dcf: Factory = manager.middleware_data["dcf"]
- hints = dcf.load(manager.dialog_data.get("time_hints", []), list[scn.TimeHint])
+ retort: Retort = manager.middleware_data["retort"]
+ hints = retort.load(manager.dialog_data.get("time_hints", []), list[scn.TimeHint])
previous_time = hints[-1].time if hints else -1
await manager.start(state=states.TimeHintSG.time, data={"previous_time": previous_time})
@@ -179,16 +200,16 @@ async def clear_hints(c: CallbackQuery, button: Button, manager: DialogManager):
async def save_level(c: CallbackQuery, button: Button, manager: DialogManager):
- dcf: Factory = manager.middleware_data["dcf"]
+ retort: Retort = manager.middleware_data["retort"]
author: dto.Player = manager.middleware_data["player"]
dao: HolderDao = manager.middleware_data["dao"]
data = manager.dialog_data
id_ = data["level_id"]
keys = set(map(normalize_key, data["keys"]))
- time_hints = dcf.load(data["time_hints"], list[scn.TimeHint])
- bonus_keys = dcf.load(data.get("bonus_keys", []), set[scn.BonusKey])
+ time_hints = retort.load(data["time_hints"], list[scn.TimeHint])
+ bonus_keys = retort.load(data.get("bonus_keys", []), set[scn.BonusKey])
level_scn = scn.LevelScenario(id=id_, keys=keys, time_hints=time_hints, bonus_keys=bonus_keys)
level = await upsert_level(author=author, scenario=level_scn, dao=dao.level)
- await manager.done(result={"level": dcf.dump(level)})
+ await manager.done(result={"level": retort.dump(level)})
await c.answer(text="Уровень успешно сохранён")
diff --git a/shvatka/tgbot/dialogs/preview_data.py b/shvatka/tgbot/dialogs/preview_data.py
index aaf2ea55..a40a27f3 100644
--- a/shvatka/tgbot/dialogs/preview_data.py
+++ b/shvatka/tgbot/dialogs/preview_data.py
@@ -1,5 +1,9 @@
from datetime import datetime
+from aiogram.fsm.state import State
+from aiogram_dialog.widgets.kbd import Start, SwitchTo
+from aiogram_dialog.widgets.text import Const
+
from shvatka.core.models import dto
from shvatka.core.models.enums import GameStatus
from shvatka.core.utils.datetime_utils import tz_utc
@@ -34,4 +38,13 @@
levels_count=13,
)
TIMES_PRESET = [5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60]
-RENDERED_HINTS_PREVIEW = "0: 📃🪪\n10: 📃\n10: 📃\n15: 📃\n20: 📃\n25: 🪪\n30: 📡\n45: 📃"
+
+
+class PreviewStart(Start):
+ def __init__(self, state: State):
+ super().__init__(Const(""), "", state)
+
+
+class PreviewSwitchTo(SwitchTo):
+ def __init__(self, state: State):
+ super().__init__(Const(""), "", state)
diff --git a/shvatka/tgbot/dialogs/starters/editor.py b/shvatka/tgbot/dialogs/starters/editor.py
index 6224d6c5..c21cfbcf 100644
--- a/shvatka/tgbot/dialogs/starters/editor.py
+++ b/shvatka/tgbot/dialogs/starters/editor.py
@@ -41,4 +41,9 @@ def setup() -> Router:
state=states.GameWriteSG.game_name,
router=router,
)
+ register_start_handler(
+ Command(commands=LEVELS_COMMAND),
+ state=states.LevelListSG.levels,
+ router=router,
+ )
return router
diff --git a/shvatka/tgbot/dialogs/time_hint/__init__.py b/shvatka/tgbot/dialogs/time_hint/__init__.py
index 81fd4bae..49726281 100644
--- a/shvatka/tgbot/dialogs/time_hint/__init__.py
+++ b/shvatka/tgbot/dialogs/time_hint/__init__.py
@@ -1,7 +1,8 @@
from aiogram import Router
-from .dialogs import time_hint
+from .dialogs import time_hint, time_hint_edit
def setup(router: Router):
router.include_router(time_hint)
+ router.include_router(time_hint_edit)
diff --git a/shvatka/tgbot/dialogs/time_hint/dialogs.py b/shvatka/tgbot/dialogs/time_hint/dialogs.py
index 52260bab..9afd32b6 100644
--- a/shvatka/tgbot/dialogs/time_hint/dialogs.py
+++ b/shvatka/tgbot/dialogs/time_hint/dialogs.py
@@ -1,12 +1,32 @@
from aiogram_dialog import Dialog, Window
from aiogram_dialog.widgets.input import MessageInput
-from aiogram_dialog.widgets.kbd import Select, Button, Group, Back, Cancel
+from aiogram_dialog.widgets.kbd import (
+ Select,
+ Button,
+ Group,
+ Back,
+ Cancel,
+ SwitchTo,
+ ScrollingGroup,
+ ListGroup,
+)
from aiogram_dialog.widgets.text import Const, Format, Case, Jinja
from shvatka.tgbot import states
from .getters import get_available_times, get_hints
-from .handlers import process_time_message, select_time, process_hint, on_finish, hint_on_start
-from shvatka.tgbot.dialogs.preview_data import TIMES_PRESET
+from .handlers import (
+ process_time_message,
+ select_time,
+ process_hint,
+ on_finish,
+ hint_on_start,
+ hint_edit_on_start,
+ process_edit_time_message,
+ edit_single_hint,
+ save_edited_time_hint,
+ delete_single_hint,
+)
+from shvatka.tgbot.dialogs.preview_data import TIMES_PRESET, PreviewSwitchTo
time_hint = Dialog(
Window(
@@ -27,6 +47,7 @@
state=states.TimeHintSG.time,
getter=get_available_times,
preview_data={"times": TIMES_PRESET},
+ preview_add_transitions=[PreviewSwitchTo(state=states.TimeHintSG.hint)],
),
Window(
Jinja("Подсказка выходящая в {{time}} мин."),
@@ -34,7 +55,7 @@
{
False: Const("Присылай сообщения с подсказками (текст, фото, видео итд)"),
True: Jinja(
- "{{rendered}}\n"
+ "{{hints | hints}}\n"
"Можно прислать ещё сообщения или перейти к следующей подсказке"
),
},
@@ -50,7 +71,73 @@
Back(text=Const("Изменить время")),
getter=get_hints,
state=states.TimeHintSG.hint,
- preview_data={"has_hints": True, "rendered": "📃🪪"},
+ preview_data={"has_hints": True},
),
on_start=hint_on_start,
)
+
+
+time_hint_edit = Dialog(
+ Window(
+ Jinja("Подсказка выходящая в {{time}}:" "{{hints | hints}}"),
+ SwitchTo(
+ Const("Изменить время"),
+ id="change_time",
+ state=states.TimeHintEditSG.time,
+ ),
+ ScrollingGroup(
+ ListGroup(
+ Button(
+ Jinja("{{item[1] | single_hint}}"),
+ on_click=edit_single_hint,
+ id="show",
+ ),
+ Button(
+ Const("🗑"),
+ on_click=delete_single_hint,
+ id="delete",
+ ),
+ id="hints",
+ item_id_getter=lambda x: x[0],
+ items="numerated_hints",
+ ),
+ id="hints_sg",
+ width=2,
+ height=10,
+ ),
+ SwitchTo(Const("Добавить"), state=states.TimeHintEditSG.add_part, id="to_add_part"),
+ Button(
+ text=Const("Сохранить изменения"),
+ id="save_time_hint",
+ on_click=save_edited_time_hint,
+ ),
+ Cancel(text=Const("Вернуться, ничего не менять")),
+ getter=get_hints,
+ state=states.TimeHintEditSG.details,
+ ),
+ Window(
+ Jinja("Введи новое время выхода подсказки"),
+ MessageInput(func=process_edit_time_message),
+ getter=get_hints,
+ state=states.TimeHintEditSG.time,
+ ),
+ Window(
+ Jinja("Подсказка выходящая в {{time}} мин."),
+ Case(
+ {
+ False: Const("Присылай сообщения с подсказками (текст, фото, видео итд)"),
+ True: Jinja("{{hints | hints}}\n" "Можно прислать ещё сообщения или вернуться"),
+ },
+ selector="has_hints",
+ ),
+ MessageInput(func=process_hint),
+ SwitchTo(
+ text=Const("Вернуться"),
+ state=states.TimeHintEditSG.details,
+ id="to_details",
+ ),
+ getter=get_hints,
+ state=states.TimeHintEditSG.add_part,
+ ),
+ on_start=hint_edit_on_start,
+)
diff --git a/shvatka/tgbot/dialogs/time_hint/getters.py b/shvatka/tgbot/dialogs/time_hint/getters.py
index 04a5710a..6decdcaa 100644
--- a/shvatka/tgbot/dialogs/time_hint/getters.py
+++ b/shvatka/tgbot/dialogs/time_hint/getters.py
@@ -1,8 +1,7 @@
+from adaptix import Retort
from aiogram_dialog import DialogManager
-from dataclass_factory import Factory
from shvatka.core.models.dto.scn.hint_part import AnyHint
-from shvatka.tgbot.views.utils import render_hints
async def get_available_times(dialog_manager: DialogManager, **_):
@@ -18,13 +17,13 @@ async def get_available_times(dialog_manager: DialogManager, **_):
async def get_hints(dialog_manager: DialogManager, **_):
dialog_data = dialog_manager.dialog_data
- dcf: Factory = dialog_manager.middleware_data["dcf"]
+ retort: Retort = dialog_manager.middleware_data["retort"]
- hints = dcf.load(dialog_data["hints"], list[AnyHint])
+ hints = retort.load(dialog_data["hints"], list[AnyHint])
time_ = dialog_data["time"]
return {
"hints": hints,
+ "numerated_hints": list(enumerate(hints)),
"time": time_,
"has_hints": len(hints) > 0,
- "rendered": render_hints(hints),
}
diff --git a/shvatka/tgbot/dialogs/time_hint/handlers.py b/shvatka/tgbot/dialogs/time_hint/handlers.py
index 1b53798a..26461897 100644
--- a/shvatka/tgbot/dialogs/time_hint/handlers.py
+++ b/shvatka/tgbot/dialogs/time_hint/handlers.py
@@ -1,20 +1,45 @@
from typing import Any
+from adaptix import Retort
+from aiogram import types
from aiogram.types import CallbackQuery, Message
-from aiogram_dialog import DialogManager
+from aiogram_dialog import DialogManager, SubManager
from aiogram_dialog.widgets.kbd import Button
-from dataclass_factory import Factory
+from dishka import AsyncContainer
+from dishka.integrations.aiogram import CONTAINER_NAME
+from shvatka.core.models.dto import scn
from shvatka.core.models.dto.scn import TimeHint
from shvatka.core.models.dto.scn.hint_part import AnyHint
+from shvatka.core.utils import exceptions
from shvatka.tgbot import states
from shvatka.tgbot.views.hint_factory.hint_parser import HintParser
+from shvatka.tgbot.views.hint_sender import HintSender
async def select_time(c: CallbackQuery, widget: Any, manager: DialogManager, item_id: str):
await set_time(int(item_id), manager)
+async def process_edit_time_message(m: Message, dialog_: Any, manager: DialogManager) -> None:
+ try:
+ time_ = int(m.text)
+ except ValueError:
+ await m.answer("Некорректный формат времени. Пожалуйста введите время в формате ЧЧ:ММ")
+ return
+ retort: Retort = manager.middleware_data["retort"]
+ hint = retort.load(manager.start_data["time_hint"], scn.TimeHint)
+ if not hint.can_update_time():
+ await m.reply(
+ "Увы, отредактировать время данной подсказки не получится. "
+ "Скорее всего это загадка уровня (Подсказка 0 мин.). "
+ "Придётся переделать прямо тут текст (или медиа, или что там)"
+ )
+ return
+ manager.dialog_data["time"] = time_
+ await manager.switch_to(states.TimeHintEditSG.details)
+
+
async def process_time_message(m: Message, dialog_: Any, manager: DialogManager) -> None:
try:
time_ = int(m.text)
@@ -27,6 +52,41 @@ async def process_time_message(m: Message, dialog_: Any, manager: DialogManager)
await m.answer("Время выхода данной подсказки должно быть больше, чем предыдущей")
+async def edit_single_hint(c: CallbackQuery, widget: Any, manager: DialogManager):
+ assert isinstance(manager, SubManager)
+ dishka: AsyncContainer = manager.middleware_data[CONTAINER_NAME]
+ retort = await dishka.get(Retort)
+ hint = retort.load(manager.dialog_data["hints"], list[AnyHint])
+ hint_sender = await dishka.get(HintSender)
+ chat: types.Chat = manager.middleware_data["event_chat"]
+ hint_index = manager.item_id
+ await hint_sender.send_hint(hint[int(hint_index)], chat.id)
+
+
+async def delete_single_hint(c: CallbackQuery, widget: Any, manager: DialogManager):
+ assert isinstance(manager, SubManager)
+ dishka: AsyncContainer = manager.middleware_data[CONTAINER_NAME]
+ retort = await dishka.get(Retort)
+ hints = retort.load(manager.dialog_data.get("hints"), list[AnyHint])
+ hint_index = manager.item_id
+ hints.pop(int(hint_index))
+ manager.dialog_data["hints"] = retort.dump(hints, list[AnyHint])
+
+
+async def save_edited_time_hint(c: CallbackQuery, widget: Any, manager: DialogManager):
+ dishka: AsyncContainer = manager.middleware_data[CONTAINER_NAME]
+ retort = await dishka.get(Retort)
+ time_hint = retort.load(manager.start_data["time_hint"], scn.TimeHint)
+ try:
+ time_hint.update_time(manager.dialog_data["time"])
+ time_hint.update_hint(retort.load(manager.dialog_data["hints"], list[AnyHint]))
+ except exceptions.LevelError as e:
+ assert isinstance(c.message, Message)
+ await c.message.reply(e.text)
+ return
+ await manager.done({"edited_time_hint": retort.dump(time_hint)})
+
+
async def set_time(time_minutes: int, manager: DialogManager):
if time_minutes <= int(manager.start_data["previous_time"]):
raise ValueError("Время меньше предыдущего")
@@ -38,18 +98,18 @@ async def set_time(time_minutes: int, manager: DialogManager):
async def process_hint(m: Message, dialog_: Any, manager: DialogManager) -> None:
- dcf: Factory = manager.middleware_data["dcf"]
+ retort: Retort = manager.middleware_data["retort"]
parser: HintParser = manager.middleware_data["hint_parser"]
hint = await parser.parse(m, manager.middleware_data["player"])
- manager.dialog_data["hints"].append(dcf.dump(hint))
+ manager.dialog_data["hints"].append(retort.dump(hint))
async def on_finish(c: CallbackQuery, button: Button, manager: DialogManager):
- dcf: Factory = manager.middleware_data["dcf"]
- hints = dcf.load(manager.dialog_data["hints"], list[AnyHint])
+ retort: Retort = manager.middleware_data["retort"]
+ hints = retort.load(manager.dialog_data["hints"], list[AnyHint])
time_ = manager.dialog_data["time"]
time_hint = TimeHint(time=time_, hint=hints)
- await manager.done({"time_hint": dcf.dump(time_hint)})
+ await manager.done({"time_hint": retort.dump(time_hint)})
async def hint_on_start(start_data: dict, manager: DialogManager):
@@ -58,3 +118,10 @@ async def hint_on_start(start_data: dict, manager: DialogManager):
manager.dialog_data["time"] = 0
await manager.switch_to(states.TimeHintSG.hint)
manager.dialog_data.setdefault("hints", [])
+
+
+async def hint_edit_on_start(start_data: dict, manager: DialogManager):
+ retort: Retort = manager.middleware_data["retort"]
+ hint = retort.load(manager.start_data["time_hint"], scn.TimeHint)
+ manager.dialog_data["hints"] = retort.dump(hint.hint, list[AnyHint])
+ manager.dialog_data["time"] = hint.time
diff --git a/shvatka/tgbot/middlewares/init_middleware.py b/shvatka/tgbot/middlewares/init_middleware.py
index ad9fa95a..ffdf557a 100644
--- a/shvatka/tgbot/middlewares/init_middleware.py
+++ b/shvatka/tgbot/middlewares/init_middleware.py
@@ -1,5 +1,6 @@
from typing import Callable, Any, Awaitable
+from adaptix import Retort
from aiogram import BaseMiddleware
from aiogram.types import TelegramObject
from aiogram_dialog.api.protocols import BgManagerFactory
@@ -40,6 +41,7 @@ async def __call__( # type: ignore[override]
data["main_config"] = await dishka.get(TgBotConfig)
data["user_getter"] = await dishka.get(UserGetter)
data["dcf"] = await dishka.get(Factory)
+ data["retort"] = await dishka.get(Retort)
data["scheduler"] = await dishka.get(Scheduler) # type: ignore[type-abstract]
data["locker"] = await dishka.get(KeyCheckerFactory) # type: ignore[type-abstract]
data["file_storage"] = file_storage
diff --git a/shvatka/tgbot/states.py b/shvatka/tgbot/states.py
index 04524197..5fc9d64a 100644
--- a/shvatka/tgbot/states.py
+++ b/shvatka/tgbot/states.py
@@ -18,6 +18,12 @@ class TimeHintSG(StatesGroup):
hint = State()
+class TimeHintEditSG(StatesGroup):
+ details = State()
+ time = State()
+ add_part = State()
+
+
class LevelListSG(StatesGroup):
levels = State()
diff --git a/shvatka/tgbot/utils/data.py b/shvatka/tgbot/utils/data.py
index bc9965ae..31e2d11d 100644
--- a/shvatka/tgbot/utils/data.py
+++ b/shvatka/tgbot/utils/data.py
@@ -1,5 +1,6 @@
from typing import TypedDict, Any
+from adaptix import Retort
from aiogram import types, Bot, Router
from aiogram.dispatcher.event.handler import HandlerObject
from aiogram.fsm.context import FSMContext
@@ -52,6 +53,7 @@ class MiddlewareData(DialogMiddlewareData, total=False):
dishka_container: AsyncContainer
user_getter: UserGetter
dcf: Factory
+ retort: Retort
dao: HolderDao
scheduler: Scheduler
locker: KeyCheckerFactory
diff --git a/shvatka/tgbot/views/commands.py b/shvatka/tgbot/views/commands.py
index 85459125..e9b6c39a 100644
--- a/shvatka/tgbot/views/commands.py
+++ b/shvatka/tgbot/views/commands.py
@@ -76,6 +76,7 @@ def __str__(self) -> str:
)
PUBLISH_COMMAND = BotCommand(command="publish_forum", description="опубликовать на форуме")
+
HELP_ORG = CommandsGroup(
"Команды для организаторов:",
[
diff --git a/shvatka/tgbot/views/jinja_filters/__init__.py b/shvatka/tgbot/views/jinja_filters/__init__.py
index 1ca15366..d8dd418c 100644
--- a/shvatka/tgbot/views/jinja_filters/__init__.py
+++ b/shvatka/tgbot/views/jinja_filters/__init__.py
@@ -6,6 +6,12 @@
from .boolean_emoji import bool_render
from .game_status import to_readable_name
from .timezone import datetime_filter, timedelta_filter
+from shvatka.tgbot.views.utils import (
+ render_single_hint,
+ render_hints,
+ render_time_hint,
+ render_time_hints,
+)
def setup_jinja(bot: Bot):
@@ -18,5 +24,9 @@ def setup_jinja(bot: Bot):
"bool_emoji": bool_render,
"game_status": to_readable_name,
"timedelta": timedelta_filter,
+ "single_hint": render_single_hint,
+ "hints": render_hints,
+ "time_hint": render_time_hint,
+ "time_hints": render_time_hints,
},
)
diff --git a/shvatka/tgbot/views/utils.py b/shvatka/tgbot/views/utils.py
index f719f70d..bd462400 100644
--- a/shvatka/tgbot/views/utils.py
+++ b/shvatka/tgbot/views/utils.py
@@ -51,4 +51,8 @@ def render_time_hint(time_hint: TimeHint) -> str:
def render_hints(hints: list[BaseHint]) -> str:
- return "".join([HINTS_EMOJI[HintType[hint.type]] for hint in hints])
+ return "".join([render_single_hint(hint) for hint in hints])
+
+
+def render_single_hint(hint: BaseHint) -> str:
+ return HINTS_EMOJI[HintType[hint.type]]
diff --git a/tests/conftest.py b/tests/conftest.py
index 118f20a2..b69340ef 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -4,6 +4,7 @@
import pytest
import pytest_asyncio
+from adaptix import Retort
from dataclass_factory import Factory
from dishka import make_async_container
@@ -41,3 +42,9 @@ def event_loop():
async def dcf():
dishka = make_async_container(DCFProvider())
return await dishka.get(Factory)
+
+
+@pytest_asyncio.fixture(scope="session")
+async def retort() -> Retort:
+ dishka = make_async_container(DCFProvider())
+ return await dishka.get(Retort)
diff --git a/tests/fixtures/game_fixtures.py b/tests/fixtures/game_fixtures.py
index f9df0e7d..ca82e697 100644
--- a/tests/fixtures/game_fixtures.py
+++ b/tests/fixtures/game_fixtures.py
@@ -2,7 +2,7 @@
from datetime import datetime
import pytest_asyncio
-from dataclass_factory import Factory
+from adaptix import Retort
from shvatka.core.interfaces.clients.file_storage import FileGateway
from shvatka.core.models import dto, enums
@@ -20,14 +20,14 @@ async def game(
complex_scn: RawGameScenario,
author: dto.Player,
dao: HolderDao,
- dcf: Factory,
+ retort: Retort,
file_gateway: FileGateway,
) -> dto.FullGame:
return await upsert_game(
complex_scn,
author,
dao.game_upserter,
- dcf,
+ retort,
file_gateway,
)
diff --git a/tests/integration/api_full/test_game.py b/tests/integration/api_full/test_game.py
index 54f8ee76..18958aa3 100644
--- a/tests/integration/api_full/test_game.py
+++ b/tests/integration/api_full/test_game.py
@@ -1,9 +1,12 @@
import pytest
+from adaptix import Retort
from dataclass_factory import Factory
from httpx import AsyncClient
from shvatka.api.models import responses
+from shvatka.common.factory import REQUIRED_GAME_RECIPES
from shvatka.core.models import dto
+from shvatka.core.models.dto import scn
from shvatka.core.models.enums import GameStatus
from shvatka.core.services.player import upsert_player
from shvatka.infrastructure.db.dao.holder import HolderDao
@@ -61,12 +64,16 @@ async def test_game_card(
assert resp.is_success
resp.read()
- dcf = Factory()
- actual = dcf.load(resp.json(), responses.FullGame)
+ retort = Retort(
+ recipe=[
+ *REQUIRED_GAME_RECIPES,
+ ]
+ )
+ actual = retort.load(resp.json(), responses.FullGame)
assert actual.id == finished_game.id
assert actual.status == GameStatus.complete
assert len(actual.levels) == len(finished_game.levels)
- assert [lvl.scenario for lvl in actual.levels] == [
+ assert [retort.load(lvl.scenario, scn.LevelScenario) for lvl in actual.levels] == [
lvl.scenario for lvl in finished_game.levels
]
diff --git a/tests/integration/test_game.py b/tests/integration/test_game.py
index 66d2831b..9a349d11 100644
--- a/tests/integration/test_game.py
+++ b/tests/integration/test_game.py
@@ -1,7 +1,7 @@
from copy import deepcopy
import pytest
-from dataclass_factory import Factory
+from adaptix import Retort
from shvatka.core.interfaces.clients.file_storage import FileGateway
from shvatka.core.models import dto
@@ -25,10 +25,10 @@ async def test_game_simple(
author: dto.Player,
three_lvl_scn: RawGameScenario,
dao: HolderDao,
- dcf: Factory,
+ retort: Retort,
file_gateway: FileGateway,
):
- game = await upsert_game(three_lvl_scn, author, dao.game_upserter, dcf, file_gateway)
+ game = await upsert_game(three_lvl_scn, author, dao.game_upserter, retort, file_gateway)
assert await dao.game.count() == 1
assert await dao.level.count() == 3
@@ -48,7 +48,7 @@ async def test_game_simple(
another_scn["levels"].append(another_scn["levels"].pop(0))
game = await upsert_game(
- RawGameScenario(scn=another_scn, files={}), author, dao.game_upserter, dcf, file_gateway
+ RawGameScenario(scn=another_scn, files={}), author, dao.game_upserter, retort, file_gateway
)
assert await dao.game.count() == 1
@@ -68,7 +68,7 @@ async def test_game_simple(
another_scn["levels"].pop()
game = await upsert_game(
- RawGameScenario(scn=another_scn, files={}), author, dao.game_upserter, dcf, file_gateway
+ RawGameScenario(scn=another_scn, files={}), author, dao.game_upserter, retort, file_gateway
)
assert await dao.game.count() == 1
@@ -98,10 +98,10 @@ async def test_game_get_full(
author: dto.Player,
simple_scn: RawGameScenario,
dao: HolderDao,
- dcf: Factory,
+ retort: Retort,
file_gateway: FileGateway,
):
- game_expected = await upsert_game(simple_scn, author, dao.game_upserter, dcf, file_gateway)
+ game_expected = await upsert_game(simple_scn, author, dao.game_upserter, retort, file_gateway)
game_actual = await dao.game.get_full(game_expected.id)
assert game_expected == game_actual
@@ -111,10 +111,10 @@ async def test_game_get_preview(
author: dto.Player,
simple_scn: RawGameScenario,
dao: HolderDao,
- dcf: Factory,
+ retort: Retort,
file_gateway: FileGateway,
):
- game_expected = await upsert_game(simple_scn, author, dao.game_upserter, dcf, file_gateway)
+ game_expected = await upsert_game(simple_scn, author, dao.game_upserter, retort, file_gateway)
game_actual = await dao.game.get_preview(game_expected.id)
assert len(game_expected.levels) == game_actual.levels_count
assert game_expected.id == game_actual.id
diff --git a/tests/integration/test_level.py b/tests/integration/test_level.py
index de41a9fc..be4fc54f 100644
--- a/tests/integration/test_level.py
+++ b/tests/integration/test_level.py
@@ -1,5 +1,5 @@
import pytest
-from dataclass_factory import Factory
+from adaptix import Retort
from shvatka.core.models.dto.scn.game import RawGameScenario
from shvatka.core.models.dto.scn.level import LevelScenario
@@ -11,12 +11,12 @@
@pytest.mark.asyncio
-async def test_simple_level(simple_scn: RawGameScenario, dao: HolderDao, dcf: Factory):
+async def test_simple_level(simple_scn: RawGameScenario, dao: HolderDao, retort: Retort):
author = await upsert_player(await upsert_user(create_dto_harry(), dao.user), dao.player)
await dao.player.promote(author, author)
await dao.commit()
author.can_be_author = True
- lvl = await upsert_raw_level(simple_scn.scn["levels"][0], author, dcf, dao.level)
+ lvl = await upsert_raw_level(simple_scn.scn["levels"][0], author, retort, dao.level)
assert lvl.db_id is not None
assert await dao.level.count() == 1
diff --git a/tests/mocks/bot.py b/tests/mocks/bot.py
index c154981e..52d775c9 100644
--- a/tests/mocks/bot.py
+++ b/tests/mocks/bot.py
@@ -7,6 +7,7 @@
from shvatka.tgbot.config.models.bot import BotConfig
from shvatka.tgbot.config.models.main import TgBotConfig
from shvatka.tgbot.views.bot_alert import BotAlert
+from shvatka.tgbot.views.jinja_filters import setup_jinja
class MockMessageManagerProvider(Provider):
@@ -22,7 +23,9 @@ class MockBotProvider(Provider):
@provide
async def get_bot(self, config: TgBotConfig) -> Bot:
- return MockedBot(token=config.bot.token)
+ bot = MockedBot(token=config.bot.token)
+ setup_jinja(bot)
+ return bot
@provide
async def bot_alert(self, bot: Bot, config: BotConfig) -> BotAlert:
diff --git a/tests/unit/serialization/test_deserialize.py b/tests/unit/serialization/test_deserialize.py
index 0af61daa..bf8c53e5 100644
--- a/tests/unit/serialization/test_deserialize.py
+++ b/tests/unit/serialization/test_deserialize.py
@@ -1,7 +1,7 @@
from copy import deepcopy
import pytest
-from dataclass_factory import Factory
+from adaptix import Retort
from shvatka.core.models.dto.scn import TextHint, GPSHint, PhotoHint, ContactHint
from shvatka.core.models.dto.scn.game import RawGameScenario
@@ -22,41 +22,41 @@
from shvatka.tgbot.views.utils import render_hints
-def test_deserialize_game(simple_scn: RawGameScenario, dcf: Factory):
- game = parse_game(simple_scn, dcf)
+def test_deserialize_game(simple_scn: RawGameScenario, retort: Retort):
+ game = parse_game(simple_scn, retort)
assert "My new game" == game.name
assert HintType.text.name == game.levels[0].time_hints[0].hint[0].type
assert "загадка" == game.levels[0].time_hints[0].hint[0].text
assert HintType.gps.name == game.levels[0].time_hints[2].hint[0].type
-def test_deserialize_level(simple_scn: RawGameScenario, dcf: Factory):
- level = load_level(simple_scn.scn["levels"][0], dcf)
+def test_deserialize_level(simple_scn: RawGameScenario, retort: Retort):
+ level = load_level(simple_scn.scn["levels"][0], retort)
assert "first" == level.id
assert HintType.text.name == level.time_hints[0].hint[0].type
assert "загадка" == level.time_hints[0].hint[0].text
assert HintType.gps.name == level.time_hints[2].hint[0].type
-def test_deserialize_invalid_level(simple_scn: RawGameScenario, dcf: Factory):
+def test_deserialize_invalid_level(simple_scn: RawGameScenario, retort: Retort):
level_source = simple_scn.scn["levels"][0]
level = deepcopy(level_source)
level["id"] = "привет"
with pytest.raises(ScenarioNotCorrect):
- load_level(level, dcf)
+ load_level(level, retort)
level["keys"] = {"SHCamelCase"}
with pytest.raises(ScenarioNotCorrect):
- load_level(level, dcf)
+ load_level(level, retort)
level = deepcopy(level_source)
level["time_hints"] = level.pop("time-hints")
with pytest.raises(ScenarioNotCorrect):
- load_level(level, dcf)
+ load_level(level, retort)
-def test_deserialize_all_types(all_types_scn: RawGameScenario, dcf: Factory):
- game_scn = parse_game(all_types_scn, dcf)
+def test_deserialize_all_types(all_types_scn: RawGameScenario, retort: Retort):
+ game_scn = parse_game(all_types_scn, retort)
hints = game_scn.levels[0].time_hints
assert 12 == len(hints)
for i, type_ in enumerate(
@@ -79,8 +79,8 @@ def test_deserialize_all_types(all_types_scn: RawGameScenario, dcf: Factory):
assert hints[i].hint[0].type == type_.type # type: ignore[attr-defined]
-def test_render_all_types(all_types_scn: RawGameScenario, dcf: Factory):
- game_scn = parse_game(all_types_scn, dcf)
+def test_render_all_types(all_types_scn: RawGameScenario, retort: Retort):
+ game_scn = parse_game(all_types_scn, retort)
hints = [time_hint.hint[0] for time_hint in game_scn.levels[0].time_hints]
assert 12 == len(hints)
assert "📃📡🧭📷🎼🎬📎🌀🎤🤳🪪🏷" == render_hints(hints)
diff --git a/tests/unit/test_time_hint.py b/tests/unit/test_time_hint.py
new file mode 100644
index 00000000..9968159f
--- /dev/null
+++ b/tests/unit/test_time_hint.py
@@ -0,0 +1,55 @@
+import pytest
+
+from shvatka.core.models.dto import scn
+from shvatka.core.utils import exceptions
+
+
+def test_create_ok_time_hint():
+ time_hint = scn.TimeHint(
+ time=0,
+ hint=[scn.TextHint(text="some text")],
+ )
+ assert time_hint.time == 0
+ assert time_hint.hint == [scn.TextHint(text="some text")]
+
+
+def test_create_empty_time_hint():
+ with pytest.raises(exceptions.LevelError):
+ scn.TimeHint(time=5, hint=[])
+
+
+def test_edit_empty_time_hint():
+ time_hint = scn.TimeHint(
+ time=0,
+ hint=[scn.TextHint(text="some text")],
+ )
+ with pytest.raises(exceptions.LevelError):
+ time_hint.update_hint([])
+
+
+def test_ok_change_time():
+ time_hint = scn.TimeHint(
+ time=5,
+ hint=[scn.TextHint(text="some text")],
+ )
+ time_hint.update_time(10)
+ assert time_hint.time == 10
+ assert time_hint.hint == [scn.TextHint(text="some text")]
+
+
+def test_change_time_for_0():
+ time_hint = scn.TimeHint(
+ time=0,
+ hint=[scn.TextHint(text="some text")],
+ )
+ with pytest.raises(exceptions.LevelError):
+ time_hint.update_time(5)
+
+
+def test_change_time_to_0():
+ time_hint = scn.TimeHint(
+ time=5,
+ hint=[scn.TextHint(text="some text")],
+ )
+ with pytest.raises(exceptions.LevelError):
+ time_hint.update_time(0)