From 0be01476faee387e620f6bc4e678dcdd0744ae73 Mon Sep 17 00:00:00 2001 From: lizerk Date: Tue, 10 Mar 2026 13:42:45 +0300 Subject: [PATCH] =?UTF-8?q?=D0=BE=D1=87=D0=B5=D0=BD=D1=8C=20=D0=BC=D0=BD?= =?UTF-8?q?=D0=BE=D0=B3=D0=BE=20=D0=BB=D0=B8=D1=88=D0=BD=D0=B5=D0=B3=D0=BE?= =?UTF-8?q?=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B8=D0=BB=20=D0=B2=20=D1=87=D0=B0?= =?UTF-8?q?=D1=81=D1=82=D0=BD=D0=BE=D1=81=D1=82=D0=B8=20=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D0=BE=D0=B2=20=D0=B8=20=D1=85=D0=B8=D0=BA=D1=86=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D1=80=D0=B0=D0=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/bots/__init__.py | 0 backend/bots/admin.py | 3 - backend/bots/apps.py | 6 - backend/bots/filters.py | 40 -- backend/bots/management/__init__.py | 0 backend/bots/management/commands/__init__.py | 0 .../management/commands/start_telegram_bot.py | 9 - backend/bots/migrations/0001_initial.py | 34 -- ...remove_botsubscriber_is_active_and_more.py | 43 -- ...ve_botsubscriber_subscribed_at_and_more.py | 21 - .../0004_botsubscriber_wechat_id.py | 18 - backend/bots/migrations/__init__.py | 0 backend/bots/models.py | 38 -- backend/bots/services/telegram.py | 441 ------------------ backend/bots/tests.py | 3 - backend/bots/utils.py | 15 - backend/bots/views.py | 3 - backend/employees/signals.py | 87 ---- backend/eusrr_backend/settings.py | 11 - backend/hikcentral/__init__.py | 1 - backend/hikcentral/admin.py | 3 - backend/hikcentral/apps.py | 10 - backend/hikcentral/migrations/0001_initial.py | 65 --- backend/hikcentral/migrations/__init__.py | 0 backend/hikcentral/models.py | 68 --- backend/hikcentral/signals.py | 77 --- backend/hikcentral/tests.py | 3 - backend/hikcentral/utils.py | 50 -- backend/hikcentral/views.py | 3 - 29 files changed, 1052 deletions(-) delete mode 100644 backend/bots/__init__.py delete mode 100644 backend/bots/admin.py delete mode 100644 backend/bots/apps.py delete mode 100644 backend/bots/filters.py delete mode 100644 backend/bots/management/__init__.py delete mode 100644 backend/bots/management/commands/__init__.py delete mode 100644 backend/bots/management/commands/start_telegram_bot.py delete mode 100644 backend/bots/migrations/0001_initial.py delete mode 100644 backend/bots/migrations/0002_remove_botsubscriber_is_active_and_more.py delete mode 100644 backend/bots/migrations/0003_remove_botsubscriber_subscribed_at_and_more.py delete mode 100644 backend/bots/migrations/0004_botsubscriber_wechat_id.py delete mode 100644 backend/bots/migrations/__init__.py delete mode 100644 backend/bots/models.py delete mode 100644 backend/bots/services/telegram.py delete mode 100644 backend/bots/tests.py delete mode 100644 backend/bots/utils.py delete mode 100644 backend/bots/views.py delete mode 100644 backend/hikcentral/__init__.py delete mode 100644 backend/hikcentral/admin.py delete mode 100644 backend/hikcentral/apps.py delete mode 100644 backend/hikcentral/migrations/0001_initial.py delete mode 100644 backend/hikcentral/migrations/__init__.py delete mode 100644 backend/hikcentral/models.py delete mode 100644 backend/hikcentral/signals.py delete mode 100644 backend/hikcentral/tests.py delete mode 100644 backend/hikcentral/utils.py delete mode 100644 backend/hikcentral/views.py diff --git a/backend/bots/__init__.py b/backend/bots/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/backend/bots/admin.py b/backend/bots/admin.py deleted file mode 100644 index 8c38f3f3..00000000 --- a/backend/bots/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/backend/bots/apps.py b/backend/bots/apps.py deleted file mode 100644 index 733cbca5..00000000 --- a/backend/bots/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class BotsConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'bots' diff --git a/backend/bots/filters.py b/backend/bots/filters.py deleted file mode 100644 index b22ef213..00000000 --- a/backend/bots/filters.py +++ /dev/null @@ -1,40 +0,0 @@ -# bots/filters.py -from aiogram import types, F -from aiogram.filters import BaseFilter -from asgiref.sync import sync_to_async -from bots.models import BotSubscriber - - -class IsBoundSubscriber_tg(BaseFilter): - """ - Пропускает только тех, чей telegram_id уже привязан. - """ - - async def __call__(self, event: types.Message | types.CallbackQuery) -> bool: - user_id = event.from_user.id - return await sync_to_async( - BotSubscriber.objects.filter(telegram_id=user_id).exists - )() - - -class IsEmployeeActive(BaseFilter): - """ - Пропускает только работающих сотрудников - (последний кадровый статус — активный). - """ - - async def __call__(self, event: types.Message | types.CallbackQuery) -> bool: - user_id = event.from_user.id - - # Получаем подписчика вместе с юзером - subscriber = await sync_to_async( - lambda: BotSubscriber.objects.select_related('user') - .filter(telegram_id=user_id) - .first() - )() - - if not subscriber or not subscriber.user: - return False - - # Проверяем свойство is_actually_active у Employee - return await sync_to_async(lambda u: u.is_actually_active)(subscriber.user) diff --git a/backend/bots/management/__init__.py b/backend/bots/management/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/backend/bots/management/commands/__init__.py b/backend/bots/management/commands/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/backend/bots/management/commands/start_telegram_bot.py b/backend/bots/management/commands/start_telegram_bot.py deleted file mode 100644 index 1fdd9a38..00000000 --- a/backend/bots/management/commands/start_telegram_bot.py +++ /dev/null @@ -1,9 +0,0 @@ -from django.core.management.base import BaseCommand -from bots.services.telegram import run_telegram_bot - - -class Command(BaseCommand): - help = "Запуск Telegram-бота для рассылки и привязки сотрудников" - - def handle(self, *args, **options): - run_telegram_bot() diff --git a/backend/bots/migrations/0001_initial.py b/backend/bots/migrations/0001_initial.py deleted file mode 100644 index 4e16fdc9..00000000 --- a/backend/bots/migrations/0001_initial.py +++ /dev/null @@ -1,34 +0,0 @@ -# Generated by Django 5.2.4 on 2025-07-25 11:08 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='BotSubscriber', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('telegram_id', models.BigIntegerField(blank=True, null=True, unique=True, verbose_name='Telegram ID')), - ('whatsapp_id', models.CharField(blank=True, max_length=64, null=True, unique=True, verbose_name='WhatsApp ID')), - ('is_active', models.BooleanField(default=True)), - ('subscribed_at', models.DateTimeField(auto_now_add=True)), - ('registration_token', models.CharField(blank=True, max_length=64, null=True, unique=True, verbose_name='Токен для привязки')), - ('token_created', models.DateTimeField(blank=True, null=True, verbose_name='Дата генерации токена')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bot_subscriptions', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name': 'Подписчик бота', - 'verbose_name_plural': 'Подписчики ботов', - }, - ), - ] diff --git a/backend/bots/migrations/0002_remove_botsubscriber_is_active_and_more.py b/backend/bots/migrations/0002_remove_botsubscriber_is_active_and_more.py deleted file mode 100644 index d449b398..00000000 --- a/backend/bots/migrations/0002_remove_botsubscriber_is_active_and_more.py +++ /dev/null @@ -1,43 +0,0 @@ -# Generated by Django 5.2.4 on 2025-07-28 06:37 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('bots', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.RemoveField( - model_name='botsubscriber', - name='is_active', - ), - migrations.RemoveField( - model_name='botsubscriber', - name='registration_token', - ), - migrations.RemoveField( - model_name='botsubscriber', - name='token_created', - ), - migrations.AddField( - model_name='botsubscriber', - name='unsubscribed_at', - field=models.DateTimeField(blank=True, null=True, verbose_name='Дата отвязки'), - ), - migrations.AlterField( - model_name='botsubscriber', - name='subscribed_at', - field=models.DateTimeField(auto_now_add=True, verbose_name='Дата привязки'), - ), - migrations.AlterField( - model_name='botsubscriber', - name='user', - field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='bot_subscription', to=settings.AUTH_USER_MODEL), - ), - ] diff --git a/backend/bots/migrations/0003_remove_botsubscriber_subscribed_at_and_more.py b/backend/bots/migrations/0003_remove_botsubscriber_subscribed_at_and_more.py deleted file mode 100644 index 2b991ffc..00000000 --- a/backend/bots/migrations/0003_remove_botsubscriber_subscribed_at_and_more.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 5.2.4 on 2025-07-28 08:39 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('bots', '0002_remove_botsubscriber_is_active_and_more'), - ] - - operations = [ - migrations.RemoveField( - model_name='botsubscriber', - name='subscribed_at', - ), - migrations.RemoveField( - model_name='botsubscriber', - name='unsubscribed_at', - ), - ] diff --git a/backend/bots/migrations/0004_botsubscriber_wechat_id.py b/backend/bots/migrations/0004_botsubscriber_wechat_id.py deleted file mode 100644 index 23fa2776..00000000 --- a/backend/bots/migrations/0004_botsubscriber_wechat_id.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.4 on 2025-08-04 13:00 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('bots', '0003_remove_botsubscriber_subscribed_at_and_more'), - ] - - operations = [ - migrations.AddField( - model_name='botsubscriber', - name='wechat_id', - field=models.CharField(blank=True, max_length=64, null=True, unique=True, verbose_name='WeChat ID'), - ), - ] diff --git a/backend/bots/migrations/__init__.py b/backend/bots/migrations/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/backend/bots/models.py b/backend/bots/models.py deleted file mode 100644 index 383a11a3..00000000 --- a/backend/bots/models.py +++ /dev/null @@ -1,38 +0,0 @@ -# bots/models.py -import uuid -from django.db import models -from django.contrib.auth import get_user_model - -User = get_user_model() - - -class BotSubscriber(models.Model): - user = models.OneToOneField( - User, - on_delete=models.CASCADE, - related_name='bot_subscription' - ) - telegram_id = models.BigIntegerField( - 'Telegram ID', - null=True, blank=True, - unique=True - ) - whatsapp_id = models.CharField( - 'WhatsApp ID', - max_length=64, - null=True, blank=True, - unique=True - ) - wechat_id = models.CharField( - 'WeChat ID', - max_length=64, - null=True, blank=True, - unique=True - ) - - class Meta: - verbose_name = 'Подписчик бота' - verbose_name_plural = 'Подписчики ботов' - - def __str__(self): - return f'{self.user} (TG={self.telegram_id or "-"} / WA={self.whatsapp_id or "-"} / WC={self.wechat_id or "-"})' diff --git a/backend/bots/services/telegram.py b/backend/bots/services/telegram.py deleted file mode 100644 index 11ecf356..00000000 --- a/backend/bots/services/telegram.py +++ /dev/null @@ -1,441 +0,0 @@ -import os -import logging -from datetime import timedelta - -from dotenv import load_dotenv - -import django -from django.conf import settings -from django.db import models -from django.utils import timezone - -from aiogram import Bot, Dispatcher, types, F -from aiogram.filters import CommandStart -from aiogram.types import ( - InlineKeyboardMarkup, InlineKeyboardButton, - ReplyKeyboardMarkup, KeyboardButton, - FSInputFile, -) -from aiogram.exceptions import TelegramBadRequest -from asgiref.sync import sync_to_async -from apscheduler.schedulers.asyncio import AsyncIOScheduler - -from django.contrib.auth import get_user_model - -from calendar_app.models import CalendarEvent -from bots.models import BotSubscriber -from bots.filters import IsBoundSubscriber_tg, IsEmployeeActive -from documents.models import Document, DocumentAcknowledgement - -# Если запускается вне manage.py, настраиваем Django -if not settings.configured: - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eusrr_backend.settings') - django.setup() - -# Логирование -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - -# Загрузка .env -load_dotenv() -BOT_TOKEN = os.getenv('TELEGRAM_BOT_TOKEN') - -# Инициализация бота и диспетчера -bot = Bot(token=BOT_TOKEN) -dp = Dispatcher() - -# Модель сотрудника -Employee = get_user_model() - -# Отделы для inline‑меню -DEPARTMENTS = { - "department_manager": { - "title": "Менеджеры", - "groups": [ - {"name": "Группа 1", "link": "https://t.me/manager_group"}, - {"name": "Рабочий чат", "link": "https://t.me/manager_workchat"}, - ], - }, - "department_tech": { - "title": "Технари", - "groups": [ - {"name": "Группа 1", "link": "https://t.me/tech_group"}, - {"name": "Рабочий чат", "link": "https://t.me/tech_workchat"}, - ], - }, -} - -# — Клавиатуры — - - -def get_contact_kb() -> ReplyKeyboardMarkup: - return ReplyKeyboardMarkup( - keyboard=[[ - KeyboardButton(text="📲 Поделиться контактом", request_contact=True) - ]], - resize_keyboard=True, - one_time_keyboard=True, - ) - - -def get_department_kb() -> InlineKeyboardMarkup: - buttons = [ - InlineKeyboardButton(text=dept["title"], callback_data=key) - for key, dept in DEPARTMENTS.items() - ] - return InlineKeyboardMarkup(inline_keyboard=[buttons]) - - -def main_menu_kb() -> InlineKeyboardMarkup: - return InlineKeyboardMarkup(inline_keyboard=[[ - InlineKeyboardButton(text="📅 Календарь", - callback_data="menu_calendar"), - InlineKeyboardButton(text="📊 Статус", callback_data="menu_status"), - InlineKeyboardButton(text="📄 Документы", callback_data="menu_docs"), - ]]) - - -def department_menu_kb() -> InlineKeyboardMarkup: - return InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text="📅 На сегодня", - callback_data="cal_today")], - [InlineKeyboardButton(text="🗓 На неделю", - callback_data="cal_week")], - [InlineKeyboardButton(text="📆 На месяц", - callback_data="cal_month")], - [InlineKeyboardButton(text="◀️ Главное меню", - callback_data="go_main")], - ]) - - -async def safe_edit( - call: types.CallbackQuery, - text: str, - inline_kb: InlineKeyboardMarkup | None = None, - **kwargs -): - """ - Редактирует call.message: - - если передан inline_kb — edit_text, - - иначе — answer. - """ - msg = call.message - try: - if inline_kb: - await msg.edit_text(text, reply_markup=inline_kb, **kwargs) - else: - await msg.answer(text, **kwargs) - except TelegramBadRequest as e: - await call.answer(text=f"Загрузка...", cache_time=10) - # await msg.answer(text, reply_markup=inline_kb, **kwargs) - -# — ORM‑хелперы — - - -@sync_to_async -def find_employee_by_phone(phone: str): - return Employee.objects.filter(telegram__endswith=phone[-10:]).first() - - -@sync_to_async -def find_employee_by_username(username: str): - uname = username.lower().lstrip('@') - return Employee.objects.filter( - models.Q(telegram__iexact=uname) | - models.Q(telegram__iexact='@' + uname) - ).first() - - -@sync_to_async -def bind_telegram(user, tg_id: int): - BotSubscriber.objects.filter(telegram_id=tg_id).exclude(user=user)\ - .update(telegram_id=None) - return BotSubscriber.objects.update_or_create( - user=user, defaults={"telegram_id": tg_id} - ) - - -@sync_to_async -def _get_subscriber_user(tg_id: int): - sub = BotSubscriber.objects.select_related("user")\ - .filter(telegram_id=tg_id).first() - return sub.user if sub and sub.user else None - - -@sync_to_async -def _get_user_documents(user): - from django.db.models import Q - return list( - Document.objects.filter(Q(sent_to_all=True) | Q(recipients=user)) - .distinct().order_by('-uploaded_at') - ) - - -@sync_to_async -def _has_acknowledged(user, document): - return DocumentAcknowledgement.objects.filter(user=user, document=document).exists() - - -@sync_to_async -def _acknowledge(user, document): - return DocumentAcknowledgement.objects.get_or_create(user=user, document=document) - - -@sync_to_async -def _get_one_time_events(date): - return list(CalendarEvent.objects.filter( - recurrence=CalendarEvent.ONE_TIME, date=date - )) - - -@sync_to_async -def _get_annual_events(): - return list(CalendarEvent.objects.filter(recurrence=CalendarEvent.ANNUAL)) - - -@sync_to_async -def _get_subscribers(): - return list(BotSubscriber.objects.filter(telegram_id__isnull=False)) - -# — Ежедневная рассылка — - - -async def send_daily_notifications(): - today = timezone.localdate() - one_time = await _get_one_time_events(today) - annual_all = await _get_annual_events() - annual_today = [ - ev for ev in annual_all - if (ev.date.month, ev.date.day) == (today.month, today.day) - ] - events = one_time + annual_today - if not events: - logger.info("Нет событий на сегодня.") - return - - subs = await _get_subscribers() - for sub in subs: - for ev in events: - text = ( - f"📅 *{ev.title}*\n" - f"Дата: `{ev.date:%d.%m.%Y}`\n" - f"{ev.description or ''}" - ) - try: - await bot.send_message( - sub.telegram_id, - text, - parse_mode="Markdown", - ) - except: - logger.exception(f"Не удалось отправить {sub.telegram_id}") - -scheduler = AsyncIOScheduler(timezone=settings.TIME_ZONE) -scheduler.add_job(send_daily_notifications, "cron", hour=9, minute=0) - - -async def on_startup_dispatcher(): - logger.info("Запуск APScheduler…") - scheduler.start() - -dp.startup.register(on_startup_dispatcher) - -# — Хендлеры бота — - - -@dp.message(CommandStart()) -async def cmd_start(msg: types.Message): - await msg.answer( - "Здравствуйте! Чтобы подключиться, пожалуйста, поделитесь контактом:", - reply_markup=get_contact_kb() - ) - - -@dp.message(lambda m: m.contact is not None) -async def on_contact(msg: types.Message): - phone = msg.contact.phone_number - tg_id = msg.from_user.id - user = ( - await find_employee_by_phone(phone) - or (msg.from_user.username and await find_employee_by_username(msg.from_user.username)) - ) - if not user: - return await msg.answer("❌ Профиль не найден.") - await bind_telegram(user, tg_id) - await msg.answer( - f"✅ Спасибо, {user.first_name}! Выберите отдел:", - reply_markup=get_department_kb() - ) - - -@dp.callback_query(F.data.startswith("department_"), IsBoundSubscriber_tg(), IsEmployeeActive()) -async def department_selected(call: types.CallbackQuery): - dept = DEPARTMENTS.get(call.data) - text = ( - f"*{dept['title']}*:\n" + - "\n".join(f"• [{g['name']}]({g['link']})" for g in dept["groups"]) - ) if dept else "Неизвестный отдел." - await safe_edit(call, text, inline_kb=main_menu_kb(), parse_mode="Markdown") - await call.answer() - - -@dp.callback_query(F.data == "menu_calendar", IsBoundSubscriber_tg(), IsEmployeeActive()) -async def menu_calendar(call: types.CallbackQuery): - await safe_edit(call, "Выберите период для просмотра:", inline_kb=department_menu_kb()) - await call.answer() - - -@dp.callback_query(F.data == "menu_status", IsBoundSubscriber_tg(), IsEmployeeActive()) -async def menu_status(call: types.CallbackQuery): - user = await _get_subscriber_user(call.from_user.id) - status = await sync_to_async(lambda u: u.employment_status)(user) if user else "неизвестен" - text = f"Ваш статус: *{status}*" - await safe_edit(call, text, inline_kb=main_menu_kb(), parse_mode="Markdown") - # await call.answer(cache_time=60, text="Смотрите статус в сообщении") - - -@dp.callback_query(F.data == "cal_today", IsBoundSubscriber_tg(), IsEmployeeActive()) -async def calendar_today(call: types.CallbackQuery): - today = timezone.localdate() - evs = (await _get_one_time_events(today)) + [ - ev for ev in await _get_annual_events() - if (ev.date.month, ev.date.day) == (today.month, today.day) - ] - text = ("Событий на сегодня нет." if not evs - else "📅 События сегодня:\n" + "\n".join(f"• {e.title} ({e.date:%d.%m})" for e in evs)) - kb = InlineKeyboardMarkup(inline_keyboard=[[ - InlineKeyboardButton(text="◀️ Главное меню", callback_data="go_main") - ]]) - await safe_edit(call, text, inline_kb=kb) - await call.answer() - - -@dp.callback_query(F.data == "cal_week", IsBoundSubscriber_tg(), IsEmployeeActive()) -async def calendar_week(call: types.CallbackQuery): - start, end = timezone.localdate(), timezone.localdate() + timedelta(days=6) - one = await _get_one_time_events(start) - ann = await _get_annual_events() - evs = one + [ - ev for ev in ann - if (start.month, start.day) <= (ev.date.month, ev.date.day) <= (end.month, end.day) - ] - text = ("Событий на эту неделю нет." if not evs - else f"📅 С {start:%d.%m} по {end:%d.%m}:\n" + "\n".join(f"• {e.title} ({e.date:%d.%m})" for e in evs)) - kb = InlineKeyboardMarkup(inline_keyboard=[[ - InlineKeyboardButton(text="◀️ Главное меню", callback_data="go_main") - ]]) - await safe_edit(call, text, inline_kb=kb) - await call.answer() - - -@dp.callback_query(F.data == "cal_month", IsBoundSubscriber_tg(), IsEmployeeActive()) -async def calendar_month(call: types.CallbackQuery): - today = timezone.localdate() - start = today.replace(day=1) - nm = start.replace(month=(start.month % 12) + 1, - year=start.year + (start.month // 12)) - end = nm - timedelta(days=1) - one = await _get_one_time_events(start) - ann = await _get_annual_events() - evs = one + [ - ev for ev in ann - if (start.month, start.day) <= (ev.date.month, ev.date.day) <= (end.month, end.day) - ] - text = ("Событий в этом месяце нет." if not evs - else f"📅 {today:%B %Y}:\n" + "\n".join(f"• {e.title} ({e.date:%d.%m})" for e in evs)) - kb = InlineKeyboardMarkup(inline_keyboard=[[ - InlineKeyboardButton(text="◀️ Главное меню", callback_data="go_main") - ]]) - await safe_edit(call, text, inline_kb=kb) - await call.answer() - - -@dp.callback_query(F.data == "go_main", IsBoundSubscriber_tg(), IsEmployeeActive()) -async def go_main(call: types.CallbackQuery): - await safe_edit(call, "Главное меню:", inline_kb=main_menu_kb()) - await call.answer() - -# — Документы через бот — - - -@dp.callback_query(F.data == "menu_docs", IsBoundSubscriber_tg(), IsEmployeeActive()) -async def menu_docs(call: types.CallbackQuery): - user = await _get_subscriber_user(call.from_user.id) - docs = await _get_user_documents(user) if user else [] - if not docs: - text = "У вас нет доступных документов." - kb = InlineKeyboardMarkup(inline_keyboard=[[ - InlineKeyboardButton(text="◀️ Главное меню", - callback_data="go_main") - ]]) - else: - text = "📄 Ваши документы:\nВыберите для просмотра:" - rows = [ - [InlineKeyboardButton( - text=d.title[:30] + ("…" if len(d.title) > 30 else ""), - callback_data=f"doc_{d.id}" - )] - for d in docs[:5] - ] - rows.append([InlineKeyboardButton( - text="◀️ Главное меню", callback_data="go_main")]) - kb = InlineKeyboardMarkup(inline_keyboard=rows) - - await safe_edit(call, text, inline_kb=kb) - await call.answer() - - -@dp.callback_query(F.data.startswith("doc_"), IsBoundSubscriber_tg(), IsEmployeeActive()) -async def show_document(call: types.CallbackQuery): - user = await _get_subscriber_user(call.from_user.id) - doc_id = int(call.data.split("_", 1)[1]) - doc = await sync_to_async(Document.objects.get)(pk=doc_id) - - caption = ( - f"*{doc.title}*\n\n" - f"{doc.description or ''}\n\n" - f"Загружен: {doc.uploaded_at:%d.%m.%Y %H:%M}" - ) - already = await _has_acknowledged(user, doc) - - rows = [] - if not already: - rows.append([InlineKeyboardButton( - text="✅ Ознакомился", callback_data=f"ack_{doc.id}")]) - rows.append([InlineKeyboardButton( - text="◀️ Документы", callback_data="menu_docs")]) - kb = InlineKeyboardMarkup(inline_keyboard=rows) - - file_input = FSInputFile( - path=doc.file.path, filename=os.path.basename(doc.file.name)) - await bot.send_document( - chat_id=call.from_user.id, - document=file_input, - caption=caption, - parse_mode="Markdown", - reply_markup=kb - ) - await call.answer() - - -@dp.callback_query(F.data.startswith("ack_"), IsBoundSubscriber_tg(), IsEmployeeActive()) -async def ack_document(call: types.CallbackQuery): - user = await _get_subscriber_user(call.from_user.id) - doc_id = int(call.data.split("_", 1)[1]) - await _acknowledge(user, await sync_to_async(Document.objects.get)(pk=doc_id)) - - old = call.message.reply_markup.inline_keyboard - new = [row for row in old if not any( - btn.callback_data == f"ack_{doc_id}" for btn in row)] - kb = InlineKeyboardMarkup(inline_keyboard=new) - - await call.message.edit_reply_markup(reply_markup=kb) - await call.answer("Ознакомление зафиксировано.") - - -def run_telegram_bot(): - dp.run_polling(bot) - - -if __name__ == "__main__": - run_telegram_bot() diff --git a/backend/bots/tests.py b/backend/bots/tests.py deleted file mode 100644 index 7ce503c2..00000000 --- a/backend/bots/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/backend/bots/utils.py b/backend/bots/utils.py deleted file mode 100644 index d2e58702..00000000 --- a/backend/bots/utils.py +++ /dev/null @@ -1,15 +0,0 @@ -# from django.shortcuts import render, redirect -# from django.contrib.auth.decorators import login_required -# import secrets - -# @login_required -# def generate_messenger_token(request): -# user = request.user -# user.registration_token = secrets.token_hex(16) -# user.save(update_fields=['registration_token']) -# telegram_link = f"https://t.me/your_bot?start={user.registration_token}" -# # Можно также вернуть QR-код -# return render(request, 'users/messenger_token.html', { -# 'telegram_link': telegram_link, -# 'token': user.registration_token, -# }) diff --git a/backend/bots/views.py b/backend/bots/views.py deleted file mode 100644 index 91ea44a2..00000000 --- a/backend/bots/views.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.shortcuts import render - -# Create your views here. diff --git a/backend/employees/signals.py b/backend/employees/signals.py index cd69420c..72b452f3 100644 --- a/backend/employees/signals.py +++ b/backend/employees/signals.py @@ -7,93 +7,6 @@ from django.utils import timezone from .constants import ACTION_HIRED from .models import Employee, EmployeeAction -from bots.models import BotSubscriber - - -if False: # pragma: no cover - временно отключаем сигнал для тестирования - @receiver(pre_save, sender=Employee) - def compress_and_cleanup_avatar(sender, instance: Employee, **kwargs): - """Сжимает новый аватар и удаляет старый файл при замене.""" - # Импортируем здесь, чтобы избежать циклических импортов - from common.image_utils import compress_avatar - - avatar_field = instance.avatar - avatar_name = getattr(avatar_field, "name", "") - if avatar_field and avatar_name: - storage = avatar_field.storage - if storage.exists(avatar_name): - try: - avatar_field.open("rb") - avatar_field.seek(0) - original_data = avatar_field.read() - - compressed_data = compress_avatar(original_data) - - if len(compressed_data) < len(original_data): - filename = avatar_name.split("/")[-1] - if not filename.lower().endswith(".jpg"): - filename = filename.rsplit(".", 1)[0] + ".jpg" - - avatar_field.save( - filename, - ContentFile(compressed_data), - save=False, - ) - except FileNotFoundError: - print( - "Avatar file missing during compression: " - f"{avatar_name}" - ) - except Exception as e: - # Логируем ошибку, но не прерываем сохранение - print(f"Error compressing avatar: {e}") - - # Удаляем старый файл если он был заменен - if instance.pk is None: - return - - try: - old = sender.objects.get(pk=instance.pk) - except sender.DoesNotExist: - return - - # Если аватар изменился или удален - if old.avatar and old.avatar != instance.avatar: - # Удаляем старый файл с диска - if os.path.isfile(old.avatar.path): - try: - os.remove(old.avatar.path) - except Exception as e: - print(f"Error deleting old avatar: {e}") - - -@receiver(pre_save, sender=Employee) -def unbind_bot_subscriber_on_contact_change( - sender, instance: Employee, **kwargs -): - # Игнорируем новых пользователей - if instance.pk is None: - return - - old = sender.objects.get(pk=instance.pk) - try: - sub = old.bot_subscription - except BotSubscriber.DoesNotExist: - return - - # Если очистили или изменили telegram в профиле — сбросим telegram_id - if old.telegram and old.telegram != (instance.telegram or "").strip(): - sub.telegram_id = None - sub.save(update_fields=["telegram_id"]) - - # Аналогично для whatsapp - if old.whatsapp and old.whatsapp != instance.whatsapp: - sub.whatsapp_id = None - sub.save(update_fields=["whatsapp_id"]) - - if old.wechat and old.wechat != instance.wechat: - sub.wechat_id = None - sub.save(update_fields=["wechat_id"]) @receiver(post_save, sender=Employee) diff --git a/backend/eusrr_backend/settings.py b/backend/eusrr_backend/settings.py index 66fecae1..ab71ebaf 100644 --- a/backend/eusrr_backend/settings.py +++ b/backend/eusrr_backend/settings.py @@ -49,7 +49,6 @@ INSTALLED_APPS = [ # Основные приложения "employees.apps.EmployeesConfig", "api.apps.ApiConfig", - "hikcentral.apps.HikcentralConfig", "schedule", # django-scheduler (проверенная библиотека для календаря) "calendar_app.apps.CalendarAppConfig", # Наше приложение календаря "documents.apps.DocumentsConfig", @@ -60,7 +59,6 @@ INSTALLED_APPS = [ "notifications.apps.NotificationsConfig", "watson", # django-watson для полнотекстового поиска "search.apps.SearchConfig", - "bots", "finance.apps.FinanceConfig", "procurement.apps.ProcurementConfig", "push_notifications", # django-push-notifications для Web Push @@ -114,15 +112,6 @@ USE_SQLITE = os.getenv("USE_SQLITE", "false").lower() == "true" DATABASES = { "default": {}, - "hikcentral": { - "ENGINE": "django.db.backends.postgresql", - "NAME": "cms", - "USER": "postgres", - "PASSWORD": None, - "HOST": "127.0.0.1", - "PORT": "5432", - "OPTIONS": {"options": "-c search_path=platform"}, - }, } if USE_SQLITE: diff --git a/backend/hikcentral/__init__.py b/backend/hikcentral/__init__.py deleted file mode 100644 index 5e0902da..00000000 --- a/backend/hikcentral/__init__.py +++ /dev/null @@ -1 +0,0 @@ -default_app_config = 'hikcentral.apps.HikcentralConfig' diff --git a/backend/hikcentral/admin.py b/backend/hikcentral/admin.py deleted file mode 100644 index 8c38f3f3..00000000 --- a/backend/hikcentral/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/backend/hikcentral/apps.py b/backend/hikcentral/apps.py deleted file mode 100644 index 5f724df6..00000000 --- a/backend/hikcentral/apps.py +++ /dev/null @@ -1,10 +0,0 @@ -# hikcentral/apps.py - -from django.apps import AppConfig - - -class HikcentralConfig(AppConfig): - name = 'hikcentral' - - def ready(self): - import hikcentral.signals diff --git a/backend/hikcentral/migrations/0001_initial.py b/backend/hikcentral/migrations/0001_initial.py deleted file mode 100644 index 53a47632..00000000 --- a/backend/hikcentral/migrations/0001_initial.py +++ /dev/null @@ -1,65 +0,0 @@ -# Generated by Django 5.2.4 on 2025-07-24 09:24 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='HikPerson', - fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), - ('person_group_id', models.IntegerField(blank=True, null=True)), - ('person_type', models.IntegerField(blank=True, null=True)), - ('person_code', models.CharField(max_length=16, unique=True)), - ('given_name', models.CharField(blank=True, max_length=255, null=True)), - ('family_name', models.CharField(blank=True, max_length=255, null=True)), - ('full_name', models.CharField(blank=True, max_length=255, null=True)), - ('gender', models.IntegerField(blank=True, null=True)), - ('email', models.EmailField(blank=True, max_length=254, null=True)), - ('phone_number', models.CharField(blank=True, max_length=32, null=True)), - ('photo_url', models.CharField(blank=True, max_length=512, null=True)), - ('photo_index', models.IntegerField(blank=True, null=True)), - ('small_photo_url', models.CharField(blank=True, max_length=512, null=True)), - ('start_valid_date', models.DateTimeField(blank=True, null=True)), - ('end_valid_date', models.DateTimeField(blank=True, null=True)), - ('remark', models.TextField(blank=True, null=True)), - ('update_time', models.DateTimeField(blank=True, null=True)), - ('create_time', models.DateTimeField(blank=True, null=True)), - ('is_deleted', models.IntegerField(default=0)), - ('object_guid', models.CharField(blank=True, max_length=64, null=True)), - ('person_from', models.IntegerField(blank=True, null=True)), - ('rdn', models.CharField(blank=True, max_length=255, null=True)), - ('dn', models.CharField(blank=True, max_length=255, null=True)), - ('usn_changed', models.BigIntegerField(blank=True, null=True)), - ('start_time_differ', models.IntegerField(blank=True, null=True)), - ('end_time_differ', models.IntegerField(blank=True, null=True)), - ], - options={ - 'db_table': 'person', - 'managed': False, - }, - ), - migrations.CreateModel( - name='HikPersonHeadPic', - fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), - ('standard_head_portrait', models.TextField(blank=True, null=True)), - ('thumbnail_head_portrait', models.TextField(blank=True, null=True)), - ('deepmode_head_portrait', models.TextField(blank=True, null=True)), - ('identi_photo', models.TextField(blank=True, null=True)), - ('create_time', models.DateTimeField(blank=True, null=True)), - ('update_time', models.DateTimeField(blank=True, null=True)), - ], - options={ - 'db_table': 'person_head_pic', - 'managed': False, - }, - ), - ] diff --git a/backend/hikcentral/migrations/__init__.py b/backend/hikcentral/migrations/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/backend/hikcentral/models.py b/backend/hikcentral/models.py deleted file mode 100644 index f870a399..00000000 --- a/backend/hikcentral/models.py +++ /dev/null @@ -1,68 +0,0 @@ -# hikcentral/models.py - -from django.db import models - - -class HikPerson(models.Model): - id = models.AutoField(primary_key=True) - person_group_id = models.IntegerField(blank=True, null=True) - person_type = models.IntegerField(blank=True, null=True) - # важно! Уникальный ID от 1 до 16 символов (цифры/буквы) - person_code = models.CharField(max_length=16, unique=True) - given_name = models.CharField(max_length=255, blank=True, null=True) - family_name = models.CharField(max_length=255, blank=True, null=True) - full_name = models.CharField(max_length=255, blank=True, null=True) - gender = models.IntegerField(blank=True, null=True) - email = models.EmailField(blank=True, null=True) - phone_number = models.CharField(max_length=32, blank=True, null=True) - photo_url = models.CharField( - max_length=512, blank=True, null=True, default="") - photo_index = models.IntegerField(blank=True, null=True) - small_photo_url = models.CharField(max_length=512, blank=True, null=True) - start_valid_date = models.DateTimeField(blank=True, null=True) - end_valid_date = models.DateTimeField(blank=True, null=True) - remark = models.TextField(blank=True, null=True) - update_time = models.DateTimeField(blank=True, null=True) - create_time = models.DateTimeField(blank=True, null=True) - is_deleted = models.IntegerField(default=0) - object_guid = models.CharField(max_length=64, blank=True, null=True) - person_from = models.IntegerField(blank=True, null=True) - rdn = models.CharField(max_length=255, blank=True, null=True) - dn = models.CharField(max_length=255, blank=True, null=True) - usn_changed = models.BigIntegerField(blank=True, null=True) - start_time_differ = models.IntegerField(blank=True, null=True) - end_time_differ = models.IntegerField(blank=True, null=True) - - class Meta: - db_table = 'person' - app_label = 'hikcentral' - managed = False # очень важно! — Django не управляет этой таблицей - - def __str__(self): - return self.full_name or self.person_code - - -class HikPersonHeadPic(models.Model): - id = models.AutoField(primary_key=True) - person = models.ForeignKey( - HikPerson, - db_column='person_id', - on_delete=models.CASCADE, - related_name='head_pics' - ) - standard_head_portrait = models.TextField( - blank=True, null=True) # основное фото (base64-строка) - thumbnail_head_portrait = models.TextField( - blank=True, null=True) # миниатюра (base64) - deepmode_head_portrait = models.TextField(blank=True, null=True) - identi_photo = models.TextField(blank=True, null=True) - create_time = models.DateTimeField(blank=True, null=True) - update_time = models.DateTimeField(blank=True, null=True) - - class Meta: - db_table = 'person_head_pic' - app_label = 'hikcentral' - managed = False - - def __str__(self): - return f"Photo for person_id={self.person_id}" diff --git a/backend/hikcentral/signals.py b/backend/hikcentral/signals.py deleted file mode 100644 index b9bd3f7e..00000000 --- a/backend/hikcentral/signals.py +++ /dev/null @@ -1,77 +0,0 @@ -# # backend/hikcentral/signals.py - -# from django.db.models.signals import post_save, post_delete -# from django.dispatch import receiver -# from users.models import Employee -# from .models import HikPerson, HikPersonHeadPic -# from django.utils import timezone -# from .utils import gen_person_code, compress_image_to_base64 - - -# @receiver(post_save, sender=Employee) -# def sync_employee_to_hikcentral(sender, instance, created, **kwargs): -# db = 'hikcentral' - -# # Определяем person_code (уникальный) -# person_code = gen_person_code(instance) - -# # Проставим даты -# now = timezone.now() -# start_valid = now -# end_valid = now.replace(year=now.year+10) - -# # 1. Создаём/обновляем запись в person -# person_values = dict( -# person_group_id=1, -# person_type=0, -# person_code=person_code, -# given_name=instance.first_name, -# family_name=instance.last_name, -# full_name=f"{instance.first_name} {instance.last_name}", -# gender=0, # по аналогии: 0 - не указан, 1 - муж, 2 - жен -# email=instance.email, -# phone_number=str( -# instance.phone_number) if instance.phone_number else "", -# photo_url=instance.avatar.url if instance.avatar else "", -# photo_index=0, -# small_photo_url=instance.avatar.url if instance.avatar else "", -# start_valid_date=start_valid, -# end_valid_date=end_valid, -# remark='', -# update_time=now, -# create_time=now, -# is_deleted=0, -# person_from=0, -# usn_changed=0, -# start_time_differ=10800, -# end_time_differ=10800, -# ) - -# # Сохраняем сотрудника в HikCentral -# person = HikPerson.objects.using(db).create(**person_values) - -# # 2. Картинка (photo/avatar) -# if instance.avatar: -# img_b64 = compress_image_to_base64(instance.avatar) -# HikPersonHeadPic.objects.using(db).create( -# person_id=person.id, -# standard_head_portrait=img_b64, -# thumbnail_head_portrait='', -# deepmode_head_portrait='', -# identi_photo='', -# create_time=now, -# update_time=now, -# ) - - -# @receiver(post_delete, sender=Employee) -# def delete_employee_from_hikcentral(sender, instance, **kwargs): -# db = 'hikcentral' -# # Найдём всех по имени и почте — или храни person_code в Employee -# HikPerson.objects.using(db).filter( -# email=instance.email, -# full_name=f"{instance.first_name} {instance.last_name}", -# ).delete() -# # Не забудь удалить фото -# # person_ids = list(...) -# # HikPersonHeadPic.objects.using(db).filter(person_id__in=person_ids).delete() diff --git a/backend/hikcentral/tests.py b/backend/hikcentral/tests.py deleted file mode 100644 index 7ce503c2..00000000 --- a/backend/hikcentral/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/backend/hikcentral/utils.py b/backend/hikcentral/utils.py deleted file mode 100644 index f2186f6b..00000000 --- a/backend/hikcentral/utils.py +++ /dev/null @@ -1,50 +0,0 @@ -import base64 -from PIL import Image -from io import BytesIO -import random -import string - - -def gen_person_code(instance): - import re - base = str(instance.phone_number or instance.email or instance.pk) - code = re.sub(r'\W+', '', base)[:16] - - # Проверяем уникальность, если не уникален — добавляем символы в конец - from .models import HikPerson - existing = ( - HikPerson.objects.using('hikcentral') - .filter(person_code=code) - .exists() - ) - orig_code = code - while existing: - # Укорачиваем до 12 символов и дописываем случайные 4 символа - code = ( - orig_code[:12] + ''.join(random.choices(string.ascii_letters + string.digits, k=4)))[:16] - existing = ( - HikPerson.objects.using('hikcentral') - .filter(person_code=code) - .exists() - ) - return code - - -def compress_image_to_base64(image_field, target_size_kb=200): - """Сжимает изображение до размера (примерно) target_size_kb и возвращает base64-строку.""" - if not image_field: - return '' - image = Image.open(image_field) - output = BytesIO() - quality = 90 - # Сжимать до ~200КБ - while True: - output.seek(0) - image.save(output, format='JPEG', quality=quality) - size_kb = output.tell() / 1024 - if size_kb <= target_size_kb or quality <= 20: - break - quality -= 10 - output.seek(0) - img_b64 = base64.b64encode(output.read()).decode('utf-8') - return img_b64 diff --git a/backend/hikcentral/views.py b/backend/hikcentral/views.py deleted file mode 100644 index 91ea44a2..00000000 --- a/backend/hikcentral/views.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.shortcuts import render - -# Create your views here.