Spaces:
Runtime error
Runtime error
Commit
·
339f372
1
Parent(s):
cfe3b6e
Add application file
Browse files- .dockerignore +6 -0
- .gitignore +176 -0
- Dockerfile +12 -0
- app/__init__.py +0 -0
- app/config/config.py +12 -0
- app/database/__init__.py +0 -0
- app/database/base.py +15 -0
- app/database/models.py +142 -0
- app/database/requests.py +708 -0
- app/handlers/__init__.py +0 -0
- app/handlers/admin/__init__.py +0 -0
- app/handlers/admin/broadcast.py +34 -0
- app/handlers/admin/catalog.py +108 -0
- app/handlers/admin/leadmagnets.py +112 -0
- app/handlers/admin/router.py +31 -0
- app/handlers/admin/tests.py +203 -0
- app/handlers/admin/view_tests.py +57 -0
- app/handlers/admin_route.py +392 -0
- app/handlers/user/__init__.py +0 -0
- app/handlers/user/catalog.py +74 -0
- app/handlers/user/info_check.py +55 -0
- app/handlers/user/leadmagnets.py +71 -0
- app/handlers/user/messaging.py +28 -0
- app/handlers/user/registration.py +96 -0
- app/handlers/user/router.py +50 -0
- app/handlers/user/send_feedback.py +42 -0
- app/handlers/user/tests.py +74 -0
- app/handlers/user_route.py +168 -0
- app/keyboards/__init__.py +0 -0
- app/keyboards/admin_keyboards.py +84 -0
- app/keyboards/user_keyboards.py +98 -0
- app/middleware/__init__.py +0 -0
- app/middleware/authentification.py +10 -0
- app/states.py +77 -0
- app/utils/exceptions.py +7 -0
- docker-compose.yml +12 -0
- env.example +3 -0
- main.py +45 -0
- requirements.txt +25 -0
.dockerignore
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
__pycache__
|
2 |
+
*.pyc
|
3 |
+
.env
|
4 |
+
.git
|
5 |
+
.gitignore
|
6 |
+
README.md
|
.gitignore
ADDED
@@ -0,0 +1,176 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
### Python ###
|
2 |
+
# Byte-compiled / optimized / DLL files
|
3 |
+
__pycache__/
|
4 |
+
*.py[cod]
|
5 |
+
*$py.class
|
6 |
+
|
7 |
+
# C extensions
|
8 |
+
*.so
|
9 |
+
|
10 |
+
# Distribution / packaging
|
11 |
+
.Python
|
12 |
+
build/
|
13 |
+
develop-eggs/
|
14 |
+
dist/
|
15 |
+
downloads/
|
16 |
+
eggs/
|
17 |
+
.eggs/
|
18 |
+
lib/
|
19 |
+
lib64/
|
20 |
+
parts/
|
21 |
+
sdist/
|
22 |
+
var/
|
23 |
+
wheels/
|
24 |
+
share/python-wheels/
|
25 |
+
*.egg-info/
|
26 |
+
.installed.cfg
|
27 |
+
*.egg
|
28 |
+
MANIFEST
|
29 |
+
|
30 |
+
# PyInstaller
|
31 |
+
# Usually these files are written by a python script from a template
|
32 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
33 |
+
*.manifest
|
34 |
+
*.spec
|
35 |
+
|
36 |
+
# Installer logs
|
37 |
+
pip-log.txt
|
38 |
+
pip-delete-this-directory.txt
|
39 |
+
|
40 |
+
# Unit test / coverage reports
|
41 |
+
htmlcov/
|
42 |
+
.tox/
|
43 |
+
.nox/
|
44 |
+
.coverage
|
45 |
+
.coverage.*
|
46 |
+
.cache
|
47 |
+
nosetests.xml
|
48 |
+
coverage.xml
|
49 |
+
*.cover
|
50 |
+
*.py,cover
|
51 |
+
.hypothesis/
|
52 |
+
.pytest_cache/
|
53 |
+
cover/
|
54 |
+
|
55 |
+
# Translations
|
56 |
+
*.mo
|
57 |
+
*.pot
|
58 |
+
|
59 |
+
# Django stuff:
|
60 |
+
*.log
|
61 |
+
local_settings.py
|
62 |
+
db.sqlite3
|
63 |
+
db.sqlite3-journal
|
64 |
+
|
65 |
+
# Flask stuff:
|
66 |
+
instance/
|
67 |
+
.webassets-cache
|
68 |
+
|
69 |
+
# Scrapy stuff:
|
70 |
+
.scrapy
|
71 |
+
|
72 |
+
# Sphinx documentation
|
73 |
+
docs/_build/
|
74 |
+
|
75 |
+
# PyBuilder
|
76 |
+
.pybuilder/
|
77 |
+
target/
|
78 |
+
|
79 |
+
# Jupyter Notebook
|
80 |
+
.ipynb_checkpoints
|
81 |
+
|
82 |
+
# IPython
|
83 |
+
profile_default/
|
84 |
+
ipython_config.py
|
85 |
+
|
86 |
+
# pyenv
|
87 |
+
# For a library or package, you might want to ignore these files since the code is
|
88 |
+
# intended to run in multiple environments; otherwise, check them in:
|
89 |
+
# .python-version
|
90 |
+
|
91 |
+
# pipenv
|
92 |
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
93 |
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
94 |
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
95 |
+
# install all needed dependencies.
|
96 |
+
#Pipfile.lock
|
97 |
+
|
98 |
+
# poetry
|
99 |
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
100 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
101 |
+
# commonly ignored for libraries.
|
102 |
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
103 |
+
#poetry.lock
|
104 |
+
|
105 |
+
# pdm
|
106 |
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
107 |
+
#pdm.lock
|
108 |
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
109 |
+
# in version control.
|
110 |
+
# https://pdm.fming.dev/#use-with-ide
|
111 |
+
.pdm.toml
|
112 |
+
|
113 |
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
114 |
+
__pypackages__/
|
115 |
+
|
116 |
+
# Celery stuff
|
117 |
+
celerybeat-schedule
|
118 |
+
celerybeat.pid
|
119 |
+
|
120 |
+
# SageMath parsed files
|
121 |
+
*.sage.py
|
122 |
+
|
123 |
+
# Environments
|
124 |
+
.env
|
125 |
+
.venv
|
126 |
+
env/
|
127 |
+
venv/
|
128 |
+
ENV/
|
129 |
+
env.bak/
|
130 |
+
venv.bak/
|
131 |
+
|
132 |
+
# Spyder project settings
|
133 |
+
.spyderproject
|
134 |
+
.spyproject
|
135 |
+
|
136 |
+
# Rope project settings
|
137 |
+
.ropeproject
|
138 |
+
|
139 |
+
# mkdocs documentation
|
140 |
+
/site
|
141 |
+
|
142 |
+
# mypy
|
143 |
+
.mypy_cache/
|
144 |
+
.dmypy.json
|
145 |
+
dmypy.json
|
146 |
+
|
147 |
+
# Pyre type checker
|
148 |
+
.pyre/
|
149 |
+
|
150 |
+
# pytype static type analyzer
|
151 |
+
.pytype/
|
152 |
+
|
153 |
+
# Cython debug symbols
|
154 |
+
cython_debug/
|
155 |
+
|
156 |
+
# PyCharm
|
157 |
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
158 |
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
159 |
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
160 |
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
161 |
+
#.idea/
|
162 |
+
|
163 |
+
### Python Patch ###
|
164 |
+
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
|
165 |
+
poetry.toml
|
166 |
+
|
167 |
+
# ruff
|
168 |
+
.ruff_cache/
|
169 |
+
|
170 |
+
# LSP config files
|
171 |
+
pyrightconfig.json
|
172 |
+
run.py
|
173 |
+
# End of https://www.toptal.com/developers/gitignore/api/python
|
174 |
+
.env
|
175 |
+
*.sqlite3
|
176 |
+
__pycache__/
|
Dockerfile
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM python:3.9-slim
|
2 |
+
|
3 |
+
WORKDIR /app
|
4 |
+
|
5 |
+
COPY requirements.txt .
|
6 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
7 |
+
|
8 |
+
COPY . .
|
9 |
+
|
10 |
+
CMD ["python", "main.py"]
|
11 |
+
|
12 |
+
EXPOSE 7860
|
app/__init__.py
ADDED
File without changes
|
app/config/config.py
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
from typing import List
|
3 |
+
from dotenv import load_dotenv
|
4 |
+
|
5 |
+
load_dotenv()
|
6 |
+
|
7 |
+
BOT_TOKEN = os.getenv('BOT_TOKEN')
|
8 |
+
DATABASE_URL = os.getenv('DATABASE_URL')
|
9 |
+
ADMIN_ID = [int(id) for id in os.getenv('ADMIN_IDS', '').split(',')]
|
10 |
+
|
11 |
+
if not BOT_TOKEN:
|
12 |
+
raise ValueError("BOT_TOKEN not found in environment")
|
app/database/__init__.py
ADDED
File without changes
|
app/database/base.py
ADDED
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from contextlib import asynccontextmanager
|
2 |
+
from .models import async_session
|
3 |
+
|
4 |
+
@asynccontextmanager
|
5 |
+
async def get_session():
|
6 |
+
"""Provide a transactional scope around a series of operations."""
|
7 |
+
session = async_session()
|
8 |
+
try:
|
9 |
+
yield session
|
10 |
+
await session.commit()
|
11 |
+
except Exception:
|
12 |
+
await session.rollback()
|
13 |
+
raise
|
14 |
+
finally:
|
15 |
+
await session.close()
|
app/database/models.py
ADDED
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from datetime import datetime
|
2 |
+
from typing import Optional
|
3 |
+
from sqlalchemy import BigInteger, String, ForeignKey, Integer, DateTime, Boolean
|
4 |
+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
5 |
+
from sqlalchemy.ext.asyncio import AsyncAttrs, async_sessionmaker, create_async_engine
|
6 |
+
from app.config.config import DATABASE_URL
|
7 |
+
|
8 |
+
engine = create_async_engine(DATABASE_URL) # подключение и создание БД
|
9 |
+
async_session = async_sessionmaker(engine, expire_on_commit=False)
|
10 |
+
|
11 |
+
|
12 |
+
class Base(AsyncAttrs, DeclarativeBase):
|
13 |
+
"""Base class for all models"""
|
14 |
+
pass
|
15 |
+
|
16 |
+
|
17 |
+
class User(Base):
|
18 |
+
"""User model for storing telegram user data"""
|
19 |
+
__tablename__ = 'users'
|
20 |
+
|
21 |
+
id: Mapped[int] = mapped_column(primary_key=True)
|
22 |
+
tg_id: Mapped[int] = mapped_column(BigInteger, unique=True, nullable=False)
|
23 |
+
name: Mapped[str] = mapped_column(String(100), nullable=True)
|
24 |
+
login: Mapped[str] = mapped_column(String(100), nullable=True)
|
25 |
+
contact: Mapped[str] = mapped_column(String(100), nullable=True)
|
26 |
+
subscription_status: Mapped[str] = mapped_column(
|
27 |
+
String(20), default='inactive')
|
28 |
+
# Relationships
|
29 |
+
test_attempts = relationship("TestAttempt", back_populates="user")
|
30 |
+
feedback = relationship("Feedback", back_populates="user")
|
31 |
+
|
32 |
+
|
33 |
+
class Service(Base):
|
34 |
+
"""Service model for storing available services"""
|
35 |
+
__tablename__ = 'services'
|
36 |
+
|
37 |
+
id: Mapped[int] = mapped_column(primary_key=True)
|
38 |
+
service_name: Mapped[str] = mapped_column(String(100), unique=True, nullable=False)
|
39 |
+
service_description: Mapped[str] = mapped_column(String(500))
|
40 |
+
service_price: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
41 |
+
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
42 |
+
# Relationships
|
43 |
+
feedback = relationship("Feedback", back_populates="service")
|
44 |
+
|
45 |
+
|
46 |
+
class Test(Base):
|
47 |
+
"""Test model for storing quiz/test information"""
|
48 |
+
__tablename__ = 'tests'
|
49 |
+
|
50 |
+
id: Mapped[int] = mapped_column(primary_key=True)
|
51 |
+
test_name: Mapped[str] = mapped_column(String(100), unique=True, nullable=False)
|
52 |
+
test_type: Mapped[str] = mapped_column(String(20), nullable=False)
|
53 |
+
test_description: Mapped[str] = mapped_column(String(250))
|
54 |
+
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
55 |
+
completion_message: Mapped[Optional[str]] = mapped_column(String(1000))
|
56 |
+
# Relationships
|
57 |
+
questions = relationship("TestQuestion", back_populates="test", cascade="all, delete-orphan")
|
58 |
+
results = relationship("TestResult", back_populates="test", cascade="all, delete-orphan")
|
59 |
+
attempts = relationship("TestAttempt", back_populates="test")
|
60 |
+
|
61 |
+
|
62 |
+
class TestQuestion(Base):
|
63 |
+
"""Model for storing test questions"""
|
64 |
+
__tablename__ = 'test_questions'
|
65 |
+
|
66 |
+
id: Mapped[int] = mapped_column(primary_key=True)
|
67 |
+
test_id: Mapped[int] = mapped_column(ForeignKey('tests.id', ondelete='CASCADE'), nullable=False)
|
68 |
+
question_content: Mapped[str] = mapped_column(String(150), nullable=False)
|
69 |
+
question_variants: Mapped[str] = mapped_column(String(500), nullable=False) # JSON string
|
70 |
+
question_points: Mapped[str] = mapped_column(String(100), nullable=False) # JSON string
|
71 |
+
# Relationships
|
72 |
+
test = relationship("Test", back_populates="questions")
|
73 |
+
|
74 |
+
|
75 |
+
class TestResult(Base):
|
76 |
+
"""Model for storing possible test results"""
|
77 |
+
__tablename__ = 'test_results'
|
78 |
+
|
79 |
+
id: Mapped[int] = mapped_column(primary_key=True)
|
80 |
+
test_id: Mapped[int] = mapped_column(ForeignKey('tests.id', ondelete='CASCADE'), nullable=False)
|
81 |
+
min_points: Mapped[int] = mapped_column(Integer, nullable=False)
|
82 |
+
max_points: Mapped[int] = mapped_column(Integer, nullable=False)
|
83 |
+
result_text: Mapped[str] = mapped_column(String(1000), nullable=False)
|
84 |
+
# Relationships
|
85 |
+
test = relationship("Test", back_populates="results")
|
86 |
+
|
87 |
+
|
88 |
+
class TestAttempt(Base):
|
89 |
+
"""Model for storing user test attempts"""
|
90 |
+
__tablename__ = 'test_attempts'
|
91 |
+
|
92 |
+
id: Mapped[int] = mapped_column(primary_key=True)
|
93 |
+
user_id: Mapped[int] = mapped_column(ForeignKey('users.id', ondelete='CASCADE'), nullable=False)
|
94 |
+
test_id: Mapped[int] = mapped_column(ForeignKey('tests.id', ondelete='CASCADE'), nullable=False)
|
95 |
+
score: Mapped[int] = mapped_column(Integer, nullable=True)
|
96 |
+
result: Mapped[Optional[str]] = mapped_column(String(1000), nullable=True)
|
97 |
+
completed_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now, nullable=True)
|
98 |
+
# Relationships
|
99 |
+
user = relationship("User", back_populates="test_attempts")
|
100 |
+
test = relationship("Test", back_populates="attempts")
|
101 |
+
|
102 |
+
|
103 |
+
class TestAnswer(Base):
|
104 |
+
"""Stores individual answers for each question"""
|
105 |
+
__tablename__ = 'test_answers'
|
106 |
+
|
107 |
+
id: Mapped[int] = mapped_column(primary_key=True)
|
108 |
+
attempt_id: Mapped[int] = mapped_column(ForeignKey('test_attempts.id', ondelete='CASCADE'))
|
109 |
+
question_id: Mapped[int] = mapped_column(ForeignKey('test_questions.id', ondelete='CASCADE'))
|
110 |
+
answer_given: Mapped[str] = mapped_column(String(500))
|
111 |
+
points_earned: Mapped[int] = mapped_column(Integer, nullable=True)
|
112 |
+
|
113 |
+
|
114 |
+
class LeadMagnet(Base):
|
115 |
+
"""Model for storing lead magnets/content triggers"""
|
116 |
+
__tablename__ = 'lead_magnets'
|
117 |
+
|
118 |
+
id: Mapped[int] = mapped_column(primary_key=True)
|
119 |
+
trigger: Mapped[str] = mapped_column(String(50), unique=True, nullable=False)
|
120 |
+
content: Mapped[str] = mapped_column(String(500), nullable=False)
|
121 |
+
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
122 |
+
|
123 |
+
|
124 |
+
class Feedback(Base):
|
125 |
+
"""Model for storing user feedback"""
|
126 |
+
__tablename__ = 'feedback'
|
127 |
+
|
128 |
+
id: Mapped[int] = mapped_column(primary_key=True)
|
129 |
+
user_id: Mapped[int] = mapped_column(ForeignKey('users.id', ondelete='CASCADE'), nullable=False)
|
130 |
+
service_id: Mapped[int] = mapped_column(ForeignKey('services.id', ondelete='CASCADE'), nullable=False)
|
131 |
+
rating: Mapped[int] = mapped_column(Integer, nullable=False)
|
132 |
+
review: Mapped[str] = mapped_column(String(1000))
|
133 |
+
is_new: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
134 |
+
# Relationships
|
135 |
+
user = relationship("User", back_populates="feedback")
|
136 |
+
service = relationship("Service", back_populates="feedback")
|
137 |
+
|
138 |
+
|
139 |
+
async def async_main():
|
140 |
+
"""Initialize database tables"""
|
141 |
+
async with engine.begin() as conn:
|
142 |
+
await conn.run_sync(Base.metadata.create_all)
|
app/database/requests.py
ADDED
@@ -0,0 +1,708 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import Optional, List, Dict, Any
|
2 |
+
from sqlalchemy import select, update, delete, func
|
3 |
+
from sqlalchemy.ext.asyncio import AsyncSession
|
4 |
+
from app.database.models import *
|
5 |
+
from app.database.base import get_session
|
6 |
+
import json
|
7 |
+
from app.utils.exceptions import DatabaseError, ValidationError
|
8 |
+
|
9 |
+
|
10 |
+
async def set_user(tg_id: int) -> Optional[User]:
|
11 |
+
"""Create a new user if not exists or return existing user."""
|
12 |
+
async with get_session() as session:
|
13 |
+
try:
|
14 |
+
query = select(User).where(User.tg_id == tg_id)
|
15 |
+
user = await session.scalar(query)
|
16 |
+
if not user:
|
17 |
+
user = User(tg_id=tg_id)
|
18 |
+
session.add(user)
|
19 |
+
print("User added")
|
20 |
+
return user
|
21 |
+
except Exception as e:
|
22 |
+
raise DatabaseError(f"Error setting user: {str(e)}")
|
23 |
+
|
24 |
+
|
25 |
+
async def user_register(
|
26 |
+
tg_id: int,
|
27 |
+
name: str,
|
28 |
+
login: str,
|
29 |
+
contact: str,
|
30 |
+
subscribe: bool
|
31 |
+
) -> None:
|
32 |
+
"""Update user registration information."""
|
33 |
+
async with get_session() as session:
|
34 |
+
try:
|
35 |
+
query = update(User).where(User.tg_id == tg_id).values(
|
36 |
+
name=name,
|
37 |
+
login=login,
|
38 |
+
contact=contact,
|
39 |
+
subscription_status="active" if subscribe else "inactive"
|
40 |
+
)
|
41 |
+
await session.execute(query)
|
42 |
+
except Exception as e:
|
43 |
+
raise DatabaseError(f"Error registering user: {str(e)}")
|
44 |
+
|
45 |
+
|
46 |
+
async def check_login_unique(login: str) -> bool:
|
47 |
+
"""Check if login is available"""
|
48 |
+
async with get_session() as session:
|
49 |
+
user = await session.scalar(
|
50 |
+
select(User).where(User.login == login)
|
51 |
+
)
|
52 |
+
return user is None
|
53 |
+
|
54 |
+
|
55 |
+
async def get_catalog() -> Optional[List[str]]:
|
56 |
+
"""Get list of all service names."""
|
57 |
+
async with get_session() as session:
|
58 |
+
try:
|
59 |
+
query = select(Service).where(Service.is_active == True)
|
60 |
+
result = await session.execute(query)
|
61 |
+
services = result.scalars().all()
|
62 |
+
return services if services else None
|
63 |
+
except Exception as e:
|
64 |
+
raise DatabaseError(f"Error getting catalog: {str(e)}")
|
65 |
+
|
66 |
+
|
67 |
+
async def get_service_info(service_idx: str) -> Optional[Service]:
|
68 |
+
"""Get detailed information about a specific service."""
|
69 |
+
async with get_session() as session:
|
70 |
+
try:
|
71 |
+
query = select(Service).where(
|
72 |
+
Service.id == service_idx,
|
73 |
+
Service.is_active == True
|
74 |
+
)
|
75 |
+
service = await session.scalar(query)
|
76 |
+
return service if service else None
|
77 |
+
except Exception as e:
|
78 |
+
raise DatabaseError(f"Error getting service info: {str(e)}")
|
79 |
+
|
80 |
+
|
81 |
+
async def add_service(name: str, desc: str, price: int, active=bool) -> None:
|
82 |
+
"""Add a new service to the catalog."""
|
83 |
+
async with get_session() as session:
|
84 |
+
try:
|
85 |
+
service = Service(
|
86 |
+
service_name=name,
|
87 |
+
service_description=desc,
|
88 |
+
service_price=price,
|
89 |
+
is_active=active
|
90 |
+
)
|
91 |
+
session.add(service)
|
92 |
+
except Exception as e:
|
93 |
+
raise DatabaseError(f"Error adding service: {str(e)}")
|
94 |
+
|
95 |
+
|
96 |
+
async def edit_service(serv_id: int, param: str, change: Any, active: bool) -> None:
|
97 |
+
"""Edit an existing service."""
|
98 |
+
param_mapping = {
|
99 |
+
'name': 'service_name',
|
100 |
+
'desc': 'service_description',
|
101 |
+
'price': 'service_price'
|
102 |
+
}
|
103 |
+
|
104 |
+
if param not in param_mapping:
|
105 |
+
raise ValueError(f"Invalid parameter: {param}")
|
106 |
+
|
107 |
+
async with get_session() as session:
|
108 |
+
try:
|
109 |
+
query = update(Service).where(
|
110 |
+
Service.id == serv_id
|
111 |
+
).values({param_mapping[param]: change})
|
112 |
+
await session.execute(query)
|
113 |
+
except Exception as e:
|
114 |
+
raise DatabaseError(f"Error editing service: {str(e)}")
|
115 |
+
|
116 |
+
|
117 |
+
async def delete_service(serv_id: int) -> bool:
|
118 |
+
"""Delete a service from the catalog."""
|
119 |
+
async with get_session() as session:
|
120 |
+
try:
|
121 |
+
query = select(Service).where(Service.id == serv_id)
|
122 |
+
service = await session.scalar(query)
|
123 |
+
if not service:
|
124 |
+
return False
|
125 |
+
feedback_query = select(Feedback).where(Feedback.service_id == service.id)
|
126 |
+
has_feedback = await session.scalar(feedback_query)
|
127 |
+
if has_feedback:
|
128 |
+
update_query = (
|
129 |
+
update(Service)
|
130 |
+
.where(Service.id == serv_id)
|
131 |
+
.values(is_active=False)
|
132 |
+
)
|
133 |
+
await session.execute(update_query)
|
134 |
+
else:
|
135 |
+
await session.delete(service)
|
136 |
+
return True
|
137 |
+
except Exception as e:
|
138 |
+
raise DatabaseError(f"Error deleting service: {str(e)}")
|
139 |
+
|
140 |
+
|
141 |
+
async def get_leadmagnets() -> Optional[List[str]]:
|
142 |
+
"""Get list of all active lead magnets."""
|
143 |
+
async with get_session() as session:
|
144 |
+
try:
|
145 |
+
query = select(LeadMagnet.trigger).where(LeadMagnet.is_active == True)
|
146 |
+
result = await session.execute(query)
|
147 |
+
magnets = result.scalars().all()
|
148 |
+
return magnets if magnets else None
|
149 |
+
except Exception as e:
|
150 |
+
raise DatabaseError(f"Error getting lead magnets: {str(e)}")
|
151 |
+
|
152 |
+
|
153 |
+
async def get_leadmagnet_info(trigger: str) -> Optional[LeadMagnet]:
|
154 |
+
"""Get detailed information about a specific lead magnet."""
|
155 |
+
async with get_session() as session:
|
156 |
+
try:
|
157 |
+
query = select(LeadMagnet).where(
|
158 |
+
LeadMagnet.trigger == trigger,
|
159 |
+
LeadMagnet.is_active == True
|
160 |
+
)
|
161 |
+
magnet = await session.scalar(query)
|
162 |
+
return magnet if magnet else None
|
163 |
+
except Exception as e:
|
164 |
+
raise DatabaseError(f"Error getting lead magnet info: {str(e)}")
|
165 |
+
|
166 |
+
|
167 |
+
async def add_leadmagnet(trigger: str, content: str, active: bool) -> None:
|
168 |
+
"""Add a new lead magnet."""
|
169 |
+
async with get_session() as session:
|
170 |
+
try:
|
171 |
+
magnet = LeadMagnet(
|
172 |
+
trigger=trigger,
|
173 |
+
content=content,
|
174 |
+
is_active=active
|
175 |
+
)
|
176 |
+
session.add(magnet)
|
177 |
+
except Exception as e:
|
178 |
+
raise DatabaseError(f"Error adding lead magnet: {str(e)}")
|
179 |
+
|
180 |
+
|
181 |
+
async def edit_leadmagnet(name, param, change):
|
182 |
+
async with get_session() as session:
|
183 |
+
replace_dict = {'trigger': 'trigger',
|
184 |
+
'content': 'content',
|
185 |
+
'status': 'is_active'}
|
186 |
+
query = select(LeadMagnet).where(LeadMagnet.trigger == name)
|
187 |
+
result = await session.execute(query)
|
188 |
+
lead = result.scalars().first()
|
189 |
+
if lead:
|
190 |
+
update_query = (
|
191 |
+
update(LeadMagnet)
|
192 |
+
.where(LeadMagnet.trigger == name)
|
193 |
+
.values({replace_dict[param]: change})
|
194 |
+
.execution_options(synchronize_session="fetch")
|
195 |
+
)
|
196 |
+
await session.execute(update_query)
|
197 |
+
await session.commit()
|
198 |
+
|
199 |
+
|
200 |
+
async def delete_leadmagnet(name: str) -> None:
|
201 |
+
"""Delete a lead magnet."""
|
202 |
+
async with get_session() as session:
|
203 |
+
try:
|
204 |
+
query = delete(LeadMagnet).where(LeadMagnet.trigger == name)
|
205 |
+
await session.execute(query)
|
206 |
+
except Exception as e:
|
207 |
+
raise DatabaseError(f"Error deleting lead magnet: {str(e)}")
|
208 |
+
|
209 |
+
|
210 |
+
async def get_tests() -> Optional[List[str]]:
|
211 |
+
"""Get list of all active tests."""
|
212 |
+
async with get_session() as session:
|
213 |
+
try:
|
214 |
+
query = select(Test).where(Test.is_active == True)
|
215 |
+
result = await session.execute(query)
|
216 |
+
tests = result.scalars().all()
|
217 |
+
return tests if tests else None
|
218 |
+
except Exception as e:
|
219 |
+
raise DatabaseError(f"Error getting tests: {str(e)}")
|
220 |
+
|
221 |
+
|
222 |
+
async def add_test_wo_points(
|
223 |
+
name: str,
|
224 |
+
test_type: str,
|
225 |
+
desc: str,
|
226 |
+
status: bool,
|
227 |
+
completion_message: str
|
228 |
+
) -> None:
|
229 |
+
"""Add a new test without points system."""
|
230 |
+
async with get_session() as session:
|
231 |
+
try:
|
232 |
+
test = Test(
|
233 |
+
test_name=name,
|
234 |
+
test_type=test_type,
|
235 |
+
test_description=desc,
|
236 |
+
is_active=status,
|
237 |
+
completion_message=completion_message
|
238 |
+
)
|
239 |
+
session.add(test)
|
240 |
+
except Exception as e:
|
241 |
+
raise DatabaseError(f"Error adding test: {str(e)}")
|
242 |
+
|
243 |
+
|
244 |
+
async def add_question_vars_wo_points(test_name: str, text: str) -> None:
|
245 |
+
"""Add questions and variants to a test without points system."""
|
246 |
+
async with get_session() as session:
|
247 |
+
try:
|
248 |
+
# Get test ID
|
249 |
+
test = await session.scalar(
|
250 |
+
select(Test).where(Test.test_name == test_name)
|
251 |
+
)
|
252 |
+
if not test:
|
253 |
+
raise ValidationError(f"Test {test_name} not found")
|
254 |
+
|
255 |
+
# Split text into question and variants
|
256 |
+
parts = text.split('***')
|
257 |
+
if len(parts) != 2:
|
258 |
+
raise ValidationError("Invalid question format")
|
259 |
+
|
260 |
+
question = TestQuestion(
|
261 |
+
test_id=test.id,
|
262 |
+
question_content=parts[0].strip(),
|
263 |
+
question_variants=parts[1].strip(),
|
264 |
+
question_points="{}" # Empty JSON for non-pointed questions
|
265 |
+
)
|
266 |
+
session.add(question)
|
267 |
+
except Exception as e:
|
268 |
+
raise DatabaseError(f"Error adding question: {str(e)}")
|
269 |
+
|
270 |
+
|
271 |
+
async def add_test_result_w_points(test_name: str, text: str) -> None:
|
272 |
+
"""Add test results with point ranges."""
|
273 |
+
async with get_session() as session:
|
274 |
+
try:
|
275 |
+
test = await session.scalar(
|
276 |
+
select(Test).where(Test.test_name == test_name)
|
277 |
+
)
|
278 |
+
if not test:
|
279 |
+
raise ValidationError(f"Test {test_name} not found")
|
280 |
+
|
281 |
+
parts = text.split('\n')
|
282 |
+
if len(parts) != 2:
|
283 |
+
raise ValidationError("Invalid result format")
|
284 |
+
|
285 |
+
point_range = parts[0].strip()
|
286 |
+
min_points, max_points = map(int, point_range.split('-'))
|
287 |
+
|
288 |
+
result = TestResult(
|
289 |
+
test_id=test.id,
|
290 |
+
min_points=min_points,
|
291 |
+
max_points=max_points,
|
292 |
+
result_text=parts[1].strip()
|
293 |
+
)
|
294 |
+
session.add(result)
|
295 |
+
except Exception as e:
|
296 |
+
raise DatabaseError(f"Error adding test result: {str(e)}")
|
297 |
+
|
298 |
+
|
299 |
+
async def delete_test(t_id: int) -> None:
|
300 |
+
"""Delete a test and all related questions and results."""
|
301 |
+
async with get_session() as session:
|
302 |
+
try:
|
303 |
+
test = await session.scalar(
|
304 |
+
select(Test).where(Test.id == t_id)
|
305 |
+
)
|
306 |
+
if test:
|
307 |
+
await session.delete(test) # Cascade will handle related records
|
308 |
+
except Exception as e:
|
309 |
+
raise DatabaseError(f"Error deleting test: {str(e)}")
|
310 |
+
|
311 |
+
|
312 |
+
async def get_test(t_id: int) -> Optional[Dict[str, Any]]:
|
313 |
+
"""Get complete test information including questions and results."""
|
314 |
+
async with get_session() as session:
|
315 |
+
try:
|
316 |
+
test_query = select(Test).where(
|
317 |
+
Test.id == t_id,
|
318 |
+
Test.is_active == True
|
319 |
+
)
|
320 |
+
test = await session.scalar(test_query)
|
321 |
+
|
322 |
+
if not test:
|
323 |
+
return None
|
324 |
+
|
325 |
+
questions_query = select(TestQuestion).where(
|
326 |
+
TestQuestion.test_id == test.id
|
327 |
+
)
|
328 |
+
results_query = select(TestResult).where(
|
329 |
+
TestResult.test_id == test.id
|
330 |
+
)
|
331 |
+
|
332 |
+
questions = (await session.execute(questions_query)).scalars().all()
|
333 |
+
results = (await session.execute(results_query)).scalars().all()
|
334 |
+
|
335 |
+
return {
|
336 |
+
"id": t_id,
|
337 |
+
"test": test,
|
338 |
+
"questions": questions,
|
339 |
+
"results": results
|
340 |
+
}
|
341 |
+
except Exception as e:
|
342 |
+
raise DatabaseError(f"Error getting test: {str(e)}")
|
343 |
+
|
344 |
+
|
345 |
+
async def change_test_status(t_id: int, status: bool) -> None:
|
346 |
+
"""Change test active status."""
|
347 |
+
async with get_session() as session:
|
348 |
+
try:
|
349 |
+
query = update(Test).where(
|
350 |
+
Test.id == t_id
|
351 |
+
).values(is_active=True if status == "Да" else False)
|
352 |
+
await session.execute(query)
|
353 |
+
except Exception as e:
|
354 |
+
raise DatabaseError(f"Error changing test status: {str(e)}")
|
355 |
+
|
356 |
+
|
357 |
+
async def add_feedback(
|
358 |
+
user_id: int,
|
359 |
+
service_name: str,
|
360 |
+
rating: int,
|
361 |
+
review: str
|
362 |
+
) -> None:
|
363 |
+
"""Add new feedback for a service."""
|
364 |
+
async with get_session() as session:
|
365 |
+
try:
|
366 |
+
service = await session.scalar(
|
367 |
+
select(Service).where(Service.service_name == service_name)
|
368 |
+
)
|
369 |
+
if not service:
|
370 |
+
raise ValidationError(f"Service {service_name} not found")
|
371 |
+
|
372 |
+
feedback = Feedback(
|
373 |
+
user_id=user_id,
|
374 |
+
service_id=service.id,
|
375 |
+
rating=rating,
|
376 |
+
review=review,
|
377 |
+
is_new=True
|
378 |
+
)
|
379 |
+
session.add(feedback)
|
380 |
+
except Exception as e:
|
381 |
+
raise DatabaseError(f"Error adding feedback: {str(e)}")
|
382 |
+
|
383 |
+
|
384 |
+
async def get_new_feedback() -> Optional[List[Feedback]]:
|
385 |
+
"""Get all new feedback entries."""
|
386 |
+
async with get_session() as session:
|
387 |
+
try:
|
388 |
+
query = select(Feedback).where(Feedback.is_new == True)
|
389 |
+
result = await session.execute(query)
|
390 |
+
feedback = result.scalars().all()
|
391 |
+
return feedback if feedback else None
|
392 |
+
except Exception as e:
|
393 |
+
raise DatabaseError(f"Error getting new feedback: {str(e)}")
|
394 |
+
|
395 |
+
|
396 |
+
async def mark_feedback_as_read(feedback_id: int) -> None:
|
397 |
+
"""Mark feedback as read."""
|
398 |
+
async with get_session() as session:
|
399 |
+
try:
|
400 |
+
query = update(Feedback).where(
|
401 |
+
Feedback.id == feedback_id
|
402 |
+
).values(is_new=False)
|
403 |
+
await session.execute(query)
|
404 |
+
except Exception as e:
|
405 |
+
raise DatabaseError(f"Error marking feedback as read: {str(e)}")
|
406 |
+
|
407 |
+
|
408 |
+
async def get_user_info(tg_id: int) -> Optional[User]:
|
409 |
+
"""Get user information by Telegram ID"""
|
410 |
+
async with get_session() as session:
|
411 |
+
try:
|
412 |
+
query = select(User).where(User.tg_id == tg_id)
|
413 |
+
user = await session.scalar(query)
|
414 |
+
return user
|
415 |
+
except Exception as e:
|
416 |
+
raise DatabaseError(f"Error getting user info: {str(e)}")
|
417 |
+
|
418 |
+
|
419 |
+
async def start_test_attempt(user_id: int, test_id: str) -> Optional[Dict[str, Any]]:
|
420 |
+
"""Create new test attempt and return first question"""
|
421 |
+
async with get_session() as session:
|
422 |
+
try:
|
423 |
+
test = await session.scalar(
|
424 |
+
select(Test).where(
|
425 |
+
Test.id == test_id,
|
426 |
+
Test.is_active == True
|
427 |
+
)
|
428 |
+
)
|
429 |
+
if not test:
|
430 |
+
return None
|
431 |
+
user = await session.scalar(
|
432 |
+
select(User).where(User.tg_id == user_id)
|
433 |
+
)
|
434 |
+
if not user:
|
435 |
+
return None
|
436 |
+
|
437 |
+
# Create test attempt
|
438 |
+
attempt = TestAttempt(
|
439 |
+
user_id=user_id,
|
440 |
+
test_id=test.id
|
441 |
+
)
|
442 |
+
session.add(attempt)
|
443 |
+
await session.flush() # Get attempt ID
|
444 |
+
|
445 |
+
# Get first question
|
446 |
+
question = await session.scalar(
|
447 |
+
select(TestQuestion)
|
448 |
+
.where(TestQuestion.test_id == test.id)
|
449 |
+
.order_by(TestQuestion.id)
|
450 |
+
)
|
451 |
+
await session.commit()
|
452 |
+
return {
|
453 |
+
"attempt_id": attempt.id,
|
454 |
+
"question": question,
|
455 |
+
"total_questions": await session.scalar(
|
456 |
+
select(func.count()).select_from(TestQuestion)
|
457 |
+
.where(TestQuestion.test_id == test.id)
|
458 |
+
)
|
459 |
+
}
|
460 |
+
except Exception as e:
|
461 |
+
raise DatabaseError(f"Error starting test: {str(e)}")
|
462 |
+
|
463 |
+
|
464 |
+
async def record_answer(attempt_id: int, question_id: int, answer: str) -> Optional[Dict[str, Any]]:
|
465 |
+
"""Record user's answer and return next question or result"""
|
466 |
+
async with get_session() as session:
|
467 |
+
try:
|
468 |
+
# Get the test attempt first
|
469 |
+
attempt = await session.scalar(
|
470 |
+
select(TestAttempt).where(TestAttempt.id == attempt_id)
|
471 |
+
)
|
472 |
+
if not attempt:
|
473 |
+
raise DatabaseError("Test attempt not found")
|
474 |
+
|
475 |
+
# Get question and test
|
476 |
+
question = await session.scalar(
|
477 |
+
select(TestQuestion).where(TestQuestion.id == question_id)
|
478 |
+
)
|
479 |
+
if not question:
|
480 |
+
raise DatabaseError("Question not found")
|
481 |
+
|
482 |
+
test = await session.scalar(
|
483 |
+
select(Test).where(Test.id == question.test_id)
|
484 |
+
)
|
485 |
+
|
486 |
+
# Calculate points
|
487 |
+
points = 0
|
488 |
+
if test.test_type == "С баллами":
|
489 |
+
variants_raw = question.question_variants.split('\n')
|
490 |
+
for variant in variants_raw:
|
491 |
+
if variant.strip():
|
492 |
+
try:
|
493 |
+
variant_parts = variant.strip().split('...')
|
494 |
+
if len(variant_parts) == 2:
|
495 |
+
variant_text, points_str = variant_parts
|
496 |
+
if variant_text.strip() == answer.split("...")[0].strip():
|
497 |
+
points = int(points_str.strip())
|
498 |
+
break
|
499 |
+
except ValueError:
|
500 |
+
continue
|
501 |
+
|
502 |
+
# Create and save answer record
|
503 |
+
answer_record = TestAnswer(
|
504 |
+
attempt_id=attempt_id,
|
505 |
+
question_id=question_id,
|
506 |
+
answer_given=answer,
|
507 |
+
points_earned=points
|
508 |
+
)
|
509 |
+
session.add(answer_record)
|
510 |
+
await session.flush()
|
511 |
+
|
512 |
+
# Get next question
|
513 |
+
next_question = await session.scalar(
|
514 |
+
select(TestQuestion)
|
515 |
+
.where(TestQuestion.test_id == test.id)
|
516 |
+
.where(TestQuestion.id > question_id)
|
517 |
+
.order_by(TestQuestion.id)
|
518 |
+
)
|
519 |
+
|
520 |
+
if next_question:
|
521 |
+
await session.commit()
|
522 |
+
return {"next_question": next_question}
|
523 |
+
|
524 |
+
# If no next question, test is complete
|
525 |
+
# Calculate total score
|
526 |
+
answers = await session.scalars(
|
527 |
+
select(TestAnswer)
|
528 |
+
.where(TestAnswer.attempt_id == attempt_id)
|
529 |
+
)
|
530 |
+
total_score = sum(ans.points_earned for ans in answers.all())
|
531 |
+
|
532 |
+
# Update attempt with final score
|
533 |
+
attempt.score = total_score
|
534 |
+
|
535 |
+
if test.test_type == "С баллами":
|
536 |
+
# Get appropriate result
|
537 |
+
result = await session.scalar(
|
538 |
+
select(TestResult)
|
539 |
+
.where(TestResult.test_id == test.id)
|
540 |
+
.where(TestResult.min_points <= total_score)
|
541 |
+
.where(TestResult.max_points >= total_score)
|
542 |
+
)
|
543 |
+
|
544 |
+
attempt.result = result.result_text if result else None
|
545 |
+
|
546 |
+
result_dict = {
|
547 |
+
"completed": True,
|
548 |
+
"total_points": total_score,
|
549 |
+
"result": result.result_text if result else None
|
550 |
+
}
|
551 |
+
else:
|
552 |
+
result_dict = {
|
553 |
+
"completed": True,
|
554 |
+
"result": test.completion_message
|
555 |
+
}
|
556 |
+
attempt.result = test.completion_message
|
557 |
+
|
558 |
+
await session.commit()
|
559 |
+
return result_dict
|
560 |
+
|
561 |
+
except Exception as e:
|
562 |
+
await session.rollback()
|
563 |
+
raise DatabaseError(f"Error recording answer: {str(e)}")
|
564 |
+
|
565 |
+
|
566 |
+
async def check_user_registered(user_id: int) -> bool:
|
567 |
+
"""Check if user has completed registration"""
|
568 |
+
async with get_session() as session:
|
569 |
+
try:
|
570 |
+
user = await session.scalar(
|
571 |
+
select(User)
|
572 |
+
.where(User.tg_id == user_id)
|
573 |
+
)
|
574 |
+
print(f"User found: {user}") # Debug print
|
575 |
+
return bool(user.name)
|
576 |
+
except Exception as e:
|
577 |
+
raise DatabaseError(f"Error checking user registration: {str(e)}")
|
578 |
+
|
579 |
+
|
580 |
+
async def get_user_test_results(user_login: str) -> List[Dict[str, Any]]:
|
581 |
+
"""Get all test results for a user"""
|
582 |
+
async with get_session() as session:
|
583 |
+
try:
|
584 |
+
user = await session.scalar(
|
585 |
+
select(User).where(User.login == user_login)
|
586 |
+
)
|
587 |
+
if not user:
|
588 |
+
return "Пользователь не найден"
|
589 |
+
attempts = await session.execute(
|
590 |
+
select(TestAttempt, Test)
|
591 |
+
.join(Test)
|
592 |
+
.where(TestAttempt.user_id == user.tg_id)
|
593 |
+
.order_by(TestAttempt.completed_at.desc())
|
594 |
+
)
|
595 |
+
if attempts:
|
596 |
+
return ([
|
597 |
+
{
|
598 |
+
"test_name": test.test_name,
|
599 |
+
"completed_at": attempt.completed_at,
|
600 |
+
"score": attempt.score,
|
601 |
+
"result": attempt.result
|
602 |
+
}
|
603 |
+
for attempt, test in attempts
|
604 |
+
])
|
605 |
+
|
606 |
+
except Exception as e:
|
607 |
+
raise DatabaseError(f"Error getting test results: {str(e)}")
|
608 |
+
|
609 |
+
|
610 |
+
async def get_user_registration_info(user_id: int) -> str:
|
611 |
+
"""Get formatted user registration information"""
|
612 |
+
async with get_session() as session:
|
613 |
+
try:
|
614 |
+
user = await session.scalar(
|
615 |
+
select(User).where(User.tg_id == user_id)
|
616 |
+
)
|
617 |
+
if not user:
|
618 |
+
return "Информация о пользователе не найдена"
|
619 |
+
|
620 |
+
return (
|
621 |
+
"📋 Ваша регистрационная информация:\n"
|
622 |
+
f"ID: {user.tg_id}\n"
|
623 |
+
f"Имя: {user.name or 'Не указано'}\n"
|
624 |
+
f"Логин: {user.login or 'Не указано'}\n"
|
625 |
+
f"Контакт: {user.contact or 'Не указано'}\n"
|
626 |
+
f"Статус подписки: {'Активна' if user.subscription_status == 'active' else 'Неактивна'}"
|
627 |
+
)
|
628 |
+
except Exception as e:
|
629 |
+
raise DatabaseError(f"Error getting user info: {str(e)}")
|
630 |
+
|
631 |
+
|
632 |
+
async def get_all_test_answers() -> List[Dict[str, Any]]:
|
633 |
+
"""Fetch all test answers with related information"""
|
634 |
+
async with get_session() as session:
|
635 |
+
try:
|
636 |
+
result = await session.execute(
|
637 |
+
select(TestAnswer, TestAttempt, User, Test, TestQuestion)
|
638 |
+
.join(TestAttempt, TestAttempt.id == TestAnswer.attempt_id)
|
639 |
+
.join(User, User.id == TestAttempt.user_id)
|
640 |
+
.join(Test, Test.id == TestAttempt.test_id)
|
641 |
+
.join(TestQuestion, TestQuestion.id == TestAnswer.question_id)
|
642 |
+
.order_by(TestAttempt.completed_at.desc())
|
643 |
+
)
|
644 |
+
answers = result.fetchall()
|
645 |
+
print(answers) # Debug print
|
646 |
+
|
647 |
+
return [
|
648 |
+
{
|
649 |
+
"answer_id": answer.id,
|
650 |
+
"user_name": user.name,
|
651 |
+
"test_name": test.test_name,
|
652 |
+
"question": question.question_content,
|
653 |
+
"answer_given": answer.answer_given,
|
654 |
+
"points_earned": answer.points_earned,
|
655 |
+
"completed_at": attempt.completed_at.strftime("%d.%m.%Y %H:%M")
|
656 |
+
}
|
657 |
+
for answer, attempt, user, test, question in answers
|
658 |
+
]
|
659 |
+
|
660 |
+
except Exception as e:
|
661 |
+
raise DatabaseError(f"Error fetching test answers: {str(e)}")
|
662 |
+
|
663 |
+
|
664 |
+
async def own_login_check(user_id: int, login: str) -> bool:
|
665 |
+
"""Check if the provided login matches the user's login"""
|
666 |
+
async with get_session() as session:
|
667 |
+
try:
|
668 |
+
user = await session.scalar(
|
669 |
+
select(User).where(User.tg_id == user_id)
|
670 |
+
)
|
671 |
+
if not user:
|
672 |
+
return False
|
673 |
+
return user.login == login
|
674 |
+
except Exception as e:
|
675 |
+
raise DatabaseError(f"Error checking login: {str(e)}")
|
676 |
+
|
677 |
+
|
678 |
+
async def update_user_data(user_id: int, param: str, change: Any) -> None:
|
679 |
+
async with get_session() as session:
|
680 |
+
replace_dict = {'Имя': 'name',
|
681 |
+
'Логин': 'login',
|
682 |
+
'Контакт': 'contact',
|
683 |
+
'Статус подписки на рассылку': 'subscription_status'}
|
684 |
+
query = select(User).where(User.tg_id == user_id)
|
685 |
+
result = await session.execute(query)
|
686 |
+
user = result.scalars().first()
|
687 |
+
if user:
|
688 |
+
update_query = (
|
689 |
+
update(User)
|
690 |
+
.where(User.tg_id == user_id)
|
691 |
+
.values({replace_dict[param]: change})
|
692 |
+
.execution_options(synchronize_session="fetch")
|
693 |
+
)
|
694 |
+
await session.execute(update_query)
|
695 |
+
await session.commit()
|
696 |
+
|
697 |
+
|
698 |
+
async def get_broadcast_users() -> List[int]:
|
699 |
+
"""Fetch all users for broadcasting"""
|
700 |
+
async with get_session() as session:
|
701 |
+
try:
|
702 |
+
result = await session.scalars(
|
703 |
+
select(User.tg_id)
|
704 |
+
.where(User.subscription_status == 'active')
|
705 |
+
)
|
706 |
+
return result.fetchall()
|
707 |
+
except Exception as e:
|
708 |
+
raise DatabaseError(f"Error fetching broadcast users: {str(e)}")
|
app/handlers/__init__.py
ADDED
File without changes
|
app/handlers/admin/__init__.py
ADDED
File without changes
|
app/handlers/admin/broadcast.py
ADDED
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from aiogram import Router, F
|
2 |
+
from aiogram.types import Message, CallbackQuery
|
3 |
+
from aiogram.fsm.context import FSMContext
|
4 |
+
from app.database import requests as rq
|
5 |
+
from app.keyboards import admin_keyboards as kb
|
6 |
+
from app.states import Other
|
7 |
+
from app.database.requests import get_broadcast_users
|
8 |
+
from app.middleware.authentification import admin_check
|
9 |
+
from time import sleep
|
10 |
+
from random import randint
|
11 |
+
|
12 |
+
router = Router()
|
13 |
+
|
14 |
+
@router.message(F.text == 'Отправить сообщение в рассылку')
|
15 |
+
async def compose_message(message: Message, state: FSMContext):
|
16 |
+
if not await admin_check(message, {}):
|
17 |
+
await message.answer("У вас нет доступа к админ-панели")
|
18 |
+
return
|
19 |
+
await state.set_state(Other.admin_send_mailing)
|
20 |
+
await message.answer('Напишите Ваше сообщение')
|
21 |
+
|
22 |
+
|
23 |
+
@router.message(Other.admin_send_mailing)
|
24 |
+
async def send_message(message: Message, state: FSMContext):
|
25 |
+
user_ids = await get_broadcast_users()
|
26 |
+
if not user_ids:
|
27 |
+
await message.answer("Нет подписчиков для рассылки")
|
28 |
+
return
|
29 |
+
for user_id in user_ids:
|
30 |
+
await message.copy_to(user_id)
|
31 |
+
sleep(randint(1,5))
|
32 |
+
await message.answer(f"Сообщение отправлено {len(user_ids)} пользователям")
|
33 |
+
await state.clear()
|
34 |
+
|
app/handlers/admin/catalog.py
ADDED
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from aiogram import Router, F
|
2 |
+
from aiogram.types import Message, CallbackQuery
|
3 |
+
from aiogram.fsm.context import FSMContext
|
4 |
+
from app.database import requests as rq
|
5 |
+
from app.keyboards import admin_keyboards as kb
|
6 |
+
from app.states import AdminAddService, AdminStates
|
7 |
+
from app.middleware.authentification import admin_check
|
8 |
+
|
9 |
+
router = Router()
|
10 |
+
|
11 |
+
@router.message(F.text == 'Отредактировать каталог услуг')
|
12 |
+
async def catalog_editor_start(message: Message):
|
13 |
+
if not await admin_check(message, {}):
|
14 |
+
await message.answer("У вас нет доступа к админ-панели")
|
15 |
+
return
|
16 |
+
await message.answer(
|
17 |
+
'Текущие услуги. Нажмите на услугу для редактирования',
|
18 |
+
reply_markup=await kb.admin_keyboard_service_catalog()
|
19 |
+
)
|
20 |
+
|
21 |
+
|
22 |
+
@router.callback_query(F.data.startswith('change_service_'))
|
23 |
+
async def edit_service(callback: CallbackQuery):
|
24 |
+
await callback.answer('')
|
25 |
+
service = await rq.get_service_info(callback.data.split('_')[2])
|
26 |
+
await callback.message.answer(f'Услуга {service.service_name} \n\n'
|
27 |
+
f'Описание: {service.service_description}\n'
|
28 |
+
f'Цена: {service.service_price} рублей \n\n'
|
29 |
+
f'Что Вы хотите изменить?',
|
30 |
+
reply_markup=await kb.admin_change_service(service.id))
|
31 |
+
|
32 |
+
|
33 |
+
@router.callback_query(F.data == 'add_service')
|
34 |
+
async def add_service(callback: CallbackQuery, state: FSMContext):
|
35 |
+
await callback.answer('Добавляем услугу')
|
36 |
+
await state.set_state(AdminAddService.admin_add_name)
|
37 |
+
await callback.message.answer('Введите название новой услуги')
|
38 |
+
|
39 |
+
|
40 |
+
@router.message(AdminAddService.admin_add_name)
|
41 |
+
async def add_name(message: Message, state: FSMContext):
|
42 |
+
await state.update_data(name=message.text)
|
43 |
+
await state.set_state(AdminAddService.admin_add_desc)
|
44 |
+
await message.answer('Введите описание новой услуги')
|
45 |
+
|
46 |
+
|
47 |
+
@router.message(AdminAddService.admin_add_desc)
|
48 |
+
async def add_desc(message: Message, state: FSMContext):
|
49 |
+
await state.update_data(desc=message.text)
|
50 |
+
await state.set_state(AdminAddService.admin_add_price)
|
51 |
+
await message.answer('Введите цену услуги в рублях')
|
52 |
+
|
53 |
+
|
54 |
+
@router.message(AdminAddService.admin_add_price)
|
55 |
+
async def add_price(message: Message, state: FSMContext):
|
56 |
+
await state.update_data(price=message.text)
|
57 |
+
data = await state.get_data()
|
58 |
+
await rq.add_service(data['name'], data['desc'], data['price'], True)
|
59 |
+
await message.answer('Услуга сохранена! Теперь можно изменить ее, выбрав услугу в меню и отредактировав',
|
60 |
+
reply_markup=kb.admin_back)
|
61 |
+
await state.clear()
|
62 |
+
|
63 |
+
|
64 |
+
@router.callback_query(F.data.startswith('editservice_'))
|
65 |
+
async def begin_service_edit(callback: CallbackQuery, state: FSMContext):
|
66 |
+
await callback.answer('')
|
67 |
+
await state.set_state(AdminStates.admin_edit_service)
|
68 |
+
await state.update_data(service_id=callback.data.split('_')[2])
|
69 |
+
await state.update_data(parameter_changed=callback.data.split("_")[1])
|
70 |
+
replace_dict = {'name': 'ое название', 'desc': 'ое описание', 'price': 'ую цену'}
|
71 |
+
await callback.message.answer(f'Введите нов{replace_dict[callback.data.split("_")[1]]} услуги')
|
72 |
+
|
73 |
+
|
74 |
+
@router.message(AdminStates.admin_edit_service)
|
75 |
+
async def finish_service_edit(message: Message, state: FSMContext):
|
76 |
+
await state.update_data(change=message.text)
|
77 |
+
data = await state.get_data()
|
78 |
+
await rq.edit_service(data['service_id'], data['parameter_changed'], data['change'], True)
|
79 |
+
await state.clear()
|
80 |
+
await message.answer(f'Информация была обновлена, можно вернуться на главную', reply_markup=kb.admin_back)
|
81 |
+
|
82 |
+
|
83 |
+
@router.callback_query(F.data.startswith('deleteservice_'))
|
84 |
+
async def confirm_service_deletion(callback: CallbackQuery, state: FSMContext):
|
85 |
+
await callback.answer('')
|
86 |
+
await state.set_state(AdminStates.admin_delete_service)
|
87 |
+
await state.update_data(serv_id=callback.data.split("_")[1])
|
88 |
+
await callback.message.answer(f'Вы уверены, что хотите удалить услугу?',
|
89 |
+
reply_markup=kb.yes_no_keyboard)
|
90 |
+
|
91 |
+
|
92 |
+
@router.message(AdminStates.admin_delete_service)
|
93 |
+
async def delete_or_not_delete(message: Message, state: FSMContext):
|
94 |
+
if message.text == 'Нет':
|
95 |
+
await state.clear()
|
96 |
+
await message.answer('Тогда вернемся в меню :)', reply_markup=kb.admin_back)
|
97 |
+
|
98 |
+
if message.text == 'Да':
|
99 |
+
data = await state.get_data()
|
100 |
+
await rq.delete_service(data['serv_id'])
|
101 |
+
await state.clear()
|
102 |
+
await message.answer('Услуга была успешно удалена! Можно вернуться в меню', reply_markup=kb.admin_back)
|
103 |
+
|
104 |
+
|
105 |
+
@router.callback_query(F.data == 'admin_to_main')
|
106 |
+
async def return_to_main(callback: CallbackQuery):
|
107 |
+
await callback.answer('Возвращаемся в меню')
|
108 |
+
await callback.message.answer('Здравствуйте! Что Вы хотите сделать?', reply_markup=kb.admin_main)
|
app/handlers/admin/leadmagnets.py
ADDED
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from aiogram import Router, F
|
2 |
+
from aiogram.types import Message, CallbackQuery
|
3 |
+
from aiogram.fsm.context import FSMContext
|
4 |
+
from app.database import requests as rq
|
5 |
+
from app.keyboards import admin_keyboards as kb
|
6 |
+
from app.states import AdminAddLeadmagnet, AdminStates
|
7 |
+
from app.middleware.authentification import admin_check
|
8 |
+
|
9 |
+
router = Router()
|
10 |
+
|
11 |
+
@router.message(F.text == 'Отредактировать кодовые слова')
|
12 |
+
async def view_leadmagnets(message: Message):
|
13 |
+
if not await admin_check(message, {}):
|
14 |
+
await message.answer("У вас нет доступа к админ-панели")
|
15 |
+
return
|
16 |
+
await message.answer(
|
17 |
+
'Текущие лидмагниты. Нажмите для редактирования',
|
18 |
+
reply_markup=await kb.admin_keyboard_leadmagnets()
|
19 |
+
)
|
20 |
+
|
21 |
+
|
22 |
+
@router.callback_query(F.data == 'add_leadmanget')
|
23 |
+
async def add_leadmagnet(callback: CallbackQuery, state: FSMContext):
|
24 |
+
await callback.answer('Добавляем лидмагнит')
|
25 |
+
await state.set_state(AdminAddLeadmagnet.admin_set_trigger)
|
26 |
+
await callback.message.answer('Введите кодовое слово (пожалуйста, не используйте "_" в названии!)')
|
27 |
+
|
28 |
+
|
29 |
+
@router.message(AdminAddLeadmagnet.admin_set_trigger)
|
30 |
+
async def add_trigger(message: Message, state: FSMContext):
|
31 |
+
await state.update_data(trigger=message.text)
|
32 |
+
await state.set_state(AdminAddLeadmagnet.admin_set_content)
|
33 |
+
await message.answer('Введите содержание лидмагнита '
|
34 |
+
'(сообщение, которое получит пользователь после отправки кодового слова)')
|
35 |
+
|
36 |
+
|
37 |
+
@router.message(AdminAddLeadmagnet.admin_set_content)
|
38 |
+
async def add_content(message: Message, state: FSMContext):
|
39 |
+
await state.update_data(content=message.text)
|
40 |
+
await state.set_state(AdminAddLeadmagnet.admin_set_status)
|
41 |
+
await message.answer('Хотите ли Вы сделать данный лидмагнит активным? Активные лидмагниты будут тут же доступны для '
|
42 |
+
'взаимодействия в пользовательском интерфейсе.', reply_markup=kb.yes_no_keyboard)
|
43 |
+
|
44 |
+
|
45 |
+
@router.message(AdminAddLeadmagnet.admin_set_status)
|
46 |
+
async def set_leadmagnet_status(message: Message, state: FSMContext):
|
47 |
+
await state.update_data(status=message.text)
|
48 |
+
data = await state.get_data()
|
49 |
+
await rq.add_leadmagnet(data['trigger'], data['content'], True if data['status'] == 'Да' else False)
|
50 |
+
await message.answer('Лидмагнит сохранен! Теперь можно отредактировать его, выбрав его в меню',
|
51 |
+
reply_markup=kb.admin_back)
|
52 |
+
await state.clear()
|
53 |
+
|
54 |
+
|
55 |
+
@router.callback_query(F.data.startswith('change_leadmagnet_'))
|
56 |
+
async def edit_leadmagnet(callback: CallbackQuery):
|
57 |
+
await callback.answer('')
|
58 |
+
leadmagnet = await rq.get_leadmagnet_info(callback.data.split('_')[2])
|
59 |
+
await callback.message.answer(f'Триггер: {leadmagnet.trigger} \n\n'
|
60 |
+
f'Содержание: {leadmagnet.content}\n'
|
61 |
+
f'Активен? {leadmagnet.is_active}\n\n'
|
62 |
+
f'Что Вы хотите изменить?',
|
63 |
+
reply_markup=await kb.admin_change_leadmagnet(leadmagnet.trigger))
|
64 |
+
|
65 |
+
|
66 |
+
@router.callback_query(F.data.startswith('editleadmagnet_'))
|
67 |
+
async def begin_leadmagnet_edit(callback: CallbackQuery, state: FSMContext):
|
68 |
+
await callback.answer('')
|
69 |
+
await state.set_state(AdminStates.admin_edit_leadmagnet)
|
70 |
+
await state.update_data(leadmagnet=callback.data.split('_')[2])
|
71 |
+
await state.update_data(parameter_changed=callback.data.split("_")[1])
|
72 |
+
replace_dict = {'trigger': 'ый триггер', 'content': 'ое содержание'}
|
73 |
+
if callback.data.split("_")[1] != 'status':
|
74 |
+
await callback.message.answer(f'Введите нов{replace_dict[callback.data.split("_")[1]]} лидмагнита')
|
75 |
+
elif callback.data.split("_")[1] == 'status':
|
76 |
+
await callback.message.answer(f'Должен ли лидмагнит быть активным?', reply_markup=kb.yes_no_keyboard)
|
77 |
+
|
78 |
+
|
79 |
+
@router.message(AdminStates.admin_edit_leadmagnet)
|
80 |
+
async def finish_leadmagnet_edit(message: Message, state: FSMContext):
|
81 |
+
await state.update_data(change=message.text)
|
82 |
+
data = await state.get_data()
|
83 |
+
await rq.edit_leadmagnet(data['leadmagnet'], data['parameter_changed'], True if data['change'] == 'Да' else False)
|
84 |
+
await message.answer(f'Информация была обновлена, можно вернуться на главную', reply_markup=kb.admin_back)
|
85 |
+
|
86 |
+
|
87 |
+
@router.callback_query(F.data.startswith('deleteleadmagnet_'))
|
88 |
+
async def confirm_leadmagnet_deletion(callback: CallbackQuery, state: FSMContext):
|
89 |
+
await callback.answer('')
|
90 |
+
await state.set_state(AdminStates.admin_delete_leadmagnet)
|
91 |
+
await state.update_data(leadmagnet=callback.data.split("_")[1])
|
92 |
+
await callback.message.answer(f'Вы уверены, что хотите удалить лидмагнит {callback.data.split("_")[1]}?',
|
93 |
+
reply_markup=kb.yes_no_keyboard)
|
94 |
+
|
95 |
+
|
96 |
+
@router.message(AdminStates.admin_delete_leadmagnet)
|
97 |
+
async def delete_or_not_delete(message: Message, state: FSMContext):
|
98 |
+
if message.text == 'Нет':
|
99 |
+
await state.clear()
|
100 |
+
await message.answer('Тогда вернемся в меню :)', reply_markup=kb.admin_back)
|
101 |
+
|
102 |
+
if message.text == 'Да':
|
103 |
+
data = await state.get_data()
|
104 |
+
await rq.delete_leadmagnet(data['leadmagnet'])
|
105 |
+
await state.clear()
|
106 |
+
await message.answer('Лидмагнит был успешно удален! Можно вернуться в меню', reply_markup=kb.admin_back)
|
107 |
+
|
108 |
+
|
109 |
+
@router.callback_query(F.data == 'admin_to_main')
|
110 |
+
async def return_to_main(callback: CallbackQuery):
|
111 |
+
await callback.answer('Возвращаемся в меню')
|
112 |
+
await callback.message.answer('Здравствуйте! Что Вы хотите сделать?', reply_markup=kb.admin_main)
|
app/handlers/admin/router.py
ADDED
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from aiogram import Router, F
|
2 |
+
from aiogram.types import Message
|
3 |
+
from app.middleware.authentification import admin_check
|
4 |
+
from app.keyboards import admin_keyboards as kb
|
5 |
+
from .catalog import router as catalog_router
|
6 |
+
from .leadmagnets import router as leadmagnets_router
|
7 |
+
from .tests import router as tests_router
|
8 |
+
from .view_tests import router as view_tests_router
|
9 |
+
from .broadcast import router as broadcast_router
|
10 |
+
|
11 |
+
admin_router = Router()
|
12 |
+
|
13 |
+
admin_router.include_router(catalog_router)
|
14 |
+
admin_router.include_router(leadmagnets_router)
|
15 |
+
admin_router.include_router(tests_router)
|
16 |
+
admin_router.include_router(view_tests_router)
|
17 |
+
admin_router.include_router(broadcast_router)
|
18 |
+
|
19 |
+
@admin_router.message(F.text.lower() == 'вход для админов')
|
20 |
+
async def admin_enter(message: Message):
|
21 |
+
if not await admin_check(message, {}):
|
22 |
+
await message.answer("У вас нет доступа к админ-панели")
|
23 |
+
return
|
24 |
+
await message.answer('Здравствуйте! Что Вы хотите сделать?',
|
25 |
+
reply_markup=kb.admin_main)
|
26 |
+
|
27 |
+
@admin_router.message(F.text == 'Вернуться в пользовательский интерфейс')
|
28 |
+
async def return_as_user(message: Message):
|
29 |
+
await message.answer(
|
30 |
+
'Спасибо! Для возврата в пользовательский режим отправьте /start'
|
31 |
+
)
|
app/handlers/admin/tests.py
ADDED
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from aiogram import Router, F
|
2 |
+
from aiogram.types import Message, CallbackQuery
|
3 |
+
from aiogram.fsm.context import FSMContext
|
4 |
+
from app.database import requests as rq
|
5 |
+
from app.keyboards import admin_keyboards as kb
|
6 |
+
from app.states import AdminAddTest, AdminStates
|
7 |
+
from app.middleware.authentification import admin_check
|
8 |
+
|
9 |
+
router = Router()
|
10 |
+
|
11 |
+
@router.message(F.text == 'Отредактировать тесты')
|
12 |
+
async def test_editor_start(message: Message):
|
13 |
+
if not await admin_check(message, {}):
|
14 |
+
await message.answer("У вас нет доступа к админ-панели")
|
15 |
+
return
|
16 |
+
await message.answer(
|
17 |
+
'Текущие тесты. Нажмите для редактирования',
|
18 |
+
reply_markup=await kb.admin_keyboard_tests()
|
19 |
+
)
|
20 |
+
|
21 |
+
|
22 |
+
@router.callback_query(F.data == 'add_test')
|
23 |
+
async def add_test(callback: CallbackQuery, state: FSMContext):
|
24 |
+
await callback.answer('Добавляем тест')
|
25 |
+
await state.set_state(AdminAddTest.admin_set_title)
|
26 |
+
await callback.message.answer('Введите название теста (пожалуйста, не используйте "_" в названии!)')
|
27 |
+
|
28 |
+
|
29 |
+
@router.message(AdminAddTest.admin_set_title)
|
30 |
+
async def add_test_name(message: Message, state: FSMContext):
|
31 |
+
await state.update_data(name=message.text)
|
32 |
+
await state.set_state(AdminAddTest.admin_set_desc)
|
33 |
+
await message.answer('Введите краткое описание теста')
|
34 |
+
|
35 |
+
|
36 |
+
@router.message(AdminAddTest.admin_set_desc)
|
37 |
+
async def add_test_desc(message: Message, state: FSMContext):
|
38 |
+
await state.update_data(desc=message.text)
|
39 |
+
await state.set_state(AdminAddTest.admin_set_status)
|
40 |
+
await message.answer('Хотите ли Вы сделать данный тест активным? Активные тесты будут тут же доступны для '
|
41 |
+
'взаимодействия в пользовательском интерфейсе.', reply_markup=kb.yes_no_keyboard)
|
42 |
+
|
43 |
+
|
44 |
+
@router.message(AdminAddTest.admin_set_status)
|
45 |
+
async def add_test_status(message: Message, state: FSMContext):
|
46 |
+
await state.update_data(status=message.text)
|
47 |
+
await state.set_state(AdminAddTest.admin_set_type)
|
48 |
+
await message.answer('Введите тип теста: с баллами или без баллов. Тесты с баллами предполагают вывод разных '
|
49 |
+
'итогов теста в зависимости от количества набранных баллов, тесты без баллов предполагают '
|
50 |
+
'одно и то же сообщение по итогам прохождения', reply_markup=kb.test_type_keyboard)
|
51 |
+
|
52 |
+
|
53 |
+
@router.message(AdminAddTest.admin_set_type)
|
54 |
+
async def add_test_type(message: Message, state: FSMContext):
|
55 |
+
await state.update_data(type=message.text)
|
56 |
+
if message.text == 'С баллами':
|
57 |
+
await state.update_data(completion_message=None)
|
58 |
+
data = await state.get_data()
|
59 |
+
await rq.add_test_wo_points(data['name'], data['type'], data['desc'], True if data['status'] == 'Да' else False,
|
60 |
+
data['completion_message'])
|
61 |
+
await state.set_state(AdminAddTest.admin_set_completion_result_set)
|
62 |
+
await message.answer('Введите вариант сообщения, показывающегося в конце теста, и количество баллов, нужное для его '
|
63 |
+
'достижения в следующем формате: \n'
|
64 |
+
'22-24 \n'
|
65 |
+
'Вы получили результат 1!')
|
66 |
+
if message.text == 'Без баллов':
|
67 |
+
await state.set_state(AdminAddTest.admin_set_completion_result)
|
68 |
+
await message.answer('Введите сообщение, показывающееся в конце теста')
|
69 |
+
|
70 |
+
|
71 |
+
@router.message(AdminAddTest.admin_set_completion_result)
|
72 |
+
async def add_test_completion_wo(message: Message, state: FSMContext):
|
73 |
+
await state.update_data(completion_message=message.text)
|
74 |
+
data = await state.get_data()
|
75 |
+
await rq.add_test_wo_points(data['name'], data['type'], data['desc'], True if data['status'] == 'Да' else False, data['completion_message'])
|
76 |
+
await state.set_state(AdminAddTest.admin_add_question_vars_wo_points)
|
77 |
+
await message.answer('Введите первый вопрос, на следующих строках введите варианты ответа. Например: \n\n'
|
78 |
+
'В каком году Юрий Гагарин полетел в космос?\n***\n'
|
79 |
+
'1963 год \n'
|
80 |
+
'1961 год \n'
|
81 |
+
'1968 год')
|
82 |
+
|
83 |
+
|
84 |
+
@router.message(AdminAddTest.admin_add_question_vars_wo_points)
|
85 |
+
async def add_question_wo(message: Message, state: FSMContext):
|
86 |
+
data = await state.get_data()
|
87 |
+
await rq.add_question_vars_wo_points(data['name'], message.text)
|
88 |
+
await state.set_state(AdminAddTest.admin_end_questions)
|
89 |
+
await message.answer('Хотите ли вы добавить еще один вопрос?', reply_markup=kb.yes_no_keyboard)
|
90 |
+
|
91 |
+
|
92 |
+
@router.message(AdminAddTest.admin_end_questions)
|
93 |
+
async def do_we_continue_q(message: Message, state: FSMContext):
|
94 |
+
if message.text == 'Да':
|
95 |
+
await state.set_state(AdminAddTest.admin_add_question_vars_wo_points)
|
96 |
+
await message.answer('Введите вопрос, придерживаясь такого же форматирования, как раньше')
|
97 |
+
elif message.text == 'Нет':
|
98 |
+
await state.clear()
|
99 |
+
await message.answer('Спасибо! Ваш тест сохранен! Можно вернуться в меню!', reply_markup=kb.admin_back)
|
100 |
+
|
101 |
+
|
102 |
+
@router.message(AdminAddTest.admin_set_completion_result_set)
|
103 |
+
async def add_test_completion_w(message: Message, state: FSMContext):
|
104 |
+
data = await state.get_data()
|
105 |
+
await rq.add_test_result_w_points(data['name'], message.text)
|
106 |
+
await state.set_state(AdminAddTest.admin_end_results)
|
107 |
+
await message.answer('Хотите ли вы добавить еще вариант?', reply_markup=kb.yes_no_keyboard)
|
108 |
+
|
109 |
+
|
110 |
+
@router.message(AdminAddTest.admin_end_results)
|
111 |
+
async def add_test_completion_choice(message: Message, state: FSMContext):
|
112 |
+
if message.text == 'Да':
|
113 |
+
await state.set_state(AdminAddTest.admin_set_completion_result_set)
|
114 |
+
await message.answer(
|
115 |
+
'Введите вариант сообщения, показывающегося в конце теста, и количество баллов, нужное для его '
|
116 |
+
'достижения в следующем формате: \n'
|
117 |
+
'22-24 \n'
|
118 |
+
'Вы получили результат 1!')
|
119 |
+
if message.text == 'Нет':
|
120 |
+
await state.set_state(AdminAddTest.admin_add_question_vars_wo_points)
|
121 |
+
await message.answer('Введите первый вопрос, на следующих строках введите варианты ответа. После каждого ответа '
|
122 |
+
'указывайте количество баллов, начисляемых за этот вариант, через .... Например: \n\n'
|
123 |
+
'В каком году Юрий Гагарин полетел в космос?\n***\n'
|
124 |
+
'1963 год...0 \n'
|
125 |
+
'1961 год...2 \n'
|
126 |
+
'1968 год...0')
|
127 |
+
|
128 |
+
|
129 |
+
@router.callback_query(F.data.startswith('change_test_'))
|
130 |
+
async def view_test(callback: CallbackQuery):
|
131 |
+
await callback.answer('')
|
132 |
+
test_data = await rq.get_test(callback.data.split('_')[2])
|
133 |
+
results_string = ''
|
134 |
+
if not test_data['results']:
|
135 |
+
results_string += 'Отдельных результатов теста нет'
|
136 |
+
else:
|
137 |
+
for i in range(len(test_data['results'])):
|
138 |
+
results_string += f'Результат теста для количества очков {test_data["results"][i].max_points} - {test_data["results"][i].min_points}: \n' \
|
139 |
+
f'{test_data["results"][i].result_text} \n'
|
140 |
+
questions_string = ''
|
141 |
+
if not test_data['questions']:
|
142 |
+
questions_string += 'У этого теста нет вопросов'
|
143 |
+
else:
|
144 |
+
for i in range(len(test_data['questions'])):
|
145 |
+
questions_string += f'Вопрос {i+1}: {test_data["questions"][i].question_content} \n' \
|
146 |
+
f'Варианты ответа: {test_data["questions"][i].question_variants} \n'
|
147 |
+
await callback.message.answer(f'Название теста: {test_data["test"].test_name}, \n'
|
148 |
+
f'Тип теста: {test_data["test"].test_type}, \n'
|
149 |
+
f'Описание теста: {test_data["test"].test_description} \n'
|
150 |
+
f'Статус активности теста: {test_data["test"].is_active} \n'
|
151 |
+
f'Сообщение о завершении теста: {test_data["test"].completion_message} \n'
|
152 |
+
f'Возможные результаты теста: \n{results_string} \n\n'
|
153 |
+
f'Вопросы теста: \n{questions_string} \n\n'
|
154 |
+
f'На данный момент подробное редактирование тестов не поддерживается. Если Вы хотите '
|
155 |
+
f'изменить статус активности теста, нажмите на соответствующую кнопку внизу. Если Вы '
|
156 |
+
f'хотите внести в тест содержательные изменения, мы рекомендуем удалить тест и '
|
157 |
+
f'внести его заново!',
|
158 |
+
reply_markup= await kb.admin_change_test(test_data['id']))
|
159 |
+
|
160 |
+
|
161 |
+
@router.callback_query(F.data.startswith('edittest_status_'))
|
162 |
+
async def change_status(callback: CallbackQuery, state: FSMContext):
|
163 |
+
await callback.answer('')
|
164 |
+
await state.update_data(id=callback.data.split('_')[2])
|
165 |
+
await state.set_state(AdminStates.admin_edit_test_status)
|
166 |
+
await callback.message.answer(f'Должен ли тест быть активным?', reply_markup=kb.yes_no_keyboard)
|
167 |
+
|
168 |
+
|
169 |
+
@router.message(AdminStates.admin_edit_test_status)
|
170 |
+
async def set_new_status(message: Message, state: FSMContext):
|
171 |
+
data = await state.get_data()
|
172 |
+
await rq.change_test_status(data['id'], message.text)
|
173 |
+
await message.answer('Изменения были сохранены! Теперь можно вернуться в меню!',
|
174 |
+
reply_markup=kb.admin_back)
|
175 |
+
await state.clear()
|
176 |
+
|
177 |
+
|
178 |
+
@router.callback_query(F.data.startswith('deletetest_'))
|
179 |
+
async def confirm_test_deletion(callback: CallbackQuery, state: FSMContext):
|
180 |
+
await callback.answer('')
|
181 |
+
await state.set_state(AdminStates.admin_delete_test)
|
182 |
+
await state.update_data(id=callback.data.split("_")[1])
|
183 |
+
await callback.message.answer(f'Вы уверены, что хотите удалить тест?',
|
184 |
+
reply_markup=kb.yes_no_keyboard)
|
185 |
+
|
186 |
+
|
187 |
+
@router.message(AdminStates.admin_delete_test)
|
188 |
+
async def delete_or_not_delete_test(message: Message, state: FSMContext):
|
189 |
+
if message.text == 'Нет':
|
190 |
+
await state.clear()
|
191 |
+
await message.answer('Тогда вернемся в меню :)', reply_markup=kb.admin_back)
|
192 |
+
|
193 |
+
if message.text == 'Да':
|
194 |
+
data = await state.get_data()
|
195 |
+
await rq.delete_test(data['id'])
|
196 |
+
await state.clear()
|
197 |
+
await message.answer('Тест был успешно удален! Можно вернуться в меню', reply_markup=kb.admin_back)
|
198 |
+
|
199 |
+
|
200 |
+
@router.callback_query(F.data == 'admin_to_main')
|
201 |
+
async def return_to_main(callback: CallbackQuery):
|
202 |
+
await callback.answer('Возвращаемся в меню')
|
203 |
+
await callback.message.answer('Здравствуйте! Что Вы хотите сделать?', reply_markup=kb.admin_main)
|
app/handlers/admin/view_tests.py
ADDED
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from aiogram import Router, F
|
2 |
+
from aiogram.types import Message, CallbackQuery
|
3 |
+
from aiogram.fsm.context import FSMContext
|
4 |
+
from app.database import requests as rq
|
5 |
+
from app.keyboards import admin_keyboards as kb
|
6 |
+
from app.database.requests import get_user_test_results, get_all_test_answers
|
7 |
+
from app.states import TestStates
|
8 |
+
from app.middleware.authentification import admin_check
|
9 |
+
|
10 |
+
router = Router()
|
11 |
+
|
12 |
+
@router.message(F.text == "Просмотреть результаты тестов")
|
13 |
+
async def name_provide(message: Message, state: FSMContext):
|
14 |
+
if not await admin_check(message, {}):
|
15 |
+
await message.answer("У вас нет доступа к админ-панели")
|
16 |
+
return
|
17 |
+
await state.set_state(TestStates.name_get)
|
18 |
+
await message.answer("Введите имя пользователя, результаты тестов которого хотите посмотреть. Для просмотра результатов теста пользователь должен быть зарегистрирован!")
|
19 |
+
|
20 |
+
|
21 |
+
@router.message(TestStates.name_get)
|
22 |
+
async def show_user_results(message: Message, state: FSMContext):
|
23 |
+
results = await get_user_test_results(message.text)
|
24 |
+
|
25 |
+
if isinstance(results, str):
|
26 |
+
await message.answer(results, reply_markup=kb.admin_main)
|
27 |
+
return
|
28 |
+
|
29 |
+
if not results:
|
30 |
+
await message.answer(
|
31 |
+
"📊 У пользователя пока нет результатов тестов",
|
32 |
+
reply_markup=kb.admin_main
|
33 |
+
)
|
34 |
+
return
|
35 |
+
|
36 |
+
response = f"📋 Результаты тестов пользователя {message.text}:\n\n"
|
37 |
+
|
38 |
+
for result in results:
|
39 |
+
completed_date = result['completed_at'].strftime("%d.%m.%Y %H:%M")
|
40 |
+
response += (
|
41 |
+
f"🔷 Тест: {result['test_name']}\n"
|
42 |
+
f"📅 Дата прохождения: {completed_date}\n"
|
43 |
+
f"📊 Набрано баллов: {result['score'] if result['score'] is not None else 'Нет баллов'}\n"
|
44 |
+
f"📝 Результат: {result['result'] if result['result'] else 'Не определен'}\n"
|
45 |
+
f"{'─' * 30}\n\n"
|
46 |
+
)
|
47 |
+
|
48 |
+
await message.answer(
|
49 |
+
response,
|
50 |
+
reply_markup=kb.admin_main,
|
51 |
+
parse_mode="HTML"
|
52 |
+
)
|
53 |
+
|
54 |
+
@router.callback_query(F.data == 'admin_to_main')
|
55 |
+
async def return_to_main(callback: CallbackQuery):
|
56 |
+
await callback.answer('Возвращаемся в меню')
|
57 |
+
await callback.message.answer('Здравствуйте! Что Вы хотите сделать?', reply_markup=kb.admin_main)
|
app/handlers/admin_route.py
ADDED
@@ -0,0 +1,392 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from aiogram.types import Message, CallbackQuery
|
2 |
+
from aiogram import F, Router
|
3 |
+
from aiogram.fsm.context import FSMContext
|
4 |
+
|
5 |
+
import app.keyboards.admin_keyboards as kb
|
6 |
+
import app.database.requests as rq
|
7 |
+
from app.states import AdminAddService, AdminAddLeadmagnet, AdminAddTest, AdminStates
|
8 |
+
|
9 |
+
admin_router = Router()
|
10 |
+
|
11 |
+
|
12 |
+
@admin_router.message((F.text == 'Вход для админов'))
|
13 |
+
async def admin_enter(message: Message):
|
14 |
+
await message.answer('Здравствуйте! Что Вы хотите сделать?', reply_markup=kb.admin_main)
|
15 |
+
|
16 |
+
|
17 |
+
@admin_router.message(F.text == 'Отредактировать каталог услуг')
|
18 |
+
async def catalog_editor_start(message: Message):
|
19 |
+
await message.answer('Сейчас у нас представлены следующие услуги. Нажмите на услугу, чтобы отредактировать ее ',
|
20 |
+
reply_markup=await kb.admin_keyboard_service_catalog())
|
21 |
+
|
22 |
+
|
23 |
+
@admin_router.callback_query(F.data.startswith('change_service_'))
|
24 |
+
async def edit_service(callback: CallbackQuery):
|
25 |
+
await callback.answer('')
|
26 |
+
service = await rq.get_service_info(callback.data.split('_')[2])
|
27 |
+
await callback.message.answer(f'Услуга {service.service_name} \n\n'
|
28 |
+
f'Описание: {service.service_description}\n'
|
29 |
+
f'Цена: {service.service_price} рублей \n\n'
|
30 |
+
f'Что Вы хотите изменить?',
|
31 |
+
reply_markup=await kb.admin_change_service(service.service_name))
|
32 |
+
|
33 |
+
|
34 |
+
@admin_router.callback_query(F.data == 'admin_to_main')
|
35 |
+
async def return_to_main(callback: CallbackQuery):
|
36 |
+
await callback.answer('Возвращаемся в меню')
|
37 |
+
await callback.message.answer('Здравствуйте! Что Вы хотите сделать?', reply_markup=kb.admin_main)
|
38 |
+
|
39 |
+
|
40 |
+
@admin_router.callback_query(F.data == 'add_service')
|
41 |
+
async def add_service(callback: CallbackQuery, state: FSMContext):
|
42 |
+
await callback.answer('Добавляем услугу')
|
43 |
+
await state.set_state(AdminAddService.admin_add_name)
|
44 |
+
await callback.message.answer('Введите название новой услуги')
|
45 |
+
|
46 |
+
|
47 |
+
@admin_router.message(AdminAddService.admin_add_name)
|
48 |
+
async def add_name(message: Message, state: FSMContext):
|
49 |
+
await state.update_data(name=message.text)
|
50 |
+
await state.set_state(AdminAddService.admin_add_desc)
|
51 |
+
await message.answer('Введите описание новой услуги')
|
52 |
+
|
53 |
+
|
54 |
+
@admin_router.message(AdminAddService.admin_add_desc)
|
55 |
+
async def add_desc(message: Message, state: FSMContext):
|
56 |
+
await state.update_data(desc=message.text)
|
57 |
+
await state.set_state(AdminAddService.admin_add_price)
|
58 |
+
await message.answer('Введите цену услуги в рублях')
|
59 |
+
|
60 |
+
|
61 |
+
@admin_router.message(AdminAddService.admin_add_price)
|
62 |
+
async def add_price(message: Message, state: FSMContext):
|
63 |
+
await state.update_data(price=message.text)
|
64 |
+
data = await state.get_data()
|
65 |
+
await rq.add_service(data['name'], data['desc'], data['price'])
|
66 |
+
await message.answer('Услуга сохранена! Теперь можно изменить ее, выбрав услугу в меню и отредактировав',
|
67 |
+
reply_markup=kb.admin_back)
|
68 |
+
await state.clear()
|
69 |
+
|
70 |
+
|
71 |
+
@admin_router.callback_query(F.data.startswith('editservice_'))
|
72 |
+
async def begin_service_edit(callback: CallbackQuery, state: FSMContext):
|
73 |
+
await callback.answer('')
|
74 |
+
await state.set_state(AdminStates.admin_edit_service)
|
75 |
+
await state.update_data(service=callback.data.split('_')[2])
|
76 |
+
await state.update_data(parameter_changed=callback.data.split("_")[1])
|
77 |
+
replace_dict = {'name': 'ое название', 'desc': 'ое описание', 'price': 'ую цену'}
|
78 |
+
await callback.message.answer(f'Введите нов{replace_dict[callback.data.split("_")[1]]} услуги')
|
79 |
+
|
80 |
+
|
81 |
+
@admin_router.message(AdminStates.admin_edit_service)
|
82 |
+
async def finish_service_edit(message: Message, state: FSMContext):
|
83 |
+
await state.update_data(change=message.text)
|
84 |
+
data = await state.get_data()
|
85 |
+
await rq.edit_service(data['service'], data['parameter_changed'], data['change'])
|
86 |
+
await state.clear()
|
87 |
+
await message.answer(f'Информация была обновлена, можно вернуться на главную', reply_markup=kb.admin_back)
|
88 |
+
|
89 |
+
|
90 |
+
@admin_router.callback_query(F.data.startswith('deleteservice_'))
|
91 |
+
async def confirm_service_deletion(callback: CallbackQuery, state: FSMContext):
|
92 |
+
await callback.answer('')
|
93 |
+
await state.set_state(AdminStates.admin_delete_service)
|
94 |
+
await state.update_data(name=callback.data.split("_")[1])
|
95 |
+
await callback.message.answer(f'Вы уверены, что хотите удалить услугу {callback.data.split("_")[1]}?',
|
96 |
+
reply_markup=kb.yes_no_keyboard)
|
97 |
+
|
98 |
+
|
99 |
+
@admin_router.message(AdminStates.admin_delete_service)
|
100 |
+
async def delete_or_not_delete(message: Message, state: FSMContext):
|
101 |
+
if message.text == 'Нет':
|
102 |
+
await state.clear()
|
103 |
+
await message.answer('Тогда вернемся в меню :)', reply_markup=kb.admin_back)
|
104 |
+
|
105 |
+
if message.text == 'Да':
|
106 |
+
data = await state.get_data()
|
107 |
+
await rq.delete_service(data['name'])
|
108 |
+
await state.clear()
|
109 |
+
await message.answer('Услуга была успешно удалена! Можно вернуться в меню', reply_markup=kb.admin_back)
|
110 |
+
|
111 |
+
|
112 |
+
@admin_router.message(F.text == 'Отредактировать кодовые слова')
|
113 |
+
async def view_leadmagnets(message: Message):
|
114 |
+
await message.answer('Вот текущие лидмагниты. Нажмите на подходящий лидмагнит, чтобы изменить его.',
|
115 |
+
reply_markup=await kb.admin_keyboard_leadmagnets())
|
116 |
+
|
117 |
+
|
118 |
+
@admin_router.callback_query(F.data == 'add_leadmanget')
|
119 |
+
async def add_leadmagnet(callback: CallbackQuery, state: FSMContext):
|
120 |
+
await callback.answer('Добавляем лидмагнит')
|
121 |
+
await state.set_state(AdminAddLeadmagnet.admin_set_trigger)
|
122 |
+
await callback.message.answer('Введите кодовое слово (пожалуйста, не используйте "_" в названии!)')
|
123 |
+
|
124 |
+
|
125 |
+
@admin_router.message(AdminAddLeadmagnet.admin_set_trigger)
|
126 |
+
async def add_trigger(message: Message, state: FSMContext):
|
127 |
+
await state.update_data(trigger=message.text)
|
128 |
+
await state.set_state(AdminAddLeadmagnet.admin_set_content)
|
129 |
+
await message.answer('Введите содержание лидмагнита '
|
130 |
+
'(сообщение, которое получит пользователь после отправки кодового слова)')
|
131 |
+
|
132 |
+
|
133 |
+
@admin_router.message(AdminAddLeadmagnet.admin_set_content)
|
134 |
+
async def add_content(message: Message, state: FSMContext):
|
135 |
+
await state.update_data(content=message.text)
|
136 |
+
await state.set_state(AdminAddLeadmagnet.admin_set_status)
|
137 |
+
await message.answer('Хотите ли Вы сделать данный лидмагнит активным? Активные лидмагниты будут тут же доступны для '
|
138 |
+
'взаимодействия в пользовательском интерфейсе.', reply_markup=kb.yes_no_keyboard)
|
139 |
+
|
140 |
+
|
141 |
+
@admin_router.message(AdminAddLeadmagnet.admin_set_status)
|
142 |
+
async def set_leadmagnet_status(message: Message, state: FSMContext):
|
143 |
+
await state.update_data(status=message.text)
|
144 |
+
data = await state.get_data()
|
145 |
+
await rq.add_leadmagnet(data['trigger'], data['content'], data['status'])
|
146 |
+
await message.answer('Лидмагнит сохранен! Теперь можно отредактировать его, выбрав его в меню',
|
147 |
+
reply_markup=kb.admin_back)
|
148 |
+
await state.clear()
|
149 |
+
|
150 |
+
|
151 |
+
@admin_router.callback_query(F.data.startswith('change_leadmagnet_'))
|
152 |
+
async def edit_leadmagnet(callback: CallbackQuery):
|
153 |
+
await callback.answer('')
|
154 |
+
leadmagnet = await rq.get_leadmagnet_info(callback.data.split('_')[2])
|
155 |
+
await callback.message.answer(f'Триггер: {leadmagnet.trigger} \n\n'
|
156 |
+
f'Содержание: {leadmagnet.content}\n'
|
157 |
+
f'Активен? {leadmagnet.active_status}\n\n'
|
158 |
+
f'Что Вы хотите изменить?',
|
159 |
+
reply_markup=await kb.admin_change_leadmagnet(leadmagnet.trigger))
|
160 |
+
|
161 |
+
|
162 |
+
@admin_router.callback_query(F.data.startswith('editleadmagnet_'))
|
163 |
+
async def begin_leadmagnet_edit(callback: CallbackQuery, state: FSMContext):
|
164 |
+
await callback.answer('')
|
165 |
+
await state.set_state(AdminStates.admin_edit_leadmagnet)
|
166 |
+
await state.update_data(leadmagnet=callback.data.split('_')[2])
|
167 |
+
await state.update_data(parameter_changed=callback.data.split("_")[1])
|
168 |
+
replace_dict = {'trigger': 'ый триггер', 'content': 'ое содержание'}
|
169 |
+
if callback.data.split("_")[1] != 'status':
|
170 |
+
await callback.message.answer(f'Введите нов{replace_dict[callback.data.split("_")[1]]} лидмагнита')
|
171 |
+
elif callback.data.split("_")[1] == 'status':
|
172 |
+
await callback.message.answer(f'Должен ли лидмагнит быть активным?', reply_markup=kb.yes_no_keyboard)
|
173 |
+
|
174 |
+
|
175 |
+
@admin_router.message(AdminStates.admin_edit_leadmagnet)
|
176 |
+
async def finish_leadmagnet_edit(message: Message, state: FSMContext):
|
177 |
+
await state.update_data(change=message.text)
|
178 |
+
data = await state.get_data()
|
179 |
+
await rq.edit_leadmagnet(data['leadmagnet'], data['parameter_changed'], data['change'])
|
180 |
+
await message.answer(f'Информация была обновлена, можно вернуться на главную', reply_markup=kb.admin_back)
|
181 |
+
|
182 |
+
|
183 |
+
@admin_router.callback_query(F.data.startswith('deleteleadmagnet_'))
|
184 |
+
async def confirm_leadmagnet_deletion(callback: CallbackQuery, state: FSMContext):
|
185 |
+
await callback.answer('')
|
186 |
+
await state.set_state(AdminStates.admin_delete_leadmagnet)
|
187 |
+
await state.update_data(leadmagnet=callback.data.split("_")[1])
|
188 |
+
await callback.message.answer(f'Вы уверены, что хотите удалить лидмагнит {callback.data.split("_")[1]}?',
|
189 |
+
reply_markup=kb.yes_no_keyboard)
|
190 |
+
|
191 |
+
|
192 |
+
@admin_router.message(AdminStates.admin_delete_leadmagnet)
|
193 |
+
async def delete_or_not_delete(message: Message, state: FSMContext):
|
194 |
+
if message.text == 'Нет':
|
195 |
+
await state.clear()
|
196 |
+
await message.answer('Тогда вернемся в меню :)', reply_markup=kb.admin_back)
|
197 |
+
|
198 |
+
if message.text == 'Да':
|
199 |
+
data = await state.get_data()
|
200 |
+
await rq.delete_leadmagnet(data['leadmagnet'])
|
201 |
+
await state.clear()
|
202 |
+
await message.answer('Лидмагнит был успешно удален! Можно вернуться в меню', reply_markup=kb.admin_back)
|
203 |
+
|
204 |
+
|
205 |
+
@admin_router.message(F.text == 'Отредактировать тесты')
|
206 |
+
async def test_editor_start(message: Message):
|
207 |
+
await message.answer('Сейчас у нас имеются следующие тесты. Нажмите на тест, чтобы отредактировать его ',
|
208 |
+
reply_markup=await kb.admin_keyboard_tests())
|
209 |
+
|
210 |
+
|
211 |
+
@admin_router.callback_query(F.data == 'add_test')
|
212 |
+
async def add_test(callback: CallbackQuery, state: FSMContext):
|
213 |
+
await callback.answer('Добавляем тест')
|
214 |
+
await state.set_state(AdminAddTest.admin_set_title)
|
215 |
+
await callback.message.answer('Введите название теста (пожалуйста, не используйте "_" в названии!)')
|
216 |
+
|
217 |
+
|
218 |
+
@admin_router.message(AdminAddTest.admin_set_title)
|
219 |
+
async def add_test_name(message: Message, state: FSMContext):
|
220 |
+
await state.update_data(name=message.text)
|
221 |
+
await state.set_state(AdminAddTest.admin_set_desc)
|
222 |
+
await message.answer('Введите краткое описание теста')
|
223 |
+
|
224 |
+
|
225 |
+
@admin_router.message(AdminAddTest.admin_set_desc)
|
226 |
+
async def add_test_desc(message: Message, state: FSMContext):
|
227 |
+
await state.update_data(desc=message.text)
|
228 |
+
await state.set_state(AdminAddTest.admin_set_status)
|
229 |
+
await message.answer('Хотите ли Вы сделать данный тест активным? Активные тесты будут тут же доступны для '
|
230 |
+
'взаимодействия в пользовательском интерфейсе.', reply_markup=kb.yes_no_keyboard)
|
231 |
+
|
232 |
+
|
233 |
+
@admin_router.message(AdminAddTest.admin_set_status)
|
234 |
+
async def add_test_status(message: Message, state: FSMContext):
|
235 |
+
await state.update_data(status=message.text)
|
236 |
+
await state.set_state(AdminAddTest.admin_set_type)
|
237 |
+
await message.answer('Введите тип теста: с баллами или без баллов. Тесты с баллами предполагают вывод разных '
|
238 |
+
'итогов теста в зависимости от количества набранных баллов, тесты без баллов предполагают '
|
239 |
+
'одно и то же сообщение по итогам прохождения', reply_markup=kb.test_type_keyboard)
|
240 |
+
|
241 |
+
|
242 |
+
@admin_router.message(AdminAddTest.admin_set_type)
|
243 |
+
async def add_test_type(message: Message, state: FSMContext):
|
244 |
+
await state.update_data(type=message.text)
|
245 |
+
if message.text == 'С баллами':
|
246 |
+
await state.update_data(completion_message=None)
|
247 |
+
data = await state.get_data()
|
248 |
+
await rq.add_test_wo_points(data['name'], data['type'], data['desc'], data['status'],
|
249 |
+
data['completion_message'])
|
250 |
+
await state.set_state(AdminAddTest.admin_set_completion_result_set)
|
251 |
+
await message.answer('Введите вариант сообщения, показывающегося в конце теста, и количество баллов, нужное для его '
|
252 |
+
'достижения в следующем формате: \n'
|
253 |
+
'22-24 \n'
|
254 |
+
'Вы получили результат 1!')
|
255 |
+
if message.text == 'Без баллов':
|
256 |
+
await state.set_state(AdminAddTest.admin_set_completion_result)
|
257 |
+
await message.answer('Введите сообщение, показывающееся в конце теста')
|
258 |
+
|
259 |
+
|
260 |
+
@admin_router.message(AdminAddTest.admin_set_completion_result)
|
261 |
+
async def add_test_completion_wo(message: Message, state: FSMContext):
|
262 |
+
await state.update_data(completion_message=message.text)
|
263 |
+
data = await state.get_data()
|
264 |
+
await rq.add_test_wo_points(data['name'], data['type'], data['desc'], data['status'], data['completion_message'])
|
265 |
+
await state.set_state(AdminAddTest.admin_add_question_vars_wo_points)
|
266 |
+
await message.answer('Введите первый вопрос, на следующих строках введите варианты ответа. Например: \n\n'
|
267 |
+
'В каком году Юрий Гагарин полетел в космос?\n>>>\n'
|
268 |
+
'1963 год \n'
|
269 |
+
'1961 год \n'
|
270 |
+
'1968 год')
|
271 |
+
|
272 |
+
|
273 |
+
@admin_router.message(AdminAddTest.admin_add_question_vars_wo_points)
|
274 |
+
async def add_question_wo(message: Message, state: FSMContext):
|
275 |
+
data = await state.get_data()
|
276 |
+
await rq.add_question_vars_wo_points(data['name'], message.text)
|
277 |
+
await state.set_state(AdminAddTest.admin_end_questions)
|
278 |
+
await message.answer('Хотите ли вы добавить еще один вопрос?', reply_markup=kb.yes_no_keyboard)
|
279 |
+
|
280 |
+
|
281 |
+
@admin_router.message(AdminAddTest.admin_end_questions)
|
282 |
+
async def do_we_continue_q(message: Message, state: FSMContext):
|
283 |
+
if message.text == 'Да':
|
284 |
+
await state.set_state(AdminAddTest.admin_add_question_vars_wo_points)
|
285 |
+
await message.answer('Введите вопрос, придерживаясь такого же форматирования, как раньше')
|
286 |
+
elif message.text == 'Нет':
|
287 |
+
await state.clear()
|
288 |
+
await message.answer('Спасибо! Ваш тест сохранен! Можно вернуться в меню!', reply_markup=kb.admin_back)
|
289 |
+
|
290 |
+
|
291 |
+
@admin_router.message(AdminAddTest.admin_set_completion_result_set)
|
292 |
+
async def add_test_completion_w(message: Message, state: FSMContext):
|
293 |
+
data = await state.get_data()
|
294 |
+
await rq.add_test_result_w_points(data['name'], message.text)
|
295 |
+
await state.set_state(AdminAddTest.admin_end_results)
|
296 |
+
await message.answer('Хотите ли вы добавить еще вариант?', reply_markup=kb.yes_no_keyboard)
|
297 |
+
|
298 |
+
|
299 |
+
@admin_router.message(AdminAddTest.admin_end_results)
|
300 |
+
async def add_test_completion_choice(message: Message, state: FSMContext):
|
301 |
+
if message.text == 'Да':
|
302 |
+
await state.set_state(AdminAddTest.admin_set_completion_result_set)
|
303 |
+
await message.answer(
|
304 |
+
'Введите вариант сообщения, показывающегося в конце теста, и количество баллов, нужное для его '
|
305 |
+
'достижения в следующем формате: \n'
|
306 |
+
'22-24 \n'
|
307 |
+
'Вы получили результат 1!')
|
308 |
+
if message.text == 'Нет':
|
309 |
+
await state.set_state(AdminAddTest.admin_add_question_vars_wo_points)
|
310 |
+
await message.answer('Введите первый вопрос, на следующих строках введите варианты ответа. После каждого ответа '
|
311 |
+
'указывайте количество баллов, начисляемых за этот вариант, через .... Например: \n\n'
|
312 |
+
'В каком году Юрий Гагарин полетел в космос?\n>>>\n'
|
313 |
+
'1963 год...0 \n'
|
314 |
+
'1961 год...2 \n'
|
315 |
+
'1968 год...0')
|
316 |
+
|
317 |
+
|
318 |
+
@admin_router.callback_query(F.data.startswith('change_test_'))
|
319 |
+
async def view_test(callback: CallbackQuery):
|
320 |
+
await callback.answer('')
|
321 |
+
test_data = await rq.get_test(callback.data.split('_')[2])
|
322 |
+
results_string = ''
|
323 |
+
if not test_data['results']:
|
324 |
+
results_string += 'Отдельных результатов теста нет'
|
325 |
+
else:
|
326 |
+
for i in range(len(test_data['results'])):
|
327 |
+
results_string += f'Результат теста для количества очков {test_data["results"][i].pointrange}: \n' \
|
328 |
+
f'{test_data["results"][i].result_text} \n'
|
329 |
+
questions_string = ''
|
330 |
+
if not test_data['questions']:
|
331 |
+
questions_string += 'У этого теста нет вопросов'
|
332 |
+
else:
|
333 |
+
for i in range(len(test_data['questions'])):
|
334 |
+
questions_string += f'Вопрос {i+1}: {test_data["questions"][i].question_content} \n' \
|
335 |
+
f'Варианты ответа: {test_data["questions"][i].question_variants} \n'
|
336 |
+
await callback.message.answer(f'Название теста: {test_data["test"].test_name}, \n'
|
337 |
+
f'Тип теста: {test_data["test"].test_type}, \n'
|
338 |
+
f'Описание теста: {test_data["test"].test_description} \n'
|
339 |
+
f'Статус активности теста: {test_data["test"].active_status} \n'
|
340 |
+
f'Сообщение о завершении теста: {test_data["test"].completion_message} \n'
|
341 |
+
f'Возможные результаты теста: \n{results_string} \n\n'
|
342 |
+
f'Вопросы теста: \n{questions_string} \n\n'
|
343 |
+
f'На данный момент подробное редактирование тестов не поддерживается. Если Вы хотите '
|
344 |
+
f'изменить статус активности теста, нажмите на соответствующую кнопку внизу. Если Вы '
|
345 |
+
f'хотите внести в тест содержательные изменения, мы рекомендуем удалить тест и '
|
346 |
+
f'внести его заново!',
|
347 |
+
reply_markup= await kb.admin_change_test(test_data['test'].test_name))
|
348 |
+
|
349 |
+
|
350 |
+
@admin_router.callback_query(F.data.startswith('edittest_status_'))
|
351 |
+
async def change_status(callback: CallbackQuery, state: FSMContext):
|
352 |
+
await callback.answer('')
|
353 |
+
await state.update_data(name=callback.data.split('_')[2])
|
354 |
+
await state.set_state(AdminStates.admin_edit_test_status)
|
355 |
+
await callback.message.answer(f'Должен ли тест быть активным?', reply_markup=kb.yes_no_keyboard)
|
356 |
+
|
357 |
+
|
358 |
+
@admin_router.message(AdminStates.admin_edit_test_status)
|
359 |
+
async def set_new_status(message: Message, state: FSMContext):
|
360 |
+
data = await state.get_data()
|
361 |
+
await rq.change_test_status(data['name'], message.text)
|
362 |
+
await message.answer('Изменения были сохранены! Теперь можно вернуться в меню!',
|
363 |
+
reply_markup=kb.admin_back)
|
364 |
+
await state.clear()
|
365 |
+
|
366 |
+
|
367 |
+
@admin_router.callback_query(F.data.startswith('deletetest_'))
|
368 |
+
async def confirm_test_deletion(callback: CallbackQuery, state: FSMContext):
|
369 |
+
await callback.answer('')
|
370 |
+
await state.set_state(AdminStates.admin_delete_test)
|
371 |
+
await state.update_data(name=callback.data.split("_")[1])
|
372 |
+
await callback.message.answer(f'Вы уверены, что хотите удалить тест {callback.data.split("_")[1]}?',
|
373 |
+
reply_markup=kb.yes_no_keyboard)
|
374 |
+
|
375 |
+
|
376 |
+
@admin_router.message(AdminStates.admin_delete_test)
|
377 |
+
async def delete_or_not_delete_test(message: Message, state: FSMContext):
|
378 |
+
if message.text == 'Нет':
|
379 |
+
await state.clear()
|
380 |
+
await message.answer('Тогда вернемся в меню :)', reply_markup=kb.admin_back)
|
381 |
+
|
382 |
+
if message.text == 'Да':
|
383 |
+
data = await state.get_data()
|
384 |
+
await rq.delete_test(data['name'])
|
385 |
+
await state.clear()
|
386 |
+
await message.answer('Тест был успешно удален! Можно вернуться в меню', reply_markup=kb.admin_back)
|
387 |
+
|
388 |
+
|
389 |
+
@admin_router.message(F.text == 'Вернуться в пользовательский интерфейс')
|
390 |
+
async def return_as_user(message: Message):
|
391 |
+
await message.answer('Спасибо! Удачного Вам дня! Отправьте в чат /start, чтобы вернуться в пользовательский режим '
|
392 |
+
'редактирования!')
|
app/handlers/user/__init__.py
ADDED
File without changes
|
app/handlers/user/catalog.py
ADDED
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from aiogram import Router, F
|
2 |
+
from aiogram.types import Message, CallbackQuery
|
3 |
+
from aiogram.fsm.context import FSMContext
|
4 |
+
from app.database import requests as rq
|
5 |
+
from app.keyboards import user_keyboards as kb
|
6 |
+
from app.states import Other
|
7 |
+
from app.database.requests import check_user_registered
|
8 |
+
from app.config.config import ADMIN_ID
|
9 |
+
|
10 |
+
router = Router()
|
11 |
+
|
12 |
+
@router.message(F.text == 'Посмотреть каталог услуг')
|
13 |
+
async def get_catalog(message: Message):
|
14 |
+
await message.answer('Спасибо за интерес! Вот наш перечень услуг. '
|
15 |
+
'Пожалуйста, выберите самую подходящую и нажмите на соответствующую кнопку, '
|
16 |
+
'чтобы узнать о ней подробнее:', reply_markup=await kb.user_keyboard_catalog())
|
17 |
+
|
18 |
+
|
19 |
+
@router.callback_query(F.data.startswith('service_'))
|
20 |
+
async def service_info(callback: CallbackQuery,state: FSMContext):
|
21 |
+
await callback.answer('')
|
22 |
+
service_info = await rq.get_service_info(callback.data.split('_')[1])
|
23 |
+
service_string = f"Вот информация по выбранной Вами услуге: \n" \
|
24 |
+
f"Название: {service_info.service_name} \n" \
|
25 |
+
f"Описание: {service_info.service_description} \n" \
|
26 |
+
f"Цена: {service_info.service_price}. \n\n" \
|
27 |
+
f"Хотите ли Вы воспользоваться данной услугой?"
|
28 |
+
await callback.answer('')
|
29 |
+
await state.set_state(Other.user_pre_buy_service)
|
30 |
+
await state.update_data(user_id=callback.from_user.id,
|
31 |
+
user_username=callback.from_user.username,
|
32 |
+
service=service_info.service_name)
|
33 |
+
await callback.message.answer(service_string, reply_markup=kb.service_confirm)
|
34 |
+
|
35 |
+
|
36 |
+
@router.message(F.text == 'Да, связаться с Ниной')
|
37 |
+
async def admin_prompt(message: Message, state: FSMContext):
|
38 |
+
await state.set_state(Other.user_buy_service)
|
39 |
+
await message.answer('Напишите Нине и изложите Вашу проблему - это поможет ей лучше понять Ваш конкретный случай'
|
40 |
+
' и сделать Ваш разговор более предметным!')
|
41 |
+
|
42 |
+
|
43 |
+
@router.message(Other.user_buy_service)
|
44 |
+
async def admin_connect(message: Message, state: FSMContext):
|
45 |
+
data = await state.get_data()
|
46 |
+
await message.bot.send_message(chat_id=ADMIN_ID[0],
|
47 |
+
text=f'Здравствуйте! Поступил новый заказ на услугу {data["service"]} от пользователя'
|
48 |
+
f' @{data["user_username"]}')
|
49 |
+
await message.send_copy(ADMIN_ID[0])
|
50 |
+
await message.answer('Спасибо за обращение! Скоро Нина свяжется с Вами в личных сообщениях для обсуждения '
|
51 |
+
'подробностей Вашего заказа!')
|
52 |
+
await state.clear()
|
53 |
+
registration_status = await check_user_registered(message.from_user.id)
|
54 |
+
if registration_status:
|
55 |
+
await message.answer('Вы можете вернуться в меню и посмотреть, какие еще услуги мы предлагаем!',
|
56 |
+
reply_markup=kb.user_back_wo_reg)
|
57 |
+
elif not registration_status:
|
58 |
+
await message.answer('Вы можете вернуться в меню и посмотреть, какие еще услуги мы предлагаем! Еще мы предлагаем '
|
59 |
+
'зарегистрироваться, чтобы получить возможность не пропускать всё самое важное, что происходит '
|
60 |
+
'в канале. Также регистрация позволит Вам вступить в СЕКРЕТНЫЙ КЛУБ пользователей, которые'
|
61 |
+
' будут иметь доступ к специальным акциям и ВКУСНЫМ ЦЕНАМ для своих!',
|
62 |
+
reply_markup=kb.user_back)
|
63 |
+
|
64 |
+
|
65 |
+
@router.message(F.text == 'Нет, вернуться в меню')
|
66 |
+
async def np_service_return_to_menu(message: Message, state: FSMContext):
|
67 |
+
await state.clear()
|
68 |
+
await message.answer('Чем я могу Вам помочь?', reply_markup=kb.user_main)
|
69 |
+
|
70 |
+
|
71 |
+
@router.callback_query(F.data == "user_to_main")
|
72 |
+
async def register(callback: CallbackQuery):
|
73 |
+
await callback.answer("Возвращаемся в меню")
|
74 |
+
await callback.message.answer("Чем я могу Вам помочь?", reply_markup=kb.user_main)
|
app/handlers/user/info_check.py
ADDED
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from aiogram import Router, F
|
2 |
+
from aiogram.types import Message, CallbackQuery
|
3 |
+
from aiogram.fsm.context import FSMContext
|
4 |
+
from app.database import requests as rq
|
5 |
+
from app.keyboards import user_keyboards as kb
|
6 |
+
from app.database.requests import check_user_registered, update_user_data, check_login_unique, own_login_check
|
7 |
+
from app.states import Other
|
8 |
+
|
9 |
+
router = Router()
|
10 |
+
|
11 |
+
@router.message(F.text == "Посмотреть сохраненные сведения о себе")
|
12 |
+
async def get_info(message: Message):
|
13 |
+
registration_status = await check_user_registered(message.from_user.id)
|
14 |
+
if registration_status:
|
15 |
+
user_info = await rq.get_user_registration_info(message.from_user.id)
|
16 |
+
await message.answer(user_info, reply_markup=kb.user_infocheck_back)
|
17 |
+
elif not registration_status:
|
18 |
+
await message.answer("Вы не зарегистрированы. Пожалуйста, зарегистрируйтесь, чтобы увидеть сохраненные сведения о себе.", reply_markup=kb.user_back)
|
19 |
+
return
|
20 |
+
|
21 |
+
|
22 |
+
@router.callback_query(F.data == 'data_correct')
|
23 |
+
async def correction_info(callback: CallbackQuery, state: FSMContext):
|
24 |
+
await state.set_state(Other.user_data_check)
|
25 |
+
await callback.message.answer('Вы правда хотите изменить регистрационные данные?', reply_markup=kb.yes_no_keyboard)
|
26 |
+
|
27 |
+
|
28 |
+
@router.message(Other.user_data_check)
|
29 |
+
async def register_check(message: Message, state: FSMContext):
|
30 |
+
if message.text == 'Да':
|
31 |
+
await state.set_state(Other.user_data_correct)
|
32 |
+
await message.answer('Упс :( Что Вы хотите исправить?', reply_markup=kb.register_correct_keyboard)
|
33 |
+
if message.text == 'Нет':
|
34 |
+
await message.answer('Хорошо! Если что-то изменится, Вы всегда можете обновить свои данные!', reply_markup=kb.user_back_wo_reg)
|
35 |
+
await state.clear()
|
36 |
+
|
37 |
+
|
38 |
+
@router.message(Other.user_data_correct)
|
39 |
+
async def correction_info(message: Message, state: FSMContext):
|
40 |
+
await state.update_data(correction=message.text)
|
41 |
+
print("check")
|
42 |
+
await state.set_state(Other.user_data_finish)
|
43 |
+
await message.answer('Какие данные Вы хотите внести?', reply_markup=kb.yes_no_keyboard)
|
44 |
+
|
45 |
+
|
46 |
+
@router.message(Other.user_data_finish)
|
47 |
+
async def correction_info_set(message: Message, state: FSMContext):
|
48 |
+
data = await state.get_data()
|
49 |
+
if data['correction'] == 'Логин':
|
50 |
+
if not await check_login_unique(message.text) and not own_login_check(message.from_user.id, message.text):
|
51 |
+
await message.answer('Этот логин уже занят. Пожалуйста, выберите другой.')
|
52 |
+
return
|
53 |
+
await update_user_data(message.from_user.id, data['correction'], message.text)
|
54 |
+
await state.clear()
|
55 |
+
await message.answer('Изменения внесены!', reply_markup=kb.user_back_wo_reg)
|
app/handlers/user/leadmagnets.py
ADDED
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from aiogram import Router, F
|
2 |
+
from aiogram.types import Message
|
3 |
+
from aiogram.fsm.context import FSMContext
|
4 |
+
from app.database import requests as rq
|
5 |
+
from app.keyboards import user_keyboards as kb
|
6 |
+
from app.states import LeadMagnetStates
|
7 |
+
|
8 |
+
router = Router()
|
9 |
+
|
10 |
+
@router.message(F.text == "Ввести кодовое слово")
|
11 |
+
async def start_leadmagnet_input(message: Message, state: FSMContext):
|
12 |
+
await state.set_state(LeadMagnetStates.waiting_for_keyword)
|
13 |
+
await message.answer(
|
14 |
+
"Пожалуйста, введите кодовое слово.\n"
|
15 |
+
"Для возврата в меню напишите 'отмена'",
|
16 |
+
reply_markup=kb.user_back
|
17 |
+
)
|
18 |
+
|
19 |
+
@router.message(LeadMagnetStates.waiting_for_keyword)
|
20 |
+
async def process_leadmagnet(message: Message, state: FSMContext):
|
21 |
+
if message.text.lower() == 'отмена':
|
22 |
+
await state.clear()
|
23 |
+
await message.answer(
|
24 |
+
"Возвращаемся в главное меню",
|
25 |
+
reply_markup=kb.user_main
|
26 |
+
)
|
27 |
+
return
|
28 |
+
|
29 |
+
leadmagnet = await rq.get_leadmagnet_info(message.text)
|
30 |
+
|
31 |
+
if not leadmagnet:
|
32 |
+
await message.answer(
|
33 |
+
"К сожалению, такое кодовое слово не найдено.\n"
|
34 |
+
"Проверьте правильность написания или попробуйте другое слово.\n"
|
35 |
+
"Для возврата в меню напишите 'отмена'"
|
36 |
+
)
|
37 |
+
return
|
38 |
+
|
39 |
+
if not leadmagnet.is_active:
|
40 |
+
await message.answer(
|
41 |
+
"К сожалению, это кодовое слово больше не активно.\n"
|
42 |
+
"Попробуйте другое слово или вернитесь в меню, написав 'отмена'"
|
43 |
+
)
|
44 |
+
return
|
45 |
+
|
46 |
+
# Send leadmagnet content
|
47 |
+
await message.answer(leadmagnet.content)
|
48 |
+
|
49 |
+
# Clear state and offer registration if user is not registered
|
50 |
+
await state.clear()
|
51 |
+
|
52 |
+
user = await rq.get_user_info(message.from_user.id)
|
53 |
+
if not user or not user.contact:
|
54 |
+
await message.answer(
|
55 |
+
"Хотите получать больше полезных материалов?\n"
|
56 |
+
"Зарегистрируйтесь, чтобы не пропустить новые акции и предложения!",
|
57 |
+
reply_markup=await kb.get_registration_keyboard()
|
58 |
+
)
|
59 |
+
else:
|
60 |
+
await message.answer(
|
61 |
+
"Можете ввести другое кодовое слово или вернуться в меню",
|
62 |
+
reply_markup=kb.leadmagnet_keyboard
|
63 |
+
)
|
64 |
+
|
65 |
+
@router.message(F.text == "Вернуться в главное меню")
|
66 |
+
async def return_to_main_menu(message: Message, state: FSMContext):
|
67 |
+
await state.clear()
|
68 |
+
await message.answer(
|
69 |
+
"Возвращаемся в главное меню",
|
70 |
+
reply_markup=kb.user_main
|
71 |
+
)
|
app/handlers/user/messaging.py
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from aiogram import Router, F
|
2 |
+
from aiogram.types import Message
|
3 |
+
from aiogram.fsm.context import FSMContext
|
4 |
+
from app.config.config import ADMIN_ID
|
5 |
+
from app.keyboards import user_keyboards as kb
|
6 |
+
from app.states import MessageStates
|
7 |
+
|
8 |
+
router = Router()
|
9 |
+
|
10 |
+
@router.message(F.text == 'Связаться с Ниной')
|
11 |
+
async def pre_contact_admin(message: Message, state: FSMContext):
|
12 |
+
await state.set_state(MessageStates.user_contact_admin)
|
13 |
+
await message.answer('Напишите ваше сообщение для Нины')
|
14 |
+
|
15 |
+
|
16 |
+
@router.message(MessageStates.user_contact_admin)
|
17 |
+
async def contact_admin(message: Message, state: FSMContext):
|
18 |
+
await message.bot.send_message(
|
19 |
+
chat_id=ADMIN_ID[0],
|
20 |
+
text=f'Сообщение от @{message.from_user.username}:'
|
21 |
+
)
|
22 |
+
await message.forward(ADMIN_ID[0])
|
23 |
+
|
24 |
+
await message.answer(
|
25 |
+
'Сообщение отправлено! Нина ответит вам в ближайшее время.',
|
26 |
+
reply_markup=kb.user_back_wo_reg
|
27 |
+
)
|
28 |
+
await state.clear()
|
app/handlers/user/registration.py
ADDED
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from aiogram import Router, F
|
2 |
+
from aiogram.types import Message, CallbackQuery
|
3 |
+
from aiogram.fsm.context import FSMContext
|
4 |
+
from app.database import requests as rq
|
5 |
+
from app.keyboards import user_keyboards as kb
|
6 |
+
from app.states import Register
|
7 |
+
from app.database.requests import check_login_unique
|
8 |
+
|
9 |
+
router = Router()
|
10 |
+
|
11 |
+
@router.callback_query(F.data == "user_register")
|
12 |
+
async def register_start(callback: CallbackQuery, state: FSMContext):
|
13 |
+
await state.set_state(Register.user_register_name)
|
14 |
+
await callback.message.answer('Как к вам обращаться?')
|
15 |
+
|
16 |
+
|
17 |
+
@router.message(Register.user_register_name)
|
18 |
+
async def register_name(message: Message, state: FSMContext):
|
19 |
+
await state.update_data(name=message.text)
|
20 |
+
await state.set_state(Register.user_register_login)
|
21 |
+
await message.answer(
|
22 |
+
'Выберите свой логин. Он должен быть уникальным!'
|
23 |
+
)
|
24 |
+
|
25 |
+
|
26 |
+
@router.message(Register.user_register_login)
|
27 |
+
async def register_name(message: Message, state: FSMContext):
|
28 |
+
if not await check_login_unique(message.text):
|
29 |
+
await message.answer('Этот логин уже занят. Пожалуйста, выберите другой.')
|
30 |
+
return
|
31 |
+
await state.update_data(login=message.text)
|
32 |
+
await state.set_state(Register.user_register_contact)
|
33 |
+
await message.answer('Укажите предпочтительный способ связи с вами')
|
34 |
+
|
35 |
+
|
36 |
+
@router.message(Register.user_register_contact)
|
37 |
+
async def register_contact(message: Message, state: FSMContext):
|
38 |
+
await state.update_data(contact=message.text)
|
39 |
+
await state.set_state(Register.user_register_subscribe)
|
40 |
+
await message.answer(
|
41 |
+
'Хотите получать уведомления о новинках и специальных предложениях?',
|
42 |
+
reply_markup=kb.yes_no_keyboard
|
43 |
+
)
|
44 |
+
|
45 |
+
|
46 |
+
@router.message(Register.user_register_subscribe)
|
47 |
+
async def register_subscribe(message: Message, state: FSMContext):
|
48 |
+
await state.update_data(include_in_broadcast=message.text)
|
49 |
+
if message.text == 'Да':
|
50 |
+
await message.answer('Спасибо за регистрацию! Добро пожаловать в наш тайный клуб клиентов!')
|
51 |
+
elif message.text == 'Нет':
|
52 |
+
await message.answer('Спасибо за регистрацию! Если Вы измените свое решение, Вы всегда можете подписаться '
|
53 |
+
'на наши секретные материалы, воспользовавшись ботом!')
|
54 |
+
data = await state.get_data()
|
55 |
+
await state.set_state(Register.user_register_check)
|
56 |
+
await message.answer(f'Давайте проверим еще раз:'
|
57 |
+
f'\n - Ваше имя - {data["name"]},'
|
58 |
+
f'\n - Ваш логин - {data["login"]},'
|
59 |
+
f'\n - Ваш контакт - {data["contact"]}, '
|
60 |
+
f'\n - Подписаны ли Вы на секретную рассылку: {data["include_in_broadcast"]} '
|
61 |
+
f'\n\n Всё верно?', reply_markup=kb.yes_no_keyboard)
|
62 |
+
|
63 |
+
|
64 |
+
@router.message(Register.user_register_check)
|
65 |
+
async def register_check(message: Message, state: FSMContext):
|
66 |
+
if message.text == 'Да':
|
67 |
+
data = await state.get_data()
|
68 |
+
await rq.user_register(message.from_user.id, data["name"], data["login"], data["contact"], data["include_in_broadcast"])
|
69 |
+
await message.answer('Ура! Спасибо, что Вы с нами! Хотите ли Вы сделать что-то еще?', reply_markup=kb.user_main)
|
70 |
+
await state.clear()
|
71 |
+
if message.text == 'Нет':
|
72 |
+
await state.set_state(Register.user_register_correct)
|
73 |
+
await message.answer('Упс :( Что Вы хотите исправить?', reply_markup=kb.register_correct_keyboard)
|
74 |
+
|
75 |
+
|
76 |
+
@router.message(Register.user_register_correct)
|
77 |
+
async def choose_the_correction(message: Message, state: FSMContext):
|
78 |
+
if message.text == 'Имя':
|
79 |
+
await state.set_state(Register.user_register_name)
|
80 |
+
await message.answer('Как к Вам обращаться?')
|
81 |
+
elif message.text == 'Логин':
|
82 |
+
await state.set_state(Register.user_register_login)
|
83 |
+
await message.answer('Выберите свой логин. Он должен быть уникальным!')
|
84 |
+
elif message.text == 'Контакт':
|
85 |
+
await state.set_state(Register.user_register_contact)
|
86 |
+
await message.answer('Оставьте контакт, с которого Вам будет удобнее всего общаться с нами')
|
87 |
+
elif message.text == 'Статус подписки на рассылку':
|
88 |
+
await state.set_state(Register.user_register_subscribe)
|
89 |
+
await message.answer('Хотите ли Вы получать от нас уведомления о новинках, акциях и даже '
|
90 |
+
'СЕКРЕТНЫХ ��АСПРОДАЖАХ ДЛЯ СВОИХ?', reply_markup=kb.yes_no_keyboard)
|
91 |
+
|
92 |
+
|
93 |
+
@router.callback_query(F.data == "user_to_main")
|
94 |
+
async def register(callback: CallbackQuery):
|
95 |
+
await callback.answer("Возвращаемся в меню")
|
96 |
+
await callback.message.answer("Чем я могу Вам помочь?", reply_markup=kb.user_main)
|
app/handlers/user/router.py
ADDED
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from aiogram import Router
|
2 |
+
from aiogram.filters import Command
|
3 |
+
from aiogram.types import Message
|
4 |
+
from app.database import requests as rq
|
5 |
+
from app.keyboards import user_keyboards as kb
|
6 |
+
|
7 |
+
from .catalog import router as catalog_router
|
8 |
+
from .registration import router as registration_router
|
9 |
+
from .messaging import router as messaging_router
|
10 |
+
from .leadmagnets import router as leadmagnet_router
|
11 |
+
from .tests import router as test_router
|
12 |
+
from .info_check import router as info_check_router
|
13 |
+
from .send_feedback import router as send_feedback_router
|
14 |
+
|
15 |
+
user_router = Router()
|
16 |
+
|
17 |
+
user_router.include_router(catalog_router)
|
18 |
+
user_router.include_router(leadmagnet_router)
|
19 |
+
user_router.include_router(registration_router)
|
20 |
+
user_router.include_router(messaging_router)
|
21 |
+
user_router.include_router(test_router)
|
22 |
+
user_router.include_router(info_check_router)
|
23 |
+
user_router.include_router(send_feedback_router)
|
24 |
+
|
25 |
+
|
26 |
+
@user_router.message(Command('start'))
|
27 |
+
async def cmd_start(message: Message):
|
28 |
+
await rq.set_user(message.from_user.id)
|
29 |
+
await message.answer(
|
30 |
+
'Добро пожаловать! Я - телеграм-бот Нины Смирновой. '
|
31 |
+
'Я могу помочь вам:\n'
|
32 |
+
'- Узнать о наших услугах\n'
|
33 |
+
'- Связать вас с Ниной\n'
|
34 |
+
'- Показать полезные материалы\n'
|
35 |
+
'- Записать ваш отзыв\n'
|
36 |
+
'- Предложить интересные тесты\n\n'
|
37 |
+
'Чем могу помочь?',
|
38 |
+
reply_markup=kb.user_main
|
39 |
+
)
|
40 |
+
|
41 |
+
@user_router.message(Command('help'))
|
42 |
+
async def cmd_help(message: Message):
|
43 |
+
await message.answer(
|
44 |
+
'Я могу помочь вам:\n'
|
45 |
+
'1. Просмотреть каталог услуг\n'
|
46 |
+
'2. Связаться с Ниной\n'
|
47 |
+
'3. Пройти тесты\n'
|
48 |
+
'4. Оставить отзыв\n'
|
49 |
+
'5. Зарегистрироваться для доступа к специальным предложениям'
|
50 |
+
)
|
app/handlers/user/send_feedback.py
ADDED
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from aiogram import Router, F
|
2 |
+
from aiogram.types import Message, CallbackQuery
|
3 |
+
from aiogram.fsm.context import FSMContext
|
4 |
+
from app.keyboards import user_keyboards as kb
|
5 |
+
from app.states import MessageStates
|
6 |
+
from app.keyboards.user_keyboards import user_keyboard_feedback
|
7 |
+
from app.database import requests as rq
|
8 |
+
from app.config.config import ADMIN_ID
|
9 |
+
|
10 |
+
router = Router()
|
11 |
+
|
12 |
+
@router.message(F.text == 'Оставить отзыв')
|
13 |
+
async def pre_contact_admin(message: Message, state: FSMContext):
|
14 |
+
await state.set_state(MessageStates.user_send_feedback)
|
15 |
+
await message.answer('Выберите услугу, на которую Вы хотите оставить отзыв', reply_markup=await user_keyboard_feedback())
|
16 |
+
|
17 |
+
|
18 |
+
@router.callback_query(F.data.startswith('feedback_'))
|
19 |
+
async def choose_service(callback: CallbackQuery, state: FSMContext):
|
20 |
+
await state.set_state(MessageStates.user_choosing_feedback_service)
|
21 |
+
await callback.answer('')
|
22 |
+
service_info = await rq.get_service_info(callback.data.split('_')[1])
|
23 |
+
await state.update_data(name=service_info.service_name)
|
24 |
+
await callback.message.answer(f"Введите ваш отзыв на услугу {service_info.service_name}")
|
25 |
+
|
26 |
+
|
27 |
+
@router.message(MessageStates.user_choosing_feedback_service)
|
28 |
+
async def contact_admin(message: Message, state: FSMContext):
|
29 |
+
await state.set_state(MessageStates.user_sending_feedback)
|
30 |
+
data = await state.get_data()
|
31 |
+
print(data)
|
32 |
+
await message.bot.send_message(
|
33 |
+
chat_id=ADMIN_ID[0],
|
34 |
+
text=f'Новый отзыв на услугу #{data["name"]}:'
|
35 |
+
)
|
36 |
+
await message.forward(ADMIN_ID[0])
|
37 |
+
|
38 |
+
await message.answer(
|
39 |
+
'Отзыв отправлен! Спасибо за использование наших услуг!.',
|
40 |
+
reply_markup=kb.user_back_wo_reg
|
41 |
+
)
|
42 |
+
await state.clear()
|
app/handlers/user/tests.py
ADDED
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from aiogram import Router, F
|
2 |
+
from aiogram.types import Message, CallbackQuery
|
3 |
+
from aiogram.fsm.context import FSMContext
|
4 |
+
from app.database import requests as rq
|
5 |
+
from app.keyboards import user_keyboards as kb
|
6 |
+
from app.states import TestStates
|
7 |
+
from app.database.requests import check_user_registered
|
8 |
+
|
9 |
+
router = Router()
|
10 |
+
|
11 |
+
@router.message(F.text == "Пройти тест")
|
12 |
+
async def show_available_tests(message: Message):
|
13 |
+
registration_status = await check_user_registered(message.from_user.id)
|
14 |
+
if registration_status:
|
15 |
+
await message.answer(
|
16 |
+
"Выберите тест:",
|
17 |
+
reply_markup=await kb.get_tests_keyboard()
|
18 |
+
)
|
19 |
+
if not registration_status:
|
20 |
+
await message.answer(
|
21 |
+
"Зарегистрируйтесь, чтобы сохранить Ваши результаты и получить персональные рекомендации!",
|
22 |
+
reply_markup=kb.user_back
|
23 |
+
)
|
24 |
+
|
25 |
+
@router.callback_query(F.data.startswith("start_test_"))
|
26 |
+
async def start_test(callback: CallbackQuery, state: FSMContext):
|
27 |
+
test_id = callback.data.split("_")[2]
|
28 |
+
test_data = await rq.start_test_attempt(
|
29 |
+
callback.from_user.id,
|
30 |
+
test_id
|
31 |
+
)
|
32 |
+
|
33 |
+
if not test_data:
|
34 |
+
await callback.message.answer("Тест недоступен")
|
35 |
+
return
|
36 |
+
|
37 |
+
await state.set_state(TestStates.answering)
|
38 |
+
await state.update_data(
|
39 |
+
attempt_id=test_data["attempt_id"],
|
40 |
+
current_question=1,
|
41 |
+
total_questions=test_data["total_questions"]
|
42 |
+
)
|
43 |
+
|
44 |
+
await callback.message.answer(
|
45 |
+
f"Вопрос 1 из {test_data['total_questions']}:\n\n"
|
46 |
+
f"{test_data['question'].question_content}",
|
47 |
+
reply_markup=await kb.get_test_question_keyboard(test_data["question"])
|
48 |
+
)
|
49 |
+
|
50 |
+
@router.callback_query(F.data.startswith("test_answer_"))
|
51 |
+
async def process_answer(callback: CallbackQuery, state: FSMContext):
|
52 |
+
_, _, question_id, answer = callback.data.split("_")
|
53 |
+
|
54 |
+
data = await state.get_data()
|
55 |
+
result = await rq.record_answer(data["attempt_id"],
|
56 |
+
int(question_id),
|
57 |
+
answer
|
58 |
+
)
|
59 |
+
|
60 |
+
if result.get("completed"):
|
61 |
+
await callback.message.answer(
|
62 |
+
f"Тест завершен!\n\n"
|
63 |
+
f"Ваш результат: {result['result']}"
|
64 |
+
)
|
65 |
+
await state.clear()
|
66 |
+
else:
|
67 |
+
current_q = data["current_question"] + 1
|
68 |
+
await state.update_data(current_question=current_q)
|
69 |
+
|
70 |
+
await callback.message.answer(
|
71 |
+
f"Вопрос {current_q} из {data['total_questions']}:\n\n"
|
72 |
+
f"{result['next_question'].question_content}",
|
73 |
+
reply_markup=await kb.get_test_question_keyboard(result["next_question"])
|
74 |
+
)
|
app/handlers/user_route.py
ADDED
@@ -0,0 +1,168 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from aiogram.filters.command import Command
|
2 |
+
from aiogram.types import Message, CallbackQuery
|
3 |
+
from aiogram import F, Router, Bot
|
4 |
+
from aiogram.fsm.context import FSMContext
|
5 |
+
|
6 |
+
import app.keyboards.user_keyboards as kb
|
7 |
+
import app.database.requests as rq
|
8 |
+
from app.states import Register, Other
|
9 |
+
|
10 |
+
user_router = Router()
|
11 |
+
bot = Bot(token='5569983238:AAFo8J083zUWOk879M7YMYfhAreeiLovGcE')
|
12 |
+
|
13 |
+
|
14 |
+
@user_router.message(Command('start'))
|
15 |
+
async def cmd_start(message: Message):
|
16 |
+
await rq.set_user(message.from_user.id)
|
17 |
+
await message.answer('Добро пожаловать! Я - телеграм-бот Нины Смирновой, и моя основная задача - взаимодействие с '
|
18 |
+
'Вами: я могу предоставить Вам информацию о наших услугах и связать Вас с Ниной, а также '
|
19 |
+
'показать Вам интересные и полезные материалы от Нины. Еще я могу записать Ваш отзыв, '
|
20 |
+
'если Вы уже воспользовались нашими услугами! А еще (только это секрет) у меня есть очень'
|
21 |
+
'интересные тесты, которые позволят Вам лучше понять себя и Вашего ребенка! \n\n '
|
22 |
+
'Чем я могу Вам помочь?',
|
23 |
+
reply_markup=kb.user_main)
|
24 |
+
|
25 |
+
|
26 |
+
@user_router.message(Command('help'))
|
27 |
+
async def cmd_help(message: Message):
|
28 |
+
await message.answer('Это кнопка помощи')
|
29 |
+
|
30 |
+
|
31 |
+
@user_router.message(F.text == 'Посмотреть каталог услуг')
|
32 |
+
async def get_catalog(message: Message):
|
33 |
+
await message.answer('Спасибо за интерес! Вот наш перечень услуг. '
|
34 |
+
'Пожалуйста, выберите самую подходящую и нажмите на соответствующую кнопку, '
|
35 |
+
'чтобы узнать о ней подробнее:', reply_markup=await kb.user_keyboard_catalog())
|
36 |
+
|
37 |
+
|
38 |
+
@user_router.callback_query(F.data.startswith('service_'))
|
39 |
+
async def service_info(callback: CallbackQuery,state: FSMContext):
|
40 |
+
await callback.answer('')
|
41 |
+
service_info = await rq.get_service_info(callback.data.split('_')[1])
|
42 |
+
service_string = f"Вот информация по выбранной Вами услуге: \n" \
|
43 |
+
f"Название: {service_info.service_name} \n" \
|
44 |
+
f"Описание: {service_info.service_description} \n" \
|
45 |
+
f"Цена: {service_info.service_price}. \n\n" \
|
46 |
+
f"Хотите ли Вы воспользоваться данной услугой?"
|
47 |
+
await callback.answer('')
|
48 |
+
await state.set_state(Other.user_pre_buy_service)
|
49 |
+
await state.update_data(user_id=callback.from_user.id,
|
50 |
+
user_username=callback.from_user.username,
|
51 |
+
service=service_info.service_name)
|
52 |
+
await callback.message.answer(service_string, reply_markup=kb.service_confirm)
|
53 |
+
|
54 |
+
|
55 |
+
@user_router.message(F.text == 'Да, связаться с Ниной')
|
56 |
+
async def admin_prompt(message: Message, state: FSMContext):
|
57 |
+
await state.set_state(Other.user_buy_service)
|
58 |
+
await message.answer('Напишите Нине и изложите Вашу проблему - это поможет ей лучше понять Ваш конкретный случай'
|
59 |
+
' и сделать Ваш разговор более предметным!')
|
60 |
+
|
61 |
+
|
62 |
+
@user_router.message(Other.user_buy_service)
|
63 |
+
async def admin_connect(message: Message, state: FSMContext):
|
64 |
+
data = await state.get_data()
|
65 |
+
await bot.send_message(chat_id=1658604792,
|
66 |
+
text=f'Здравствуйте! Поступил новый заказ на услугу {data["service"]} от пользователя'
|
67 |
+
f' @{data["user_username"]}')
|
68 |
+
await message.send_copy(1658604792)
|
69 |
+
await message.answer('Спасибо за обращение! Скоро Нина свяжется с Вами в личных сообщениях для обсуждения '
|
70 |
+
'подробностей Вашего заказа!')
|
71 |
+
await state.clear()
|
72 |
+
await message.answer('Вы можете вернуться в меню и посмотреть, какие еще услуги мы предлагаем! Еще мы предлагаем '
|
73 |
+
'зарегистрироваться, чтобы получить возможность не пропускать всё самое важное, что происходит '
|
74 |
+
'в канале. Также регистрация позволит ��ам вступить в СЕКРЕТНЫЙ КЛУБ пользователей, которые'
|
75 |
+
' будут иметь доступ к специальным акциям и ВКУСНЫМ ЦЕНАМ для своих!',
|
76 |
+
reply_markup=kb.user_back)
|
77 |
+
|
78 |
+
|
79 |
+
@user_router.message(F.text == 'Нет, вернуться в меню')
|
80 |
+
async def np_service_return_to_menu(message: Message, state: FSMContext):
|
81 |
+
await state.clear()
|
82 |
+
await message.answer('Чем я могу Вам помочь?', reply_markup=kb.user_main)
|
83 |
+
|
84 |
+
|
85 |
+
@user_router.callback_query(F.data == "user_register")
|
86 |
+
async def register(callback: CallbackQuery, state: FSMContext):
|
87 |
+
await state.set_state(Register.user_register_name)
|
88 |
+
await callback.message.answer('Как к Вам обращаться?')
|
89 |
+
|
90 |
+
|
91 |
+
@user_router.callback_query(F.data == "user_to_main")
|
92 |
+
async def register(callback: CallbackQuery):
|
93 |
+
await callback.answer("Возвращаемся в меню")
|
94 |
+
await callback.message.answer("Чем я могу Вам помочь?", reply_markup=kb.user_main)
|
95 |
+
|
96 |
+
|
97 |
+
@user_router.message(Register.user_register_name)
|
98 |
+
async def register_name(message: Message, state: FSMContext):
|
99 |
+
await state.update_data(name=message.text)
|
100 |
+
await state.set_state(Register.user_register_contact)
|
101 |
+
await message.answer('Оставьте контакт, с которого Вам будет удобнее всего общаться с нами')
|
102 |
+
|
103 |
+
|
104 |
+
@user_router.message(Register.user_register_contact)
|
105 |
+
async def register_contact(message: Message, state: FSMContext):
|
106 |
+
await state.update_data(contact=message.text)
|
107 |
+
await state.set_state(Register.user_register_subscribe)
|
108 |
+
await message.answer('Хотите ли Вы получать от нас уведомления о новинках, акциях и даже '
|
109 |
+
'СЕКРЕТНЫХ РАСПРОДАЖАХ ДЛЯ СВОИХ?', reply_markup=kb.yes_no_keyboard)
|
110 |
+
|
111 |
+
|
112 |
+
@user_router.message(Register.user_register_subscribe)
|
113 |
+
async def register_subscribe(message: Message, state: FSMContext):
|
114 |
+
await state.update_data(include_in_broadcast=message.text)
|
115 |
+
if message.text == 'Да':
|
116 |
+
await message.answer('Спасибо за регистрацию! Добро пожаловать в наш тайный клуб клиентов!')
|
117 |
+
elif message.text == 'Нет':
|
118 |
+
await message.answer('Спасибо за регистрацию! Если Вы измените свое решение, Вы всегда можете подписаться '
|
119 |
+
'на наши секретные материалы, воспользовавшись ботом!')
|
120 |
+
data = await state.get_data()
|
121 |
+
await state.set_state(Register.user_register_check)
|
122 |
+
await message.answer(f'Давайте проверим еще раз:'
|
123 |
+
f'\n - Ваше имя - {data["name"]},'
|
124 |
+
f'\n - Ваш контакт - {data["contact"]}, '
|
125 |
+
f'\n - Подписаны ли Вы на секретную рассылку: {data["include_in_broadcast"]} '
|
126 |
+
f'\n\n Всё верно?', reply_markup=kb.yes_no_keyboard)
|
127 |
+
|
128 |
+
|
129 |
+
@user_router.message(Register.user_register_check)
|
130 |
+
async def register_check(message: Message, state: FSMContext):
|
131 |
+
if message.text == 'Да':
|
132 |
+
data = await state.get_data()
|
133 |
+
await rq.user_register(message.from_user.id, data["name"], data["contact"], data["include_in_broadcast"])
|
134 |
+
await message.answer('Ура! Спасибо, что Вы с нами! Хотите ли Вы сделать что-то еще?', reply_markup=kb.user_main)
|
135 |
+
await state.clear()
|
136 |
+
if message.text == 'Нет':
|
137 |
+
await state.set_state(Register.user_register_correct)
|
138 |
+
await message.answer('Упс :( Что Вы хотите исправить?', reply_markup=kb.register_correct_keyboard)
|
139 |
+
|
140 |
+
|
141 |
+
@user_router.message(Register.user_register_correct)
|
142 |
+
async def choose_the_correction(message: Message, state: FSMContext):
|
143 |
+
if message.text == 'Имя':
|
144 |
+
await state.set_state(Register.user_register_name)
|
145 |
+
await message.answer('Как к Вам обращаться?')
|
146 |
+
elif message.text == 'Контакт':
|
147 |
+
await state.set_state(Register.user_register_contact)
|
148 |
+
await message.answer('Оставьте контакт, с которого Вам будет удобнее всего общаться с нами')
|
149 |
+
elif message.text == 'Статус подписки на рассылку':
|
150 |
+
await state.set_state(Register.user_register_subscribe)
|
151 |
+
await message.answer('Хотите ли Вы получать от нас уведомления о новинках, акциях и даже '
|
152 |
+
'СЕКРЕТНЫХ РАСПРОДАЖАХ ДЛЯ СВОИХ?', reply_markup=kb.yes_no_keyboard)
|
153 |
+
|
154 |
+
|
155 |
+
@user_router.message(F.text == 'Связаться с Ниной')
|
156 |
+
async def pre_get_back_to_admin(message: Message, state: FSMContext):
|
157 |
+
await state.set_state(Other.user_contact_admin)
|
158 |
+
await message.answer('Введите Ваше сообщение - и я отправлю его Нине!')
|
159 |
+
|
160 |
+
|
161 |
+
@user_router.message(Other.user_contact_admin)
|
162 |
+
async def get_back_to_admin(message: Message, state: FSMContext):
|
163 |
+
await bot.send_message(chat_id=1658604792,
|
164 |
+
text=f'Здравствуйте! Вам поступило сообщение от пользователя'
|
165 |
+
f' @{message.from_user.username}:')
|
166 |
+
await message.send_copy(1658604792)
|
167 |
+
await message.answer('Спасибо за обращение! Нина получила Ваше сообщение и ответит Вам, как только сможет!',
|
168 |
+
reply_markup=kb.user_back_wo_reg)
|
app/keyboards/__init__.py
ADDED
File without changes
|
app/keyboards/admin_keyboards.py
ADDED
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from aiogram.types import ReplyKeyboardMarkup, KeyboardButton, InlineKeyboardMarkup, InlineKeyboardButton
|
2 |
+
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
3 |
+
from app.database.requests import get_catalog, get_leadmagnets, get_tests
|
4 |
+
|
5 |
+
|
6 |
+
async def admin_keyboard_service_catalog():
|
7 |
+
all_items = await get_catalog()
|
8 |
+
keyboard = InlineKeyboardBuilder()
|
9 |
+
if all_items:
|
10 |
+
for item in all_items:
|
11 |
+
keyboard.add(InlineKeyboardButton(text=item.service_name, callback_data=f'change_service_{item.id}'))
|
12 |
+
keyboard.add(InlineKeyboardButton(text='Вернуться', callback_data='admin_to_main'))
|
13 |
+
keyboard.add(InlineKeyboardButton(text='Добавить услугу', callback_data='add_service'))
|
14 |
+
return keyboard.adjust(1).as_markup()
|
15 |
+
|
16 |
+
|
17 |
+
async def admin_change_service(service_id):
|
18 |
+
keyboard = InlineKeyboardBuilder()
|
19 |
+
keyboard.add(InlineKeyboardButton(text='Название', callback_data=f'editservice_name_{service_id}'))
|
20 |
+
keyboard.add(InlineKeyboardButton(text='Описание', callback_data=f'editservice_desc_{service_id}'))
|
21 |
+
keyboard.add(InlineKeyboardButton(text='Цена', callback_data=f'editservice_price_{service_id}'))
|
22 |
+
keyboard.add(InlineKeyboardButton(text='Удалить услугу', callback_data=f'deleteservice_{service_id}'))
|
23 |
+
keyboard.add(InlineKeyboardButton(text='Вернуться', callback_data='admin_to_main'))
|
24 |
+
return keyboard.adjust(1).as_markup()
|
25 |
+
|
26 |
+
|
27 |
+
async def admin_keyboard_leadmagnets():
|
28 |
+
all_items = await get_leadmagnets()
|
29 |
+
keyboard = InlineKeyboardBuilder()
|
30 |
+
if all_items:
|
31 |
+
for item in all_items:
|
32 |
+
keyboard.add(InlineKeyboardButton(text=item, callback_data=f'change_leadmagnet_{item}'))
|
33 |
+
keyboard.add(InlineKeyboardButton(text='Вернуться', callback_data='admin_to_main'))
|
34 |
+
keyboard.add(InlineKeyboardButton(text='Добавить кодовое слово', callback_data='add_leadmanget'))
|
35 |
+
return keyboard.adjust(1).as_markup()
|
36 |
+
|
37 |
+
|
38 |
+
async def admin_change_leadmagnet(trigger):
|
39 |
+
keyboard = InlineKeyboardBuilder()
|
40 |
+
keyboard.add(InlineKeyboardButton(text='Слово-триггер', callback_data=f'editleadmagnet_trigger_{trigger}'))
|
41 |
+
keyboard.add(InlineKeyboardButton(text='Содержание лидмагнита', callback_data=f'editleadmagnet_content_{trigger}'))
|
42 |
+
keyboard.add(InlineKeyboardButton(text='Статус активности лидмагнита', callback_data=f'editleadmagnet_status_{trigger}'))
|
43 |
+
keyboard.add(InlineKeyboardButton(text='Удалить лидмагнит', callback_data=f'deleteleadmagnet_{trigger}'))
|
44 |
+
keyboard.add(InlineKeyboardButton(text='Вернуться', callback_data='admin_to_main'))
|
45 |
+
return keyboard.adjust(1).as_markup()
|
46 |
+
|
47 |
+
|
48 |
+
async def admin_change_test(t_id):
|
49 |
+
keyboard = InlineKeyboardBuilder()
|
50 |
+
keyboard.add(
|
51 |
+
InlineKeyboardButton(text='Статус активности теста', callback_data=f'edittest_status_{t_id}'))
|
52 |
+
keyboard.add(InlineKeyboardButton(text='Удалить тест', callback_data=f'deletetest_{t_id}'))
|
53 |
+
keyboard.add(InlineKeyboardButton(text='Вернуться', callback_data='admin_to_main'))
|
54 |
+
return keyboard.adjust(1).as_markup()
|
55 |
+
|
56 |
+
|
57 |
+
async def admin_keyboard_tests():
|
58 |
+
all_items = await get_tests()
|
59 |
+
keyboard = InlineKeyboardBuilder()
|
60 |
+
if all_items:
|
61 |
+
for item in all_items:
|
62 |
+
keyboard.add(InlineKeyboardButton(text=item.test_name, callback_data=f'change_test_{item.id}'))
|
63 |
+
keyboard.add(InlineKeyboardButton(text='Вернуться', callback_data='admin_to_main'))
|
64 |
+
keyboard.add(InlineKeyboardButton(text='Добавить тест', callback_data='add_test'))
|
65 |
+
return keyboard.adjust(1).as_markup()
|
66 |
+
|
67 |
+
|
68 |
+
admin_main = ReplyKeyboardMarkup(keyboard=[[KeyboardButton(text='Отредактировать каталог услуг'),
|
69 |
+
KeyboardButton(text='Отредактировать тесты')],
|
70 |
+
[KeyboardButton(text='Отредактировать кодовые слова'),
|
71 |
+
KeyboardButton(text='Отправить сообщение в рассылку'),],
|
72 |
+
[KeyboardButton(text='Просмотреть результаты тестов')],
|
73 |
+
[KeyboardButton(text='Вернуться в пользовательский интерфейс')]],
|
74 |
+
resize_keyboard=True,
|
75 |
+
input_field_placeholder='Выберите подходящий Вам вариант, нажав на кнопку внизу:')
|
76 |
+
|
77 |
+
admin_back = InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text='Вернуться в меню',
|
78 |
+
callback_data='admin_to_main')]])
|
79 |
+
|
80 |
+
yes_no_keyboard = ReplyKeyboardMarkup(keyboard=[[KeyboardButton(text='Да'), KeyboardButton(text='Нет')]],
|
81 |
+
resize_keyboard=True)
|
82 |
+
|
83 |
+
test_type_keyboard = ReplyKeyboardMarkup(keyboard=[[KeyboardButton(text='С баллами'), KeyboardButton(text='Без баллов')]],
|
84 |
+
resize_keyboard=True)
|
app/keyboards/user_keyboards.py
ADDED
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from aiogram.types import ReplyKeyboardMarkup, KeyboardButton, InlineKeyboardMarkup, InlineKeyboardButton
|
2 |
+
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
3 |
+
from app.database.requests import get_catalog, get_tests
|
4 |
+
from app.database.requests import check_login_unique
|
5 |
+
|
6 |
+
|
7 |
+
async def user_keyboard_catalog():
|
8 |
+
all_items = await get_catalog()
|
9 |
+
keyboard = InlineKeyboardBuilder()
|
10 |
+
if all_items:
|
11 |
+
for item in all_items:
|
12 |
+
keyboard.add(InlineKeyboardButton(text=item.service_name, callback_data=f'service_{item.id}'))
|
13 |
+
keyboard.add(InlineKeyboardButton(text='Вернуться', callback_data='user_to_main'))
|
14 |
+
return keyboard.adjust(1).as_markup()
|
15 |
+
|
16 |
+
|
17 |
+
async def user_keyboard_feedback():
|
18 |
+
all_items = await get_catalog()
|
19 |
+
keyboard = InlineKeyboardBuilder()
|
20 |
+
if all_items:
|
21 |
+
for item in all_items:
|
22 |
+
keyboard.add(InlineKeyboardButton(text=item.service_name, callback_data=f'feedback_{item.id}'))
|
23 |
+
keyboard.add(InlineKeyboardButton(text='Вернуться', callback_data='user_to_main'))
|
24 |
+
return keyboard.adjust(1).as_markup()
|
25 |
+
|
26 |
+
|
27 |
+
user_main = ReplyKeyboardMarkup(keyboard=[[KeyboardButton(text='Посмотреть каталог услуг'),
|
28 |
+
KeyboardButton(text='Пройти тест')],
|
29 |
+
[KeyboardButton(text='Ввести кодовое слово'),
|
30 |
+
KeyboardButton(text='Оставить отзыв')],
|
31 |
+
[KeyboardButton(text='Посмотреть сохраненные сведения о себе'),
|
32 |
+
KeyboardButton(text='Связаться с Ниной')]],
|
33 |
+
resize_keyboard=True,
|
34 |
+
input_field_placeholder='Выберите подходящий Вам вариант, нажав на кнопку внизу:')
|
35 |
+
|
36 |
+
|
37 |
+
yes_no_keyboard = ReplyKeyboardMarkup(keyboard=[[KeyboardButton(text='Да'), KeyboardButton(text='Нет')]],
|
38 |
+
resize_keyboard=True)
|
39 |
+
|
40 |
+
register_correct_keyboard = ReplyKeyboardMarkup(keyboard=[[KeyboardButton(text='Имя'),
|
41 |
+
KeyboardButton(text='Контакт'),
|
42 |
+
KeyboardButton(text='Логин'),
|
43 |
+
KeyboardButton(text='Статус подписки на рассылку')]],
|
44 |
+
resize_keyboard=True,
|
45 |
+
input_field_placeholder='Выберите, какой параметр Вы бы хотели изменить')
|
46 |
+
|
47 |
+
service_confirm = ReplyKeyboardMarkup(keyboard=[[KeyboardButton(text='Да, связаться с Ниной'),
|
48 |
+
KeyboardButton(text='Нет, вернуться в меню')]],
|
49 |
+
resize_keyboard=True)
|
50 |
+
|
51 |
+
user_back = InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text='Зарегистрироваться',
|
52 |
+
callback_data='user_register')],
|
53 |
+
[InlineKeyboardButton(text='Вернуться в меню',
|
54 |
+
callback_data='user_to_main')]])
|
55 |
+
|
56 |
+
user_back_wo_reg = InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text='Вернуться в меню',
|
57 |
+
callback_data='user_to_main')]])
|
58 |
+
|
59 |
+
|
60 |
+
user_infocheck_back = InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text='Вернуться в меню',
|
61 |
+
callback_data='user_to_main'),
|
62 |
+
InlineKeyboardButton(text='Изменить данные',
|
63 |
+
callback_data='data_correct')]])
|
64 |
+
|
65 |
+
|
66 |
+
leadmagnet_keyboard = ReplyKeyboardMarkup(
|
67 |
+
keyboard=[
|
68 |
+
[KeyboardButton(text="Ввести кодовое слово")],
|
69 |
+
[KeyboardButton(text="Вернуться в главное меню")]
|
70 |
+
],
|
71 |
+
resize_keyboard=True
|
72 |
+
)
|
73 |
+
|
74 |
+
|
75 |
+
async def get_test_question_keyboard(question) -> InlineKeyboardMarkup:
|
76 |
+
"""Create keyboard for test question variants"""
|
77 |
+
variants = question.question_variants.split('\n')
|
78 |
+
buttons = []
|
79 |
+
for variant in variants:
|
80 |
+
variant = variant.strip()
|
81 |
+
if variant:
|
82 |
+
buttons.append([
|
83 |
+
InlineKeyboardButton(
|
84 |
+
text=variant.split('...')[0],
|
85 |
+
callback_data=f"test_answer_{question.id}_{variant}"
|
86 |
+
)
|
87 |
+
])
|
88 |
+
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
89 |
+
|
90 |
+
|
91 |
+
async def get_tests_keyboard():
|
92 |
+
all_items = await get_tests()
|
93 |
+
keyboard = InlineKeyboardBuilder()
|
94 |
+
if all_items:
|
95 |
+
for item in all_items:
|
96 |
+
keyboard.add(InlineKeyboardButton(text=f"📝 {item.test_name}", callback_data=f"start_test_{item.id}"))
|
97 |
+
keyboard.add(InlineKeyboardButton(text="◀️ Вернуться в меню", callback_data="user_to_main"))
|
98 |
+
return keyboard.adjust(1).as_markup()
|
app/middleware/__init__.py
ADDED
File without changes
|
app/middleware/authentification.py
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import Dict, Any
|
2 |
+
from aiogram.types import Message
|
3 |
+
from app.config.config import ADMIN_ID
|
4 |
+
from app.database.requests import check_user_registered
|
5 |
+
|
6 |
+
async def admin_check(message: Message, data: Dict[str, Any]) -> bool:
|
7 |
+
return message.from_user.id in ADMIN_ID
|
8 |
+
|
9 |
+
async def registration_check(message: Message, data: Dict[str, Any]) -> bool:
|
10 |
+
return await check_user_registered(message.from_user.id)
|
app/states.py
ADDED
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from aiogram.fsm.state import State, StatesGroup
|
2 |
+
|
3 |
+
|
4 |
+
class AdminStates(StatesGroup):
|
5 |
+
admin_delete_service = State()
|
6 |
+
admin_edit_service = State()
|
7 |
+
admin_edit_leadmagnet = State()
|
8 |
+
admin_delete_leadmagnet = State()
|
9 |
+
admin_add_test = State()
|
10 |
+
admin_broadcast = State()
|
11 |
+
admin_edit_test_status = State()
|
12 |
+
admin_delete_test = State()
|
13 |
+
|
14 |
+
|
15 |
+
class AdminAddService(StatesGroup):
|
16 |
+
admin_add_name = State()
|
17 |
+
admin_add_desc = State()
|
18 |
+
admin_add_price = State()
|
19 |
+
|
20 |
+
|
21 |
+
class AdminAddLeadmagnet(StatesGroup):
|
22 |
+
admin_set_status = State()
|
23 |
+
admin_set_trigger = State()
|
24 |
+
admin_set_content = State()
|
25 |
+
|
26 |
+
|
27 |
+
class AdminAddTest(StatesGroup):
|
28 |
+
admin_set_title = State()
|
29 |
+
admin_set_type = State()
|
30 |
+
admin_set_desc = State()
|
31 |
+
admin_set_status = State()
|
32 |
+
admin_set_completion_result_set = State()
|
33 |
+
admin_set_completion_result = State()
|
34 |
+
admin_add_question_content = State()
|
35 |
+
admin_add_question_vars = State()
|
36 |
+
admin_add_question_vars_wo_points = State()
|
37 |
+
admin_end_results = State()
|
38 |
+
admin_end_questions = State()
|
39 |
+
admin_add_question_points = State()
|
40 |
+
|
41 |
+
|
42 |
+
class Register(StatesGroup):
|
43 |
+
user_register_name = State()
|
44 |
+
user_register_login = State()
|
45 |
+
user_register_contact = State()
|
46 |
+
user_register_subscribe = State()
|
47 |
+
user_register_check = State()
|
48 |
+
user_register_correct = State()
|
49 |
+
|
50 |
+
|
51 |
+
class Other(StatesGroup):
|
52 |
+
user_buy_service = State()
|
53 |
+
user_pre_buy_service = State()
|
54 |
+
user_pass_test = State()
|
55 |
+
user_receive_magnet = State()
|
56 |
+
user_leave_feedback = State()
|
57 |
+
user_contact_admin = State()
|
58 |
+
user_data_check = State()
|
59 |
+
user_data_correct = State()
|
60 |
+
user_data_finish = State()
|
61 |
+
admin_send_mailing = State()
|
62 |
+
|
63 |
+
|
64 |
+
class MessageStates(StatesGroup):
|
65 |
+
user_contact_admin = State()
|
66 |
+
user_send_feedback = State()
|
67 |
+
user_choosing_feedback_service = State()
|
68 |
+
user_sending_feedback = State()
|
69 |
+
|
70 |
+
|
71 |
+
class LeadMagnetStates(StatesGroup):
|
72 |
+
waiting_for_keyword = State()
|
73 |
+
|
74 |
+
|
75 |
+
class TestStates(StatesGroup):
|
76 |
+
answering = State()
|
77 |
+
name_get = State()
|
app/utils/exceptions.py
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
class DatabaseError(Exception):
|
2 |
+
"""Base exception for database operations."""
|
3 |
+
pass
|
4 |
+
|
5 |
+
class ValidationError(Exception):
|
6 |
+
"""Exception for data validation errors."""
|
7 |
+
pass
|
docker-compose.yml
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
version: '3.8'
|
2 |
+
|
3 |
+
services:
|
4 |
+
bot:
|
5 |
+
build: .
|
6 |
+
env_file:
|
7 |
+
- .env
|
8 |
+
volumes:
|
9 |
+
- ./db.sqlite3:/app/db.sqlite3
|
10 |
+
ports:
|
11 |
+
- "7860:7860"
|
12 |
+
restart: always
|
env.example
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
BOT_TOKEN=8787878787878787878787
|
2 |
+
DATABASE_URL=sqlite+aiosqlite:///db.sqlite3
|
3 |
+
ADMIN_IDS=ID1,ID2,ID3
|
main.py
ADDED
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import asyncio
|
2 |
+
import logging
|
3 |
+
from aiogram import Bot, Dispatcher
|
4 |
+
from app.database.models import async_main
|
5 |
+
from app.handlers.user.router import user_router
|
6 |
+
from app.handlers.admin.router import admin_router
|
7 |
+
from app.config.config import BOT_TOKEN
|
8 |
+
|
9 |
+
|
10 |
+
logging.basicConfig(
|
11 |
+
level=logging.INFO,
|
12 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
13 |
+
)
|
14 |
+
logger = logging.getLogger(__name__)
|
15 |
+
|
16 |
+
|
17 |
+
async def shutdown(bot: Bot):
|
18 |
+
"""Выключение"""
|
19 |
+
logger.info("Выключение бота...")
|
20 |
+
await bot.close()
|
21 |
+
logger.info("Бот успешно выключен")
|
22 |
+
|
23 |
+
|
24 |
+
async def main():
|
25 |
+
try:
|
26 |
+
await async_main()
|
27 |
+
bot = Bot(token=BOT_TOKEN)
|
28 |
+
dp = Dispatcher()
|
29 |
+
dp.include_router(user_router)
|
30 |
+
dp.include_router(admin_router)
|
31 |
+
logger.info("Бот запускается...")
|
32 |
+
await dp.start_polling(bot)
|
33 |
+
except Exception as e:
|
34 |
+
logger.error(f"Error in main function: {e}")
|
35 |
+
await shutdown(bot)
|
36 |
+
raise
|
37 |
+
|
38 |
+
|
39 |
+
if __name__ == '__main__':
|
40 |
+
try:
|
41 |
+
asyncio.run(main())
|
42 |
+
except (KeyboardInterrupt, RuntimeError) as e:
|
43 |
+
logger.warning(f"Бот выключен: {type(e).__name__}")
|
44 |
+
except Exception as e:
|
45 |
+
logger.error(f"Внезапная ошибка: {e}")
|
requirements.txt
ADDED
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
aiofiles==24.1.0
|
2 |
+
aiogram==3.17.0
|
3 |
+
aiogram_broadcaster==0.6.4
|
4 |
+
aiohappyeyeballs==2.4.4
|
5 |
+
aiohttp==3.11.11
|
6 |
+
aiosignal==1.3.2
|
7 |
+
aiosqlite==0.20.0
|
8 |
+
annotated-types==0.7.0
|
9 |
+
asyncio==3.4.3
|
10 |
+
attrs==24.3.0
|
11 |
+
certifi==2024.12.14
|
12 |
+
frozenlist==1.5.0
|
13 |
+
greenlet==3.1.1
|
14 |
+
idna==3.10
|
15 |
+
magic-filter==1.0.12
|
16 |
+
multidict==6.1.0
|
17 |
+
propcache==0.2.1
|
18 |
+
pydantic==2.10.5
|
19 |
+
pydantic_core==2.27.2
|
20 |
+
python-dotenv==1.0.1
|
21 |
+
setuptools==75.1.0
|
22 |
+
SQLAlchemy==2.0.37
|
23 |
+
typing_extensions==4.12.2
|
24 |
+
wheel==0.44.0
|
25 |
+
yarl==1.18.3
|