mattermost: возможности бота галочками вместо одной роли
Бот больше не привязан к единственной role (unique). Вместо неё три флага- галочки: can_provisioning / can_channels / can_tasks — можно совмещать. get_active(role) ищет активного бота с соответствующей галочкой. - модель: убран role, добавлены 3 boolean, get_active/caps по флагам; - миграция 0006: перенос старой role в галочки, затем удаление role; - админка: чекбоксы «Возможности бота»; - команда create_mattermost_bot: --role включает нужную галочку (username-based upsert). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -61,11 +61,22 @@ class ChannelSyncSettingsForm(forms.ModelForm):
|
||||
|
||||
@admin.register(MattermostBot)
|
||||
class MattermostBotAdmin(ModelAdmin):
|
||||
"""Боты Mattermost по ролям. Токен хранится в БД зашифрованным."""
|
||||
list_display = ('display_name', 'role', 'username', 'user_id', 'is_active', 'updated_at')
|
||||
list_filter = ('role', 'is_active')
|
||||
"""Боты Mattermost. Возможности выдаются галочками (провижинг/каналы/задачи).
|
||||
Токен хранится в БД зашифрованным."""
|
||||
list_display = ('display_name', 'caps_display', 'username', 'user_id', 'is_active', 'updated_at')
|
||||
list_filter = ('is_active', 'can_provisioning', 'can_channels', 'can_tasks')
|
||||
search_fields = ('display_name', 'username', 'user_id')
|
||||
fields = ('role', 'display_name', 'username', 'user_id', 'token', 'is_active')
|
||||
fieldsets = (
|
||||
(None, {'fields': ('display_name', 'username', 'user_id', 'token', 'is_active')}),
|
||||
('Возможности бота', {
|
||||
'fields': ('can_provisioning', 'can_channels', 'can_tasks'),
|
||||
'description': 'Отметьте, что этот бот умеет делать. Один бот может совмещать несколько ролей.',
|
||||
}),
|
||||
)
|
||||
|
||||
@display(description='Возможности')
|
||||
def caps_display(self, obj):
|
||||
return ', '.join(obj.caps()) or '—'
|
||||
|
||||
|
||||
@admin.register(MattermostTeam)
|
||||
|
||||
@@ -10,7 +10,7 @@ class Command(BaseCommand):
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--role', default='tasks',
|
||||
choices=['provisioning', 'channels', 'tasks'],
|
||||
help='Роль бота (MattermostBot.role)')
|
||||
help='Возможность бота (галочка), которую активировать')
|
||||
parser.add_argument('--username', default='acore-portal-bot')
|
||||
parser.add_argument('--display-name', default='ACORE Портал')
|
||||
parser.add_argument('--force', action='store_true', help='Пересоздать токен, даже если бот уже настроен')
|
||||
@@ -28,18 +28,22 @@ class Command(BaseCommand):
|
||||
raise CommandError('Коннектор Mattermost не настроен или отключён')
|
||||
|
||||
role = options['role']
|
||||
cap_field = MattermostBot.CAP_FIELD[role]
|
||||
|
||||
def _upsert(user_id, token):
|
||||
MattermostBot.objects.update_or_create(
|
||||
role=role,
|
||||
bot, _ = MattermostBot.objects.update_or_create(
|
||||
username=options['username'],
|
||||
defaults={
|
||||
'username': options['username'],
|
||||
'display_name': options['display_name'],
|
||||
'user_id': user_id,
|
||||
'token': token,
|
||||
'is_active': True,
|
||||
},
|
||||
)
|
||||
# Включаем нужную возможность-галочку, не сбрасывая уже выданные.
|
||||
if not getattr(bot, cap_field):
|
||||
setattr(bot, cap_field, True)
|
||||
bot.save(update_fields=[cap_field])
|
||||
|
||||
if options['set_token']:
|
||||
# Провижининг-бот уже должен существовать, чтобы найти нового по username.
|
||||
@@ -50,7 +54,7 @@ class Command(BaseCommand):
|
||||
bot_user = service.driver.users.get_user_by_username(options['username'])
|
||||
_upsert(bot_user['id'], options['set_token'])
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
f"Токен бота {options['username']} (id={bot_user['id']}) сохранён как MattermostBot(role={role})"
|
||||
f"Токен бота {options['username']} (id={bot_user['id']}) сохранён; включена возможность «{role}»"
|
||||
))
|
||||
return
|
||||
|
||||
@@ -84,4 +88,4 @@ class Command(BaseCommand):
|
||||
token = driver.users.create_user_access_token(bot['user_id'], description='ACORE portal notifications')
|
||||
_upsert(bot['user_id'], token['token'])
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(f'Токен бота сохранён как MattermostBot(role={role})'))
|
||||
self.stdout.write(self.style.SUCCESS(f'Токен бота сохранён; включена возможность «{role}»'))
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def role_to_caps(apps, schema_editor):
|
||||
Bot = apps.get_model('mattermost_connector', 'MattermostBot')
|
||||
for bot in Bot.objects.all():
|
||||
if bot.role == 'provisioning':
|
||||
bot.can_provisioning = True
|
||||
elif bot.role == 'channels':
|
||||
bot.can_channels = True
|
||||
elif bot.role == 'tasks':
|
||||
bot.can_tasks = True
|
||||
bot.save(update_fields=['can_provisioning', 'can_channels', 'can_tasks'])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mattermost_connector', '0005_mattermostsynclog'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='mattermostbot',
|
||||
name='can_provisioning',
|
||||
field=models.BooleanField(default=False, verbose_name='Провижининг (сотрудники)'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='mattermostbot',
|
||||
name='can_channels',
|
||||
field=models.BooleanField(default=False, verbose_name='Каналы'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='mattermostbot',
|
||||
name='can_tasks',
|
||||
field=models.BooleanField(default=False, verbose_name='Задачи / уведомления портала'),
|
||||
),
|
||||
migrations.RunPython(role_to_caps, migrations.RunPython.noop),
|
||||
migrations.AlterModelOptions(
|
||||
name='mattermostbot',
|
||||
options={
|
||||
'ordering': ['display_name', 'username'],
|
||||
'verbose_name': 'Mattermost-бот',
|
||||
'verbose_name_plural': 'Mattermost-боты',
|
||||
},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='mattermostbot',
|
||||
name='role',
|
||||
),
|
||||
]
|
||||
@@ -18,10 +18,9 @@ User = get_user_model()
|
||||
|
||||
|
||||
class MattermostBot(models.Model):
|
||||
"""Отдельный Mattermost-бот на роль. Каждая интеграционная операция берёт
|
||||
бота по своей роли, а не общий сервисный токен. Токен хранится в БД
|
||||
зашифрованным (EncryptedCharField). Имя бота (display_name/username) —
|
||||
только метка: сменить тему/переименовать можно, не трогая код."""
|
||||
"""Mattermost-бот с набором возможностей (галочки): провижининг сотрудников,
|
||||
каналы, задачи/уведомления. Один бот может совмещать несколько ролей.
|
||||
Токен хранится зашифрованным (EncryptedCharField)."""
|
||||
|
||||
ROLE_PROVISIONING = 'provisioning'
|
||||
ROLE_CHANNELS = 'channels'
|
||||
@@ -31,13 +30,21 @@ class MattermostBot(models.Model):
|
||||
(ROLE_CHANNELS, 'Каналы'),
|
||||
(ROLE_TASKS, 'Задачи / уведомления портала'),
|
||||
]
|
||||
# Возможность (ключ get_active) → поле-галочка.
|
||||
CAP_FIELD = {
|
||||
ROLE_PROVISIONING: 'can_provisioning',
|
||||
ROLE_CHANNELS: 'can_channels',
|
||||
ROLE_TASKS: 'can_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)
|
||||
can_provisioning = models.BooleanField('Провижининг (сотрудники)', default=False)
|
||||
can_channels = models.BooleanField('Каналы', default=False)
|
||||
can_tasks = models.BooleanField('Задачи / уведомления портала', default=False)
|
||||
is_active = models.BooleanField('Активен', default=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
@@ -46,16 +53,23 @@ class MattermostBot(models.Model):
|
||||
db_table = 'mattermost_bots'
|
||||
verbose_name = 'Mattermost-бот'
|
||||
verbose_name_plural = 'Mattermost-боты'
|
||||
ordering = ['role']
|
||||
ordering = ['display_name', 'username']
|
||||
|
||||
def caps(self) -> list:
|
||||
"""Список названий включённых возможностей."""
|
||||
return [lbl for key, lbl in self.ROLE_CHOICES if getattr(self, self.CAP_FIELD[key])]
|
||||
|
||||
def __str__(self):
|
||||
label = self.display_name or self.username or self.get_role_display()
|
||||
return f'{label} ({self.get_role_display()})'
|
||||
label = self.display_name or self.username or 'Mattermost-бот'
|
||||
return f'{label} ({", ".join(self.caps()) or "без ролей"})'
|
||||
|
||||
@classmethod
|
||||
def get_active(cls, role: str) -> 'MattermostBot | None':
|
||||
"""Активный бот для роли или None."""
|
||||
return cls.objects.filter(role=role, is_active=True).first()
|
||||
"""Активный бот с включённой возможностью role (provisioning/channels/tasks)."""
|
||||
field = cls.CAP_FIELD.get(role)
|
||||
if not field:
|
||||
return None
|
||||
return cls.objects.filter(is_active=True, **{field: True}).order_by('id').first()
|
||||
|
||||
class MattermostUser(models.Model):
|
||||
"""Связь между Django пользователем и Mattermost пользователем"""
|
||||
@@ -306,34 +320,34 @@ class ChannelSyncSettings(models.Model):
|
||||
|
||||
def __str__(self):
|
||||
status = "✓" if self.is_active else "✗"
|
||||
return f"{status} {self.channel.display_name} ({self.channel.team.display_name})"
|
||||
|
||||
class MattermostSyncLog(BaseModel):
|
||||
"""Журнал синхронизации сотрудников с Mattermost (по образцу AD/Carbonio/Nextcloud)."""
|
||||
employee = models.ForeignKey(
|
||||
'employees.Employee', on_delete=models.CASCADE,
|
||||
related_name='mattermost_sync_logs', verbose_name='Сотрудник',
|
||||
)
|
||||
action = models.CharField(
|
||||
'Действие', max_length=20,
|
||||
choices=[
|
||||
('create', 'Создание пользователя'),
|
||||
('add_team', 'Добавление в команду'),
|
||||
('add_channel', 'Добавление в канал'),
|
||||
('update', 'Обновление'),
|
||||
('deactivate', 'Отключение'),
|
||||
],
|
||||
)
|
||||
status = models.CharField(
|
||||
'Статус', max_length=10,
|
||||
choices=[('success', 'Успех'), ('error', 'Ошибка')],
|
||||
)
|
||||
message = models.TextField('Сообщение', blank=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Запись синхронизации Mattermost'
|
||||
verbose_name_plural = 'Журнал синхронизации Mattermost'
|
||||
ordering = ['-created_at']
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.employee} — {self.get_action_display()} — {self.get_status_display()}'
|
||||
return f"{status} {self.channel.display_name} ({self.channel.team.display_name})"
|
||||
|
||||
class MattermostSyncLog(BaseModel):
|
||||
"""Журнал синхронизации сотрудников с Mattermost (по образцу AD/Carbonio/Nextcloud)."""
|
||||
employee = models.ForeignKey(
|
||||
'employees.Employee', on_delete=models.CASCADE,
|
||||
related_name='mattermost_sync_logs', verbose_name='Сотрудник',
|
||||
)
|
||||
action = models.CharField(
|
||||
'Действие', max_length=20,
|
||||
choices=[
|
||||
('create', 'Создание пользователя'),
|
||||
('add_team', 'Добавление в команду'),
|
||||
('add_channel', 'Добавление в канал'),
|
||||
('update', 'Обновление'),
|
||||
('deactivate', 'Отключение'),
|
||||
],
|
||||
)
|
||||
status = models.CharField(
|
||||
'Статус', max_length=10,
|
||||
choices=[('success', 'Успех'), ('error', 'Ошибка')],
|
||||
)
|
||||
message = models.TextField('Сообщение', blank=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Запись синхронизации Mattermost'
|
||||
verbose_name_plural = 'Журнал синхронизации Mattermost'
|
||||
ordering = ['-created_at']
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.employee} — {self.get_action_display()} — {self.get_status_display()}'
|
||||
|
||||
Reference in New Issue
Block a user