Игорь Надеин b38d2bc44b fix: автоматическое обновление JWT токенов
- ApiClient теперь обрабатывает 401 и 403 ошибки для обновления токена
- Добавлен JWTRefreshMiddleware для проактивного обновления токенов за 5 минут до истечения
- Добавлена настройка JWT_REFRESH_THRESHOLD_MINUTES (по умолчанию 5)
- Исправлена проблема с периодическими 403 ошибками после истечения access token
2025-11-20 18:52:29 +03:00

415 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from django.db import models
from django.conf import settings
from django.utils import timezone
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
class NotificationCategory(models.Model):
"""Категории уведомлений для группировки"""
CATEGORY_CHOICES = [
('communications', 'Коммуникации'),
('documents', 'Документы'),
('requests', 'Заявления'),
('calendar', 'Календарь'),
('department', 'Отдел'),
('profile', 'Профиль'),
('feed', 'Новости'),
('system', 'Система'),
]
code = models.CharField(
max_length=50,
unique=True,
choices=CATEGORY_CHOICES,
verbose_name='Код'
)
name = models.CharField(max_length=100, verbose_name='Название')
description = models.TextField(blank=True, verbose_name='Описание')
icon = models.CharField(
max_length=50,
default='bi-bell',
verbose_name='Иконка',
help_text='Bootstrap icon класс'
)
color = models.CharField(
max_length=20,
default='primary',
verbose_name='Цвет',
help_text='Bootstrap цвет'
)
is_active = models.BooleanField(default=True, verbose_name='Активна')
order = models.IntegerField(default=0, verbose_name='Порядок')
class Meta:
ordering = ['order', 'name']
verbose_name = 'Категория уведомлений'
verbose_name_plural = 'Категории уведомлений'
db_table = 'notifications_category'
def __str__(self):
return self.name
class NotificationType(models.Model):
"""Конкретные типы уведомлений внутри категорий"""
category = models.ForeignKey(
NotificationCategory,
on_delete=models.CASCADE,
related_name='types',
verbose_name='Категория'
)
code = models.CharField(
max_length=100,
unique=True,
verbose_name='Код',
help_text='Уникальный код типа, например: chat_new_message'
)
name = models.CharField(max_length=200, verbose_name='Название')
description = models.TextField(blank=True, verbose_name='Описание')
# Настройки по умолчанию для всех пользователей
default_enabled = models.BooleanField(
default=True,
verbose_name='Включено по умолчанию'
)
default_channels = models.JSONField(
default=dict,
verbose_name='Каналы по умолчанию',
help_text='{"web": true, "email": false, "telegram": false}'
)
# Приоритет
PRIORITY_CHOICES = [
('low', 'Низкий'),
('normal', 'Обычный'),
('high', 'Высокий'),
('urgent', 'Срочный'),
]
priority = models.CharField(
max_length=20,
choices=PRIORITY_CHOICES,
default='normal',
verbose_name='Приоритет'
)
# Можно ли отключить
is_required = models.BooleanField(
default=False,
verbose_name='Обязательное',
help_text='Нельзя отключить (например, системные уведомления)'
)
# Группировка
is_groupable = models.BooleanField(
default=True,
verbose_name='Можно группировать',
help_text='Объединять несколько похожих уведомлений'
)
grouping_window_minutes = models.IntegerField(
default=5,
verbose_name='Окно группировки (минуты)',
help_text='Группировать уведомления за последние N минут'
)
is_active = models.BooleanField(default=True, verbose_name='Активен')
created_at = models.DateTimeField(auto_now_add=True, verbose_name='Создан')
updated_at = models.DateTimeField(auto_now=True, verbose_name='Обновлен')
class Meta:
ordering = ['category__order', 'name']
verbose_name = 'Тип уведомления'
verbose_name_plural = 'Типы уведомлений'
db_table = 'notifications_type'
def __str__(self):
return f'{self.category.name}: {self.name}'
class Notification(models.Model):
"""Конкретное уведомление для пользователя"""
recipient = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='notifications',
verbose_name='Получатель'
)
notification_type = models.ForeignKey(
NotificationType,
on_delete=models.CASCADE,
verbose_name='Тип'
)
# Заголовок и текст
title = models.CharField(max_length=255, verbose_name='Заголовок')
message = models.TextField(verbose_name='Сообщение')
short_message = models.CharField(
max_length=150,
blank=True,
verbose_name='Краткое сообщение',
help_text='Для превью'
)
# Связь с объектом (GenericForeignKey)
content_type = models.ForeignKey(
ContentType,
on_delete=models.CASCADE,
null=True,
blank=True,
verbose_name='Тип объекта'
)
object_id = models.PositiveIntegerField(
null=True,
blank=True,
verbose_name='ID объекта'
)
content_object = GenericForeignKey('content_type', 'object_id')
# Метаданные
metadata = models.JSONField(
default=dict,
blank=True,
verbose_name='Метаданные',
help_text='Дополнительные данные для рендеринга'
)
# Ссылка действия
action_url = models.CharField(
max_length=500,
blank=True,
verbose_name='Ссылка действия'
)
action_text = models.CharField(
max_length=100,
default='Посмотреть',
verbose_name='Текст кнопки'
)
# Статусы
is_read = models.BooleanField(
default=False,
verbose_name='Прочитано',
db_index=True
)
read_at = models.DateTimeField(
null=True,
blank=True,
verbose_name='Прочитано когда'
)
is_archived = models.BooleanField(
default=False,
verbose_name='В архиве'
)
archived_at = models.DateTimeField(
null=True,
blank=True,
verbose_name='Заархивировано когда'
)
# Доставка
sent_web = models.BooleanField(default=False, verbose_name='Отправлено на сайт')
sent_email = models.BooleanField(default=False, verbose_name='Отправлено на email')
sent_telegram = models.BooleanField(default=False, verbose_name='Отправлено в Telegram')
sent_whatsapp = models.BooleanField(default=False, verbose_name='Отправлено в WhatsApp')
sent_wechat = models.BooleanField(default=False, verbose_name='Отправлено в WeChat')
sent_at = models.DateTimeField(
null=True,
blank=True,
verbose_name='Отправлено когда'
)
# Группировка
group_key = models.CharField(
max_length=255,
blank=True,
db_index=True,
verbose_name='Ключ группировки',
help_text='Для объединения похожих уведомлений'
)
is_grouped = models.BooleanField(
default=False,
verbose_name='Сгруппировано'
)
grouped_count = models.IntegerField(
default=1,
verbose_name='Количество в группе',
help_text='Сколько уведомлений объединено'
)
created_at = models.DateTimeField(
auto_now_add=True,
db_index=True,
verbose_name='Создано'
)
updated_at = models.DateTimeField(auto_now=True, verbose_name='Обновлено')
class Meta:
ordering = ['-created_at']
indexes = [
models.Index(fields=['recipient', '-created_at']),
models.Index(fields=['recipient', 'is_read']),
models.Index(fields=['notification_type', '-created_at']),
models.Index(fields=['group_key', '-created_at']),
]
verbose_name = 'Уведомление'
verbose_name_plural = 'Уведомления'
db_table = 'notifications_notification'
def __str__(self):
return f'{self.recipient.get_short_name()}: {self.title}'
def mark_as_read(self):
"""Отметить как прочитанное"""
if not self.is_read:
self.is_read = True
self.read_at = timezone.now()
self.save(update_fields=['is_read', 'read_at'])
def archive(self):
"""Заархивировать уведомление"""
if not self.is_archived:
self.is_archived = True
self.archived_at = timezone.now()
self.save(update_fields=['is_archived', 'archived_at'])
class UserNotificationSettings(models.Model):
"""Персональные настройки уведомлений пользователя"""
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='notification_settings',
verbose_name='Пользователь'
)
notification_type = models.ForeignKey(
NotificationType,
on_delete=models.CASCADE,
verbose_name='Тип уведомления'
)
# Включено ли уведомление
is_enabled = models.BooleanField(
default=True,
verbose_name='Включено'
)
# Каналы доставки
send_web = models.BooleanField(
default=True,
verbose_name='Отправлять на сайт'
)
send_email = models.BooleanField(
default=False,
verbose_name='Отправлять на email'
)
send_telegram = models.BooleanField(
default=False,
verbose_name='Отправлять в Telegram'
)
send_whatsapp = models.BooleanField(
default=False,
verbose_name='Отправлять в WhatsApp'
)
send_wechat = models.BooleanField(
default=False,
verbose_name='Отправлять в WeChat'
)
# Тихий режим (не беспокоить)
quiet_hours_enabled = models.BooleanField(
default=False,
verbose_name='Тихий режим включен'
)
quiet_start_time = models.TimeField(
null=True,
blank=True,
verbose_name='Начало тихого режима',
help_text='Например, 22:00'
)
quiet_end_time = models.TimeField(
null=True,
blank=True,
verbose_name='Конец тихого режима',
help_text='Например, 08:00'
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name='Создано')
updated_at = models.DateTimeField(auto_now=True, verbose_name='Обновлено')
class Meta:
unique_together = ['user', 'notification_type']
verbose_name = 'Настройка уведомлений пользователя'
verbose_name_plural = 'Настройки уведомлений пользователей'
db_table = 'notifications_user_settings'
def __str__(self):
return f'{self.user.get_short_name()}: {self.notification_type.name}'
class NotificationTemplate(models.Model):
"""Шаблоны для разных каналов доставки"""
notification_type = models.ForeignKey(
NotificationType,
on_delete=models.CASCADE,
related_name='templates',
verbose_name='Тип уведомления'
)
CHANNEL_CHOICES = [
('web', 'Веб'),
('email', 'Email'),
('telegram', 'Telegram'),
('whatsapp', 'WhatsApp'),
('wechat', 'WeChat'),
]
channel = models.CharField(
max_length=20,
choices=CHANNEL_CHOICES,
verbose_name='Канал'
)
# Шаблоны (используем Django template syntax)
title_template = models.TextField(
verbose_name='Шаблон заголовка',
help_text='Django template синтаксис'
)
message_template = models.TextField(
verbose_name='Шаблон сообщения',
help_text='Django template синтаксис'
)
# Для email
html_template = models.TextField(
blank=True,
verbose_name='HTML шаблон',
help_text='Для email'
)
# Для кнопок действий
action_button_template = models.CharField(
max_length=200,
blank=True,
verbose_name='Шаблон кнопки действия'
)
is_active = models.BooleanField(default=True, verbose_name='Активен')
created_at = models.DateTimeField(auto_now_add=True, verbose_name='Создан')
updated_at = models.DateTimeField(auto_now=True, verbose_name='Обновлен')
class Meta:
unique_together = ['notification_type', 'channel']
verbose_name = 'Шаблон уведомления'
verbose_name_plural = 'Шаблоны уведомлений'
db_table = 'notifications_template'
def __str__(self):
return f'{self.notification_type.name} - {self.get_channel_display()}'