diff --git a/acore/settings/base.py b/acore/settings/base.py index 868b64f..292ae2c 100644 --- a/acore/settings/base.py +++ b/acore/settings/base.py @@ -598,6 +598,11 @@ UNFOLD = { "icon": "cloud", "link": reverse_lazy("admin:nextcloud_connector_nextcloudsynclog_changelist"), }, + { + "title": "Журнал Mattermost", + "icon": "forum", + "link": reverse_lazy("admin:mattermost_connector_mattermostsynclog_changelist"), + }, ], }, ], diff --git a/apps/mattermost_connector/admin.py b/apps/mattermost_connector/admin.py index 51475da..5ecd865 100755 --- a/apps/mattermost_connector/admin.py +++ b/apps/mattermost_connector/admin.py @@ -13,9 +13,30 @@ from .models import ( TeamSyncSettings, ChannelSyncSettings, MattermostBot, + MattermostSyncLog, ) +@admin.register(MattermostSyncLog) +class MattermostSyncLogAdmin(ModelAdmin): + """Журнал синхронизации Mattermost — только просмотр (по образцу AD).""" + list_display = ('employee', 'action', 'status', 'created_at') + list_filter = ('action', 'status') + search_fields = ('employee__username', 'employee__last_name', 'employee__first_name', 'message') + date_hierarchy = 'created_at' + actions = None + + def has_add_permission(self, request): + return False + + def has_change_permission(self, request, obj=None): + return False + + def has_delete_permission(self, request, obj=None): + # Разрешаем CASCADE при удалении сотрудника; UI журнала без delete_selected. + return request.user.has_perm('employees.delete_employee') + + class TeamSyncSettingsForm(forms.ModelForm): location_types = forms.MultipleChoiceField( choices=LOCATION_TYPE_CHOICES, required=False, diff --git a/apps/mattermost_connector/migrations/0005_mattermostsynclog.py b/apps/mattermost_connector/migrations/0005_mattermostsynclog.py new file mode 100644 index 0000000..74b2f17 --- /dev/null +++ b/apps/mattermost_connector/migrations/0005_mattermostsynclog.py @@ -0,0 +1,35 @@ +# Generated by Django 6.0.3 on 2026-09-16 13:39 + +import django.db.models.deletion +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mattermost_connector', '0004_channelsyncsettings_filter_by_location_type_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='MattermostSyncLog', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('action', models.CharField(choices=[('create', 'Создание пользователя'), ('add_team', 'Добавление в команду'), ('add_channel', 'Добавление в канал'), ('update', 'Обновление'), ('deactivate', 'Отключение')], max_length=20, verbose_name='Действие')), + ('status', models.CharField(choices=[('success', 'Успех'), ('error', 'Ошибка')], max_length=10, verbose_name='Статус')), + ('message', models.TextField(blank=True, verbose_name='Сообщение')), + ('employee', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='mattermost_sync_logs', to=settings.AUTH_USER_MODEL, verbose_name='Сотрудник')), + ], + options={ + 'verbose_name': 'Запись синхронизации Mattermost', + 'verbose_name_plural': 'Журнал синхронизации Mattermost', + 'ordering': ['-created_at'], + }, + ), + ] diff --git a/apps/mattermost_connector/models.py b/apps/mattermost_connector/models.py index b098b46..37c86da 100755 --- a/apps/mattermost_connector/models.py +++ b/apps/mattermost_connector/models.py @@ -4,6 +4,7 @@ from django.db import models from django.contrib.auth import get_user_model from django.utils import timezone +from core.models import BaseModel from core.models.fields import EncryptedCharField # Типы локаций сотрудника (Employee.LOCATION_TYPES) — для фильтра синхронизации. @@ -305,4 +306,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})" \ No newline at end of file + 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()}' diff --git a/apps/mattermost_connector/services/sync_service.py b/apps/mattermost_connector/services/sync_service.py index ff5d369..86505c4 100755 --- a/apps/mattermost_connector/services/sync_service.py +++ b/apps/mattermost_connector/services/sync_service.py @@ -328,7 +328,7 @@ def add_user_to_teams(employee: Employee) -> bool: Returns: bool: True если успешно добавлен хотя бы в одну команду """ - from apps.mattermost_connector.models import TeamSyncSettings, MattermostTeamMembership, MattermostUser + from apps.mattermost_connector.models import TeamSyncSettings, MattermostTeamMembership, MattermostUser, MattermostSyncLog if not employee.mattermost_user_id: logger.warning(f"Пользователь {employee.email} не имеет mattermost_user_id") @@ -386,9 +386,17 @@ def add_user_to_teams(employee: Employee) -> bool: added_to_teams += 1 logger.info(f"Пользователь {employee.email} добавлен в команду {setting.team.display_name}") + MattermostSyncLog.objects.create( + employee=employee, action='add_team', status='success', + message=f'Команда: {setting.team.display_name}', + ) except Exception as e: logger.error(f"Ошибка добавления {employee.email} в команду {setting.team.display_name}: {e}") + MattermostSyncLog.objects.create( + employee=employee, action='add_team', status='error', + message=f'{setting.team.display_name}: {e}', + ) else: logger.debug(f"Пользователь {employee.email} уже в команде {setting.team.display_name}") @@ -414,7 +422,7 @@ def add_user_to_channels(employee: Employee) -> bool: Returns: bool: True если успешно добавлен хотя бы в один канал """ - from apps.mattermost_connector.models import ChannelSyncSettings, MattermostChannelMembership, MattermostUser + from apps.mattermost_connector.models import ChannelSyncSettings, MattermostChannelMembership, MattermostUser, MattermostSyncLog if not employee.mattermost_user_id: logger.warning(f"Пользователь {employee.email} не имеет mattermost_user_id") @@ -471,9 +479,17 @@ def add_user_to_channels(employee: Employee) -> bool: added_to_channels += 1 logger.info(f"Пользователь {employee.email} добавлен в канал {setting.channel.display_name}") + MattermostSyncLog.objects.create( + employee=employee, action='add_channel', status='success', + message=f'Канал: {setting.channel.display_name}', + ) except Exception as e: logger.error(f"Ошибка добавления {employee.email} в канал {setting.channel.display_name}: {e}") + MattermostSyncLog.objects.create( + employee=employee, action='add_channel', status='error', + message=f'{setting.channel.display_name}: {e}', + ) else: logger.debug(f"Пользователь {employee.email} уже в канале {setting.channel.display_name}")