704d5d9538
Добавлен фильтр по user_location_type (Аптека/Склад/Офис) в TeamSyncSettings и ChannelSyncSettings — наряду с фильтром по отделам. При включении обоих фильтров условия объединяются по AND. - поля filter_by_location_type + location_types (ArrayField, чекбоксы в админке); - логика add_user_to_teams/channels учитывает тип локации; - django.contrib.postgres в INSTALLED_APPS (для ArrayField). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
308 lines
13 KiB
Python
Executable File
308 lines
13 KiB
Python
Executable File
# models.py
|
|
from django.contrib.postgres.fields import ArrayField
|
|
from django.db import models
|
|
from django.contrib.auth import get_user_model
|
|
from django.utils import timezone
|
|
|
|
from core.models.fields import EncryptedCharField
|
|
|
|
# Типы локаций сотрудника (Employee.LOCATION_TYPES) — для фильтра синхронизации.
|
|
LOCATION_TYPE_CHOICES = [
|
|
('pharmacy', 'Аптека'),
|
|
('warehouse', 'Склад'),
|
|
('office', 'Офис'),
|
|
]
|
|
|
|
User = get_user_model()
|
|
|
|
|
|
class MattermostBot(models.Model):
|
|
"""Отдельный Mattermost-бот на роль. Каждая интеграционная операция берёт
|
|
бота по своей роли, а не общий сервисный токен. Токен хранится в БД
|
|
зашифрованным (EncryptedCharField). Имя бота (display_name/username) —
|
|
только метка: сменить тему/переименовать можно, не трогая код."""
|
|
|
|
ROLE_PROVISIONING = 'provisioning'
|
|
ROLE_CHANNELS = 'channels'
|
|
ROLE_TASKS = 'tasks'
|
|
ROLE_CHOICES = [
|
|
(ROLE_PROVISIONING, 'Провижининг (сотрудники)'),
|
|
(ROLE_CHANNELS, 'Каналы'),
|
|
(ROLE_TASKS, 'Задачи / уведомления портала'),
|
|
]
|
|
|
|
role = models.CharField('Роль', max_length=20, choices=ROLE_CHOICES, unique=True)
|
|
display_name = models.CharField('Имя', max_length=100, blank=True)
|
|
username = models.CharField('Username в Mattermost', max_length=150, blank=True)
|
|
user_id = models.CharField('User ID в Mattermost', max_length=100, blank=True)
|
|
# Access-токен хранится зашифрованным; max_length с запасом под шифртекст.
|
|
token = EncryptedCharField('Access-токен', max_length=512, blank=True)
|
|
is_active = models.BooleanField('Активен', default=True)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
db_table = 'mattermost_bots'
|
|
verbose_name = 'Mattermost-бот'
|
|
verbose_name_plural = 'Mattermost-боты'
|
|
ordering = ['role']
|
|
|
|
def __str__(self):
|
|
label = self.display_name or self.username or self.get_role_display()
|
|
return f'{label} ({self.get_role_display()})'
|
|
|
|
@classmethod
|
|
def get_active(cls, role: str) -> 'MattermostBot | None':
|
|
"""Активный бот для роли или None."""
|
|
return cls.objects.filter(role=role, is_active=True).first()
|
|
|
|
class MattermostUser(models.Model):
|
|
"""Связь между Django пользователем и Mattermost пользователем"""
|
|
django_user = models.OneToOneField(
|
|
User,
|
|
on_delete=models.CASCADE,
|
|
related_name='mattermost_profile'
|
|
)
|
|
mattermost_id = models.CharField(max_length=100, unique=True)
|
|
username = models.CharField(max_length=150)
|
|
email = models.EmailField()
|
|
is_active = models.BooleanField(default=True)
|
|
access_token_encrypted = models.TextField(blank=True, default='')
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
last_sync = models.DateTimeField(null=True, blank=True)
|
|
|
|
class Meta:
|
|
db_table = 'mattermost_users'
|
|
verbose_name = 'Mattermost User'
|
|
verbose_name_plural = 'Mattermost Users'
|
|
|
|
def __str__(self):
|
|
return f"{self.django_user.username} -> {self.username}"
|
|
|
|
def set_access_token(self, plain: str) -> None:
|
|
from core.utils.crypto import encrypt_secret
|
|
self.access_token_encrypted = encrypt_secret(plain)
|
|
|
|
def get_access_token(self) -> str:
|
|
from core.utils.crypto import decrypt_secret
|
|
return decrypt_secret(self.access_token_encrypted)
|
|
|
|
class MattermostTeam(models.Model):
|
|
"""Команды Mattermost"""
|
|
team_id = models.CharField(max_length=100, unique=True)
|
|
name = models.CharField(max_length=150)
|
|
display_name = models.CharField(max_length=150)
|
|
description = models.TextField(blank=True)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
class Meta:
|
|
db_table = 'mattermost_teams'
|
|
|
|
def __str__(self):
|
|
return self.display_name
|
|
|
|
class MattermostChannel(models.Model):
|
|
"""Каналы Mattermost"""
|
|
CHANNEL_TYPES = [
|
|
('O', 'Open'),
|
|
('P', 'Private'),
|
|
('D', 'Direct'),
|
|
('G', 'Group'),
|
|
]
|
|
|
|
channel_id = models.CharField(max_length=100, unique=True)
|
|
team = models.ForeignKey(MattermostTeam, on_delete=models.CASCADE)
|
|
name = models.CharField(max_length=150)
|
|
display_name = models.CharField(max_length=150)
|
|
channel_type = models.CharField(max_length=1, choices=CHANNEL_TYPES)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
class Meta:
|
|
db_table = 'mattermost_channels'
|
|
|
|
def __str__(self):
|
|
return f"{self.team.name}:{self.name}"
|
|
|
|
class MattermostTeamMembership(models.Model):
|
|
"""Членство пользователей в командах"""
|
|
user = models.ForeignKey(MattermostUser, on_delete=models.CASCADE)
|
|
team = models.ForeignKey(MattermostTeam, on_delete=models.CASCADE)
|
|
role = models.CharField(max_length=50, default='team_user')
|
|
joined_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
class Meta:
|
|
db_table = 'mattermost_team_memberships'
|
|
unique_together = ['user', 'team']
|
|
|
|
class MattermostChannelMembership(models.Model):
|
|
"""Членство пользователей в каналах"""
|
|
user = models.ForeignKey(MattermostUser, on_delete=models.CASCADE)
|
|
channel = models.ForeignKey(MattermostChannel, on_delete=models.CASCADE)
|
|
role = models.CharField(max_length=50, default='channel_user')
|
|
joined_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
class Meta:
|
|
db_table = 'mattermost_channel_memberships'
|
|
unique_together = ['user', 'channel']
|
|
|
|
|
|
class TeamSyncSettings(models.Model):
|
|
"""Настройки синхронизации команд Mattermost"""
|
|
team = models.OneToOneField(
|
|
MattermostTeam,
|
|
on_delete=models.CASCADE,
|
|
related_name='sync_settings',
|
|
verbose_name='Команда Mattermost'
|
|
)
|
|
|
|
auto_add_users = models.BooleanField(
|
|
default=True,
|
|
verbose_name='Автоматически добавлять пользователей',
|
|
help_text='Автоматически добавлять новых пользователей в эту команду при синхронизации'
|
|
)
|
|
|
|
is_default = models.BooleanField(
|
|
default=False,
|
|
verbose_name='Команда по умолчанию',
|
|
help_text='Использовать эту команду как основную для синхронизации'
|
|
)
|
|
|
|
priority = models.IntegerField(
|
|
default=0,
|
|
verbose_name='Приоритет',
|
|
help_text='Порядок добавления пользователей (выше = раньше)'
|
|
)
|
|
|
|
filter_by_department = models.BooleanField(
|
|
default=False,
|
|
verbose_name='Фильтровать по отделам',
|
|
help_text='Добавлять только пользователей из указанных отделов'
|
|
)
|
|
|
|
departments = models.ManyToManyField(
|
|
'core.Directory',
|
|
blank=True,
|
|
limit_choices_to={'directory_type__code': 'department', 'is_active': True},
|
|
verbose_name='Отделы',
|
|
help_text='Отделы, пользователи которых будут добавлены в команду'
|
|
)
|
|
|
|
filter_by_location_type = models.BooleanField(
|
|
default=False,
|
|
verbose_name='Фильтровать по типу локации',
|
|
help_text='Добавлять только пользователей с выбранным типом локации (Аптека/Склад/Офис)'
|
|
)
|
|
|
|
location_types = ArrayField(
|
|
models.CharField(max_length=20, choices=LOCATION_TYPE_CHOICES),
|
|
blank=True, default=list,
|
|
verbose_name='Типы локаций',
|
|
help_text='Пользователи с этими типами локации будут добавлены в команду'
|
|
)
|
|
|
|
is_active = models.BooleanField(
|
|
default=True,
|
|
verbose_name='Активна',
|
|
help_text='Включить/выключить синхронизацию для этой команды'
|
|
)
|
|
|
|
notes = models.TextField(
|
|
blank=True,
|
|
verbose_name='Примечания',
|
|
help_text='Дополнительная информация о команде'
|
|
)
|
|
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
db_table = 'mattermost_team_sync_settings'
|
|
verbose_name = 'Настройки синхронизации команды'
|
|
verbose_name_plural = 'Настройки синхронизации команд'
|
|
ordering = ['-priority', 'team__display_name']
|
|
|
|
def __str__(self):
|
|
status = "✓" if self.is_active else "✗"
|
|
default = " (по умолчанию)" if self.is_default else ""
|
|
return f"{status} {self.team.display_name}{default}"
|
|
|
|
def save(self, *args, **kwargs):
|
|
# Если эта команда устанавливается как default, снимаем флаг с других
|
|
if self.is_default:
|
|
TeamSyncSettings.objects.filter(is_default=True).exclude(pk=self.pk).update(is_default=False)
|
|
super().save(*args, **kwargs)
|
|
|
|
|
|
class ChannelSyncSettings(models.Model):
|
|
"""Настройки синхронизации каналов Mattermost"""
|
|
channel = models.OneToOneField(
|
|
MattermostChannel,
|
|
on_delete=models.CASCADE,
|
|
related_name='sync_settings',
|
|
verbose_name='Канал Mattermost'
|
|
)
|
|
|
|
auto_add_users = models.BooleanField(
|
|
default=True,
|
|
verbose_name='Автоматически добавлять пользователей',
|
|
help_text='Автоматически добавлять новых пользователей в этот канал при синхронизации'
|
|
)
|
|
|
|
priority = models.IntegerField(
|
|
default=0,
|
|
verbose_name='Приоритет',
|
|
help_text='Порядок добавления пользователей (выше = раньше)'
|
|
)
|
|
|
|
filter_by_department = models.BooleanField(
|
|
default=False,
|
|
verbose_name='Фильтровать по отделам',
|
|
help_text='Добавлять только пользователей из указанных отделов'
|
|
)
|
|
|
|
departments = models.ManyToManyField(
|
|
'core.Directory',
|
|
blank=True,
|
|
limit_choices_to={'directory_type__code': 'department', 'is_active': True},
|
|
verbose_name='Отделы',
|
|
help_text='Отделы, пользователи которых будут добавлены в канал'
|
|
)
|
|
|
|
filter_by_location_type = models.BooleanField(
|
|
default=False,
|
|
verbose_name='Фильтровать по типу локации',
|
|
help_text='Добавлять только пользователей с выбранным типом локации (Аптека/Склад/Офис)'
|
|
)
|
|
|
|
location_types = ArrayField(
|
|
models.CharField(max_length=20, choices=LOCATION_TYPE_CHOICES),
|
|
blank=True, default=list,
|
|
verbose_name='Типы локаций',
|
|
help_text='Пользователи с этими типами локации будут добавлены в канал'
|
|
)
|
|
|
|
is_active = models.BooleanField(
|
|
default=True,
|
|
verbose_name='Активна',
|
|
help_text='Включить/выключить синхронизацию для этого канала'
|
|
)
|
|
|
|
notes = models.TextField(
|
|
blank=True,
|
|
verbose_name='Примечания',
|
|
help_text='Дополнительная информация о канале'
|
|
)
|
|
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
db_table = 'mattermost_channel_sync_settings'
|
|
verbose_name = 'Настройки синхронизации канала'
|
|
verbose_name_plural = 'Настройки синхронизации каналов'
|
|
ordering = ['-priority', 'channel__display_name']
|
|
|
|
def __str__(self):
|
|
status = "✓" if self.is_active else "✗"
|
|
return f"{status} {self.channel.display_name} ({self.channel.team.display_name})" |