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
|