очень много лишнего удалил в частности ботов и хикцентрал

This commit is contained in:
lizerk 2026-03-10 13:42:45 +03:00
parent 0f8dcff4ad
commit 0be01476fa
29 changed files with 0 additions and 1052 deletions

View File

@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View File

@ -1,6 +0,0 @@
from django.apps import AppConfig
class BotsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'bots'

View File

@ -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)

View File

@ -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()

View File

@ -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': 'Подписчики ботов',
},
),
]

View File

@ -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),
),
]

View File

@ -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',
),
]

View File

@ -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'),
),
]

View File

@ -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 "-"})'

View File

@ -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()

View File

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@ -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,
# })

View File

@ -1,3 +0,0 @@
from django.shortcuts import render
# Create your views here.

View File

@ -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)

View File

@ -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:

View File

@ -1 +0,0 @@
default_app_config = 'hikcentral.apps.HikcentralConfig'

View File

@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View File

@ -1,10 +0,0 @@
# hikcentral/apps.py
from django.apps import AppConfig
class HikcentralConfig(AppConfig):
name = 'hikcentral'
def ready(self):
import hikcentral.signals

View File

@ -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,
},
),
]

View File

@ -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}"

View File

@ -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()

View File

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@ -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

View File

@ -1,3 +0,0 @@
from django.shortcuts import render
# Create your views here.