очень много лишнего удалил в частности ботов и хикцентрал
This commit is contained in:
parent
0f8dcff4ad
commit
0be01476fa
@ -1,3 +0,0 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
@ -1,6 +0,0 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class BotsConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'bots'
|
||||
@ -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)
|
||||
@ -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()
|
||||
@ -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': 'Подписчики ботов',
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
@ -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',
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -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 "-"})'
|
||||
@ -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()
|
||||
@ -1,3 +0,0 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@ -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,
|
||||
# })
|
||||
@ -1,3 +0,0 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
@ -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)
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -1 +0,0 @@
|
||||
default_app_config = 'hikcentral.apps.HikcentralConfig'
|
||||
@ -1,3 +0,0 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
@ -1,10 +0,0 @@
|
||||
# hikcentral/apps.py
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class HikcentralConfig(AppConfig):
|
||||
name = 'hikcentral'
|
||||
|
||||
def ready(self):
|
||||
import hikcentral.signals
|
||||
@ -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,
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -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}"
|
||||
@ -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()
|
||||
@ -1,3 +0,0 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@ -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
|
||||
@ -1,3 +0,0 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
Loading…
x
Reference in New Issue
Block a user