Files
ACORE/apps/mattermost_connector/models.py
T
lokses 704d5d9538 mattermost: синхронизация команд/каналов по типу локации
Добавлен фильтр по 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>
2026-09-16 16:17:16 +03:00

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