Compare commits

..

No commits in common. "c72df3fd00cfd14ed6afcaa9c470632b8f2913a7" and "45bba687cd1a3a1c951d24f368156580462474dc" have entirely different histories.

61 changed files with 783 additions and 73148 deletions

View File

@ -4,8 +4,4 @@
/.env
/.gitignore
/.git
/logs/
/telezab.db
/db/
/db/telezab.db
/logs/

8
.gitignore vendored
View File

@ -4,10 +4,4 @@
/TODO.txt
/logs
/__pycache__
/venv
/app.log
/logs/
/logs/app.log
/logs/error.log
/db/
/db/telezab.db
/venv

View File

@ -1,17 +1,12 @@
FROM python:3.12.3-slim
FROM python:3.11.9-slim
LABEL authors="UdoChudo"
# Установим необходимые пакеты
RUN apt-get update && apt-get install -y \
build-essential \
libpq-dev \
gcc \
tzdata \
sqlite3 \
curl \
telnet \
&& rm -rf /var/lib/apt/lists/*
# Установим рабочую директорию
WORKDIR /app
@ -23,9 +18,6 @@ RUN pip install --no-cache-dir -r requirements.txt
# Откроем порт для нашего приложения
EXPOSE 5000
ENV TZ=Europe/Moscow
ENV FLASK_APP telezab.py
ENV PYTHONUNBUFFERED 1
ENV FLASK_APP=telezab.py
# Запуск Gunicorn
CMD ["python3", "telezab.py"]

View File

@ -1,201 +0,0 @@
import logging
from logging.config import dictConfig
from logging.handlers import TimedRotatingFileHandler
import os
import zipfile
from datetime import datetime, timedelta
class UTF8StreamHandler(logging.StreamHandler):
def __init__(self, stream=None):
super().__init__(stream)
self.setStream(stream)
def setStream(self, stream):
super().setStream(stream)
if hasattr(stream, 'reconfigure'):
stream.reconfigure(encoding='utf-8')
class FilterByMessage(logging.Filter):
def filter(self, record):
# Фильтруем сообщения, содержащие 'Received 1 new updates'
return 'Received ' not in record.getMessage()
class LogManager:
def __init__(self, log_dir='logs', retention_days=30):
self.log_dir = log_dir
self.retention_days = retention_days
self.log_files = {
'flask': os.path.join(self.log_dir, 'flask.log'),
'flask_error': os.path.join(self.log_dir, 'flask_error.log'),
'app': os.path.join(self.log_dir, 'app.log'),
'app_error': os.path.join(self.log_dir, 'app_error.log'),
'debug': os.path.join(self.log_dir, 'debug.log'),
}
# Ensure the log directory exists
if not os.path.exists(self.log_dir):
os.makedirs(self.log_dir)
# Setup logging configuration
self.setup_logging()
def setup_logging(self):
dictConfig({
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'default': {
'format': '[%(asctime)s] %(levelname)s %(module)s: %(message)s',
},
'error': {
'format': '[%(asctime)s] %(levelname)s %(module)s: %(message)s',
},
'werkzeug': {
'format': '[%(asctime)s] %(levelname)s %(message)s'
},
'debug': {
'format': '[%(asctime)s] %(levelname)s %(module)s [%(funcName)s:%(lineno)d]: %(message)s'
}
},
'handlers': {
'telebot_console': {
'class': 'log_manager.UTF8StreamHandler',
'stream': 'ext://sys.stdout',
'formatter': 'default',
},
'flask_console': {
'class': 'log_manager.UTF8StreamHandler',
'stream': 'ext://sys.stdout',
'formatter': 'werkzeug',
},
'flask_file': {
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': self.log_files['flask'],
'when': 'midnight',
'backupCount': self.retention_days,
'formatter': 'werkzeug',
'encoding': 'utf-8',
},
'flask_error_file': {
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': self.log_files['flask_error'],
'when': 'midnight',
'backupCount': self.retention_days,
'formatter': 'werkzeug',
'encoding': 'utf-8',
'level': 'ERROR',
},
'app_file': {
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': self.log_files['app'],
'when': 'midnight',
'backupCount': self.retention_days,
'formatter': 'default',
'encoding': 'utf-8',
},
'app_error_file': {
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': self.log_files['app_error'],
'when': 'midnight',
'backupCount': self.retention_days,
'formatter': 'error',
'encoding': 'utf-8',
'level': 'ERROR',
},
'debug_file': {
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': self.log_files['debug'],
'when': 'midnight',
'backupCount': self.retention_days,
'formatter': 'debug',
'encoding': 'utf-8',
'level': 'DEBUG',
},
},
'loggers': {
'flask': {
'level': 'INFO',
'handlers': ['flask_file', 'flask_error_file', 'flask_console'],
'propagate': False,
},
'telebot': {
'level': 'INFO',
'handlers': ['app_file', 'app_error_file', 'telebot_console'],
'propagate': False,
},
'werkzeug': {
'level': 'INFO',
'handlers': ['flask_file', 'flask_error_file', 'flask_console'],
'propagate': False,
},
'debug': {
'level': 'DEBUG',
'handlers': ['debug_file'],
'propagate': False,
},
}
# 'root': {
# 'level': 'DEBUG',
# 'handlers': ['flask_console', 'telebot_console'],
# }
})
def archive_old_logs(self):
"""Archives old log files and removes logs older than retention_days."""
# Get yesterday's date
yesterday_date = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d')
for log_name, log_file in self.log_files.items():
if os.path.exists(log_file):
archive_name = f"{log_name}_{yesterday_date}.zip"
archive_path = os.path.join(self.log_dir, archive_name)
# Archive the log file
with zipfile.ZipFile(archive_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
zipf.write(log_file, arcname=os.path.basename(log_file))
# Remove the old log file after archiving
os.remove(log_file)
# Clean up old archives
self.cleanup_old_archives()
def configure_werkzeug_logging(self):
"""Отключаем встроенный логгер Werkzeug и задаём собственные настройки логирования."""
werkzeug_logger = logging.getLogger('werkzeug')
werkzeug_logger.handlers = [] # Удаляем существующие обработчики
# Добавляем кастомный обработчик для форматирования логов
handler = TimedRotatingFileHandler(self.log_files['flask'], when='midnight', backupCount=self.retention_days, encoding='utf-8')
handler.setFormatter(logging.Formatter('[%(asctime)s] %(levelname)s %(message)s'))
werkzeug_logger.addHandler(handler)
# Отключаем дублирование логов
werkzeug_logger.propagate = False
def cleanup_old_archives(self):
"""Deletes archived logs older than retention_days."""
now = datetime.now()
cutoff = now - timedelta(days=self.retention_days)
for file in os.listdir(self.log_dir):
if file.endswith('.zip'):
file_path = os.path.join(self.log_dir, file)
file_time = datetime.fromtimestamp(os.path.getmtime(file_path))
if file_time < cutoff:
os.remove(file_path)
def schedule_log_rotation(self):
"""Schedules daily log rotation and archiving."""
from threading import Timer
now = datetime.now()
next_midnight = now.replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1)
delay = (next_midnight - now).total_seconds()
Timer(delay, self.rotate_and_archive_logs).start()
def rotate_and_archive_logs(self):
"""Rotates and archives logs."""
self.archive_old_logs()
self.schedule_log_rotation() # Schedule the next rotation

View File

@ -1,107 +0,0 @@
import sqlite3
import logging
from threading import Lock
db_lock = Lock()
# Инициализируем логгер
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG) # Устанавливаем уровень логирования
class RegionAPI:
def __init__(self, db_path):
self.db_path = db_path
def add_region(self, region_id: int, region_name: str):
logger.info(f"Запрос на добавление региона: id={region_id}, name={region_name}")
# Проверка валидности region_id
if not str(region_id).isdigit():
logger.error(f"region_id {region_id} не является числом.")
return {"status": "failure", "message": "Region_id must be digit only"}
with db_lock, sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
logger.debug(f"Проверка существования региона с id={region_id}")
cursor.execute('SELECT COUNT(*) FROM regions WHERE region_id = ?', (region_id,))
count = cursor.fetchone()[0]
if count == 0:
# Добавляем новый регион
cursor.execute('INSERT INTO regions (region_id, region_name, active) VALUES (?, ?, 1)',
(region_id, region_name))
conn.commit()
logger.info(f"Регион с id={region_id} успешно добавлен.")
return {"status": "success", "message": "Region added successfully"}
else:
logger.warning(f"Регион с id={region_id} уже существует.")
return {"status": "error", "message": "Region already exists"}
def remove_region(self, region_id):
logger.info(f"Запрос на удаление региона: id={region_id}")
with db_lock, sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
logger.debug(f"Проверка существования региона с id={region_id}")
cursor.execute('SELECT COUNT(*) FROM regions WHERE region_id = ?', (region_id,))
count = cursor.fetchone()[0]
if count == 0:
logger.warning(f"Регион с id={region_id} не найден.")
return {"status": "error", "message": "Region not found"}
else:
cursor.execute('DELETE FROM regions WHERE region_id = ?', (region_id,))
conn.commit()
logger.info(f"Регион с id={region_id} успешно удалён.")
return {"status": "success", "message": "Region removed successfully"}
def get_regions(self):
logger.info("Запрос на получение списка регионов.")
with db_lock, sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
logger.debug("Извлечение данных из таблицы regions.")
cursor.execute('SELECT region_id, region_name, active FROM regions')
regions = cursor.fetchall()
logger.info(f"Получено {len(regions)} регионов.")
return [{"region_id": r[0], "region_name": r[1], "regions_active": r[2]} for r in regions]
def change_region_status(self, region_id, active):
logger.info(f"Запрос на изменение статуса региона: id={region_id}, статус={active}")
with db_lock, sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
logger.debug(f"Проверка существования региона с id={region_id}")
cursor.execute('SELECT COUNT(*) FROM regions WHERE region_id = ?', (region_id,))
count = cursor.fetchone()[0]
if count == 0:
logger.warning(f"Регион с id={region_id} не найден.")
return {"status": "error", "message": "Region not found"}
else:
cursor.execute('UPDATE regions SET active = ? WHERE region_id = ?', (active, region_id))
conn.commit()
logger.info(f"Статус региона с id={region_id} успешно изменён.")
return {"status": "success", "message": "Region status updated successfully"}
def update_region_status(self, region_id, active):
logger.info(f"Запрос на обновление статуса региона: id={region_id}, активность={active}")
with db_lock, sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
# Проверяем существование региона
logger.debug(f"Проверка существования региона с id={region_id}")
cursor.execute("SELECT region_name FROM regions WHERE region_id = ?", (region_id,))
result = cursor.fetchone()
if not result:
logger.warning(f"Регион с id={region_id} не найден.")
return {"status": "error", "message": "Регион не найден"}
# Обновляем статус активности региона
cursor.execute("UPDATE regions SET active = ? WHERE region_id = ?", (int(active), region_id))
conn.commit()
action = "Активирован" if active else "Отключён"
logger.info(f"Регион с id={region_id} {action}.")
return {"status": "success", "message": f"Регион {region_id} {action}"}

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,485 +0,0 @@
/*!
* Bootstrap Reboot v5.1.3 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/
:root {
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--bs-white: #fff;
--bs-gray: #6c757d;
--bs-gray-dark: #343a40;
--bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d;
--bs-gray-700: #495057;
--bs-gray-800: #343a40;
--bs-gray-900: #212529;
--bs-primary: #0d6efd;
--bs-secondary: #6c757d;
--bs-success: #198754;
--bs-info: #0dcaf0;
--bs-warning: #ffc107;
--bs-danger: #dc3545;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 13, 110, 253;
--bs-secondary-rgb: 108, 117, 125;
--bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-body-color-rgb: 33, 37, 41;
--bs-body-bg-rgb: 255, 255, 255;
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #212529;
--bs-body-bg: #fff;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
background-color: currentColor;
border: 0;
opacity: 0.25;
}
hr:not([size]) {
height: 1px;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title],
abbr[data-bs-original-title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-left: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.2em;
background-color: #fcf8e3;
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: #0d6efd;
text-decoration: underline;
}
a:hover {
color: #0a58ca;
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: var(--bs-font-monospace);
font-size: 1em;
direction: ltr /* rtl:ignore */;
unicode-bidi: bidi-override;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: #d63384;
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.2rem 0.4rem;
font-size: 0.875em;
color: #fff;
background-color: #212529;
border-radius: 0.2rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
font-weight: 700;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: #6c757d;
text-align: left;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]::-webkit-calendar-picker-indicator {
display: none;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: left;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: left;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
outline-offset: -2px;
-webkit-appearance: textfield;
}
/* rtl:raw:
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-webkit-file-upload-button {
font: inherit;
}
::file-selector-button {
font: inherit;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.css.map */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,482 +0,0 @@
/*!
* Bootstrap Reboot v5.1.3 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/
:root {
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--bs-white: #fff;
--bs-gray: #6c757d;
--bs-gray-dark: #343a40;
--bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d;
--bs-gray-700: #495057;
--bs-gray-800: #343a40;
--bs-gray-900: #212529;
--bs-primary: #0d6efd;
--bs-secondary: #6c757d;
--bs-success: #198754;
--bs-info: #0dcaf0;
--bs-warning: #ffc107;
--bs-danger: #dc3545;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 13, 110, 253;
--bs-secondary-rgb: 108, 117, 125;
--bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-body-color-rgb: 33, 37, 41;
--bs-body-bg-rgb: 255, 255, 255;
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #212529;
--bs-body-bg: #fff;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
background-color: currentColor;
border: 0;
opacity: 0.25;
}
hr:not([size]) {
height: 1px;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title],
abbr[data-bs-original-title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-right: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-right: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.2em;
background-color: #fcf8e3;
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: #0d6efd;
text-decoration: underline;
}
a:hover {
color: #0a58ca;
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: var(--bs-font-monospace);
font-size: 1em;
direction: ltr ;
unicode-bidi: bidi-override;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: #d63384;
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.2rem 0.4rem;
font-size: 0.875em;
color: #fff;
background-color: #212529;
border-radius: 0.2rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
font-weight: 700;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: #6c757d;
text-align: right;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]::-webkit-calendar-picker-indicator {
display: none;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: right;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: right;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
outline-offset: -2px;
-webkit-appearance: textfield;
}
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-webkit-file-upload-button {
font: inherit;
}
::file-selector-button {
font: inherit;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

11266
static/css/bootstrap.css vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,23 +0,0 @@
/* Увеличение ширины колонки с регионами */
#region-list {
max-height: 600px; /* Устанавливаем максимальную высоту для вертикальной прокрутки */
overflow-y: auto;
}
#user-list {
max-height: 400px; /* Устанавливаем максимальную высоту для вертикальной прокрутки */
overflow-y: auto;
}
/* Стилизация заголовков и кнопок */
h3 {
margin-bottom: 20px;
}
#user-info {
margin-bottom: 20px;
}
.btn {
margin: 0 5px; /* Добавляем немного пространства между кнопками */
}

View File

@ -1,102 +0,0 @@
/* Обнуляем отступы для более гибкого макета */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* Основные контейнеры */
html, body {
height: 100%;
width: 100%;
}
body {
font-family: Arial, sans-serif;
display: flex;
flex-direction: column;
}
/* Основной контейнер, занимает весь экран */
.container {
display: flex;
height: 100%;
width: 100%;
padding: 20px;
box-sizing: border-box;
}
/* Контейнер для списка пользователей */
.user-list-container {
width: 30%; /* Список пользователей занимает 30% ширины */
display: flex;
flex-direction: column;
margin-right: 20px;
}
/* Прокручивающийся список пользователей */
.user-list {
flex: 1;
max-height: 100%;
overflow-y: auto;
border: 1px solid #ddd;
padding: 10px;
margin-bottom: 10px;
}
.user-item {
margin: 10px 0;
padding: 10px;
border: 1px solid #ddd;
cursor: pointer;
}
.user-item.selected {
background-color: #f0f0f0;
}
/* Блок управления */
.controls {
display: flex;
flex-direction: column;
width: 70%; /* Блок с информацией и кнопкой занимает 70% ширины */
}
#show-events {
margin-bottom: 20px;
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
}
#user-info {
margin-top: 10px;
}
#events {
margin-top: 10px;
max-height: 300px;
overflow-y: auto;
border: 1px solid #ddd;
padding: 10px;
}
#events ul {
list-style: none;
padding: 0;
}
#events ul li {
margin-bottom: 5px;
}
/* Контейнер для строки поиска */
.search-container {
margin-bottom: 10px;
}
.search-container input {
width: 100%;
padding: 8px;
box-sizing: border-box;
}

View File

@ -1,25 +0,0 @@
/* Добавим выравнивание и отступы для кнопок управления */
.table-hover tbody tr td {
vertical-align: middle;
}
.table-hover tbody tr td .btn-manage {
margin-left: 20px; /* Отступ кнопки от названия региона */
}
th, td {
text-align: left; /* Выровнять содержимое слева */
}
.table-hover tbody tr {
height: 50px; /* Сделать строки таблицы выше для лучшего визуального эффекта */
}
.modal-footer .btn {
margin-left: 10px;
}
/* Стили для пагинации */
.d-flex .btn {
margin: 0 5px;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

5046
static/js/bootstrap.js vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

10881
static/js/jquery-3.6.0.js vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -1,133 +0,0 @@
$(document).ready(function() {
const regionList = $('#region-list');
const userInfo = $('#user-info');
const userList = $('#user-list');
const curentRegion = $('#curent-region')
const pagination = $('#pagination');
let regions = [];
let users = [];
let selectedRegionName = null;
let selectedRegionId = null;
const regionsPerPage = 10;
let currentPage = 1;
// Загрузка регионов
function loadRegions() {
$.getJSON('/telezab/regions/get', function(data) {
regions = data;
regions.sort((a, b) => a.region_id - b.region_id); // Сортировка по региону
renderRegions();
renderPagination();
}).fail(function() {
console.error("Не удалось получить регионы.");
});
}
// Загрузка пользователей
function loadUsers() {
$.getJSON('/telezab/users/get', function(data) {
users = data;
}).fail(function() {
console.error("Не удалось получить пользователей.");
});
}
// Отрисовка списка регионов
function renderRegions() {
regionList.empty();
const start = (currentPage - 1) * regionsPerPage;
const end = start + regionsPerPage;
const paginatedRegions = regions.slice(start, end);
paginatedRegions.forEach(region => {
const listItem = $('<a>', {
href: '#',
class: 'list-group-item list-group-item-action',
text: `${region.region_name} (${region.region_id})`,
'data-region-id': region.region_id,
'data-region-name': region.region_name
}).on('click', function() {
selectedRegionId = $(this).data('region-id');
selectedRegionName = $(this).data('region-name');
showRegionDetails();
});
regionList.append(listItem);
});
}
// Отрисовка кнопок пагинации
function renderPagination() {
pagination.empty();
const totalPages = Math.ceil(regions.length / regionsPerPage);
for (let i = 1; i <= totalPages; i++) {
const pageItem = $('<li>', {
class: `page-item ${i === currentPage ? 'active' : ''}`
}).append($('<a>', {
class: 'page-link',
href: '#',
text: i
}).on('click', function(e) {
e.preventDefault();
currentPage = i;
renderRegions();
renderPagination();
}));
pagination.append(pageItem);
}
}
// Показать детали региона
function showRegionDetails() {
userInfo.empty();
userList.empty();
curentRegion.empty(); // Очистить текущий элемент
// Найти текущий регион по selectedRegionId
const selectedRegion = regions.find(region => region.region_name === selectedRegionName);
// Проверить, что регион найден
if (selectedRegion) {
// Создать и добавить заголовок h3 с именем региона
var newHeading = $('<h3>').text(selectedRegion.region_name);
$('#curent-region').append(newHeading);
} else {
$('#curent-region').text('Регион не найден.');
}
// Фильтровать пользователей, подписанных на этот регион
const subscribedUsers = users.filter(user =>
user.subscriptions.split(', ').includes(selectedRegionId.toString())
);
// Показать список пользователей или сообщение, если подписчиков нет
if (subscribedUsers.length > 0) {
subscribedUsers.forEach(user => {
userList.append(`<li>${user.email}</li>`);
});
} else {
userInfo.text('Нет подписчиков на этот регион.');
}
}
// Инициализация
loadRegions();
loadUsers();
// Обработчики кнопок в модальном окне
$('#delete-region').on('click', function() {
// Функция удаления региона
console.log('Удалить регион', selectedRegionId);
});
$('#disable-region').on('click', function() {
// Функция отключения региона
console.log('Отключить регион', selectedRegionId);
});
$('#activate-region').on('click', function() {
// Функция активации региона
console.log('Активировать регион', selectedRegionId);
});
});

View File

@ -1,55 +0,0 @@
$(document).ready(function() {
// Получаем список сотрудников
$.getJSON('/telezab/users/get', function(data) {
var userList = $('#user-list');
userList.empty();
data.forEach(function(user) {
var email = user.email;
var name = email.split('@')[0].replace(/\./g, ' ').replace(/\b\w/g, char => char.toUpperCase());
var listItem = $('<li class="list-group-item"></li>').text(name).data('user', user);
userList.append(listItem);
});
});
// Обработчик кликов по пользователям
$('#user-list').on('click', '.list-group-item', function() {
// Удаляем активный класс у всех элементов списка
$('.list-group-item').removeClass('active');
// Добавляем активный класс к выбранному элементу
$(this).addClass('active');
var user = $(this).data('user');
$('#user-info').removeClass('d-none');
$('#user-name').text(user.email.split('@')[0].replace(/\./g, ' ').replace(/\b\w/g, char => char.toUpperCase()));
// Отображаем регионы в одну строку
var regions = $('#user-regions');
regions.empty();
if (user.subscriptions) {
var subscriptions = user.subscriptions.split(',').map(function(sub) { return sub.trim(); });
if (subscriptions.length > 0) {
regions.text(subscriptions.join(', '));
} else {
regions.text('Нет подписок');
}
} else {
regions.text('Нет подписок');
}
// Отображаем действия
var events = $('#user-events');
events.empty();
if (user.events && user.events.length > 0) {
user.events.forEach(function(event) {
var eventText = event.type;
if (event.region) {
eventText += ' (Регион: ' + event.region + ')';
}
events.append('<div><strong>' + event.date + '</strong> - ' + eventText + '</div>');
});
} else {
events.append('<div>Нет действий</div>');
}
});
});

2013
telezab.py

File diff suppressed because it is too large Load Diff

View File

@ -1,53 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Region Management</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/regions.css') }}">
</head>
<body>
<div class="container mt-4">
<div class="row">
<!-- Левый контейнер со списком регионов -->
<div class="col-md-6">
<h3>Список Регионов</h3>
<div class="list-group" id="region-list">
<!-- Список регионов будет добавляться сюда -->
</div>
<!-- Пагинация -->
<nav aria-label="Page navigation">
<ul class="pagination" id="pagination">
<!-- Кнопки пагинации будут добавляться сюда -->
</ul>
</nav>
</div>
<!-- Правый контейнер с информацией о пользователях -->
<div class="col-md-6">
<h3>Пользователи</h3>
<div id="curent-region">
</div>
<div id="user-info">
<!-- Информация о пользователях будет добавляться сюда -->
</div>
<ul id="user-list">
<!-- Список пользователей будет добавляться сюда -->
</ul>
<!-- Кнопки управления регионами -->
<div class="d-flex justify-content-end mt-3">
<button type="button" class="btn btn-danger me-2" id="delete-region">Удалить регион</button>
<button type="button" class="btn btn-warning me-2" id="disable-region">Отключить регион</button>
<button type="button" class="btn btn-success" id="activate-region">Активировать регион</button>
</div>
</div>
</div>
</div>
<script src="{{ url_for('static', filename='js/jquery-3.6.0.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/bootstrap.bundle.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/regions.js') }}"></script>
</body>
</html>

View File

@ -1,37 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Сотрудники</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/users.css') }}">
<script src="{{ url_for('static', filename='js/jquery-3.6.0.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/bootstrap.bundle.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/users.js') }}"></script>
</head>
<body>
<div class="container mt-4">
<div class="row">
<div class="col-md-4">
<ul id="user-list" class="list-group">
<!-- Список сотрудников будет вставлен сюда -->
</ul>
</div>
<div class="col-md-8">
<div id="user-info" class="d-none">
<h3 id="user-name"></h3>
<h5>Подписки на регионы</h5>
<ul id="user-regions" class="list-group">
<!-- Подписки на регионы будут вставлены сюда -->
</ul>
<h5>Действия</h5>
<div id="user-events" class="overflow-auto" style="max-height: 300px;">
<!-- Действия будут вставлены сюда -->
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -1,16 +0,0 @@
class UserStateManager:
def __init__(self):
# Словарь для хранения состояния каждого пользователя
self.user_states = {}
def set_state(self, chat_id, state):
"""Устанавливает состояние для пользователя."""
self.user_states[chat_id] = state
def get_state(self, chat_id):
"""Получает текущее состояние пользователя."""
return self.user_states.get(chat_id, "MAIN_MENU")
def reset_state(self, chat_id):
"""Сбрасывает состояние пользователя в главное меню."""
self.user_states[chat_id] = "MAIN_MENU"