Replace role field with auth.Group membership

- Remove Employee.role CharField — was a display-only label with no
  actual permission logic
- 3 migrations: create groups, migrate data, remove field
- Admin: list/filter/fieldset now use groups; added groups_display method
- Serializer: role → groups (SlugRelatedField returning group names)
- Template: get_role_display → groups.first.name
- EmployeePermissionOverride and PermissionGroup remain for fine-grained
  per-employee and per-group permission management
This commit is contained in:
2026-05-31 15:04:31 +03:00
parent 1f549daae3
commit 27a917e8c3
7 changed files with 138 additions and 20 deletions
+10 -3
View File
@@ -105,18 +105,18 @@ class EmployeeAdmin(ModelAdmin):
js = ('assets/js/admin_phone_format.js',)
list_display = [
'photo_thumbnail', 'username', 'last_name', 'first_name', 'role',
'photo_thumbnail', 'username', 'last_name', 'first_name', 'groups_display',
'user_location_type', 'user_pharmacy', 'department_display', 'post',
'is_active', 'hire_date'
]
list_filter = ['role', 'is_active', 'is_staff', 'user_location_type', 'department']
list_filter = ['groups', 'is_active', 'is_staff', 'user_location_type', 'department']
search_fields = ['username', 'email', 'last_name', 'first_name', 'middle_name']
list_select_related = ['user_pharmacy', 'department', 'post']
readonly_fields = ('photo_preview',)
fieldsets = (
(_('Аутентификация'), {
'fields': ('username', 'email', 'role', 'is_staff', 'is_active'),
'fields': ('username', 'email', 'groups', 'is_staff', 'is_active'),
'classes': ('tab',),
}),
(_('ФИО'), {
@@ -203,6 +203,13 @@ class EmployeeAdmin(ModelAdmin):
return mark_safe('<span style="color:#999;">Фото не загружено</span>')
photo_preview.short_description = _('Предпросмотр')
def groups_display(self, obj):
names = list(obj.groups.values_list('name', flat=True))
if not names:
return mark_safe('<span style="color:#999;">—</span>')
return ', '.join(names)
groups_display.short_description = _('Группы')
def department_display(self, obj): # noqa: E303
"""Отдел с полным путём иерархии без дубликатов (Офис → HR → Кадры)."""
if not obj.department:
@@ -0,0 +1,44 @@
"""
Data migration: создаём auth.Group + PermissionGroup для каждой роли.
"""
from django.db import migrations
ROLE_GROUPS = [
('admin', 'Администратор сети'),
('warehouse', 'Сотрудник склада'),
('pharmacist', 'Фармацевт'),
('manager', 'Заведующий аптекой'),
('revisor', 'Ревизор'),
('office', 'Офисный сотрудник'),
]
def create_groups(apps, schema_editor):
Group = apps.get_model('auth', 'Group')
PermissionGroup = apps.get_model('employees', 'PermissionGroup')
for code, name in ROLE_GROUPS:
group, _ = Group.objects.get_or_create(name=name)
PermissionGroup.objects.get_or_create(
group=group,
defaults={'description': f'Группа для роли: {name}', 'is_active': True},
)
def delete_groups(apps, schema_editor):
Group = apps.get_model('auth', 'Group')
names = [name for _, name in ROLE_GROUPS]
Group.objects.filter(name__in=names).delete()
class Migration(migrations.Migration):
dependencies = [
('employees', '0003_employee_user_location'),
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.RunPython(create_groups, delete_groups),
]
@@ -0,0 +1,56 @@
"""
Data migration: переносим employee.role → employee.groups.
Каждый сотрудник добавляется в группу соответствующую его текущей роли.
"""
from django.db import migrations
ROLE_TO_GROUP = {
'admin': 'Администратор сети',
'warehouse': 'Сотрудник склада',
'pharmacist': 'Фармацевт',
'manager': 'Заведующий аптекой',
'revisor': 'Ревизор',
'office': 'Офисный сотрудник',
}
def role_to_groups(apps, schema_editor):
Employee = apps.get_model('employees', 'Employee')
Group = apps.get_model('auth', 'Group')
group_cache = {
code: Group.objects.get(name=name)
for code, name in ROLE_TO_GROUP.items()
}
for employee in Employee.objects.exclude(role='').exclude(role__isnull=True):
group = group_cache.get(employee.role)
if group:
employee.groups.add(group)
def groups_to_role(apps, schema_editor):
Employee = apps.get_model('employees', 'Employee')
Group = apps.get_model('auth', 'Group')
name_to_role = {name: code for code, name in ROLE_TO_GROUP.items()}
for employee in Employee.objects.all():
for group in employee.groups.all():
role = name_to_role.get(group.name)
if role:
employee.role = role
employee.save(update_fields=['role'])
break
class Migration(migrations.Migration):
dependencies = [
('employees', '0004_create_role_groups'),
]
operations = [
migrations.RunPython(role_to_groups, groups_to_role),
]
@@ -0,0 +1,22 @@
"""
Schema migration: удаляем поле role и его индекс из модели Employee.
"""
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('employees', '0005_migrate_role_to_groups'),
]
operations = [
migrations.RemoveIndex(
model_name='employee',
name='employees_e_role_caa0a5_idx',
),
migrations.RemoveField(
model_name='employee',
name='role',
),
]
+1 -15
View File
@@ -28,15 +28,6 @@ class EmployeeManager(BaseUserManager):
class Employee(AbstractBaseUser, PermissionsMixin, BaseModel):
ROLES = [
('admin', _('Администратор сети')),
('warehouse', _('Сотрудник склада')),
('pharmacist', _('Фармацевт')),
('manager', _('Заведующий аптекой')),
('revisor', _('Ревизор')),
('office', _('Офисный сотрудник')),
]
username = models.CharField(
_('Логин'), max_length=150, unique=True,
help_text=_('Уникальный логин для входа в систему')
@@ -45,10 +36,6 @@ class Employee(AbstractBaseUser, PermissionsMixin, BaseModel):
_('Email адрес'), unique=True, blank=True, null=True,
help_text=_('Рабочий email. Если не указан — генерируется из username@local.domain')
)
role = models.CharField(
_('Роль'), max_length=20, choices=ROLES,
help_text=_('Роль определяет права доступа в API')
)
last_name = models.CharField(_('Фамилия'), max_length=150)
first_name = models.CharField(_('Имя'), max_length=150)
@@ -177,7 +164,7 @@ class Employee(AbstractBaseUser, PermissionsMixin, BaseModel):
objects = EmployeeManager()
USERNAME_FIELD = 'username'
REQUIRED_FIELDS = ['first_name', 'last_name', 'hire_date', 'role']
REQUIRED_FIELDS = ['first_name', 'last_name', 'hire_date']
class Meta:
verbose_name = _('Сотрудник')
@@ -186,7 +173,6 @@ class Employee(AbstractBaseUser, PermissionsMixin, BaseModel):
models.Index(fields=['last_name', 'first_name', 'middle_name']),
models.Index(fields=['email']),
models.Index(fields=['is_active']),
models.Index(fields=['role']),
]
def __str__(self):
+4 -1
View File
@@ -1,12 +1,15 @@
from rest_framework import serializers
from apps.employees.models import Employee
class EmployeeSerializer(serializers.ModelSerializer):
groups = serializers.SlugRelatedField(many=True, read_only=True, slug_field='name')
class Meta:
model = Employee
fields = [
'id', 'username', 'first_name', 'last_name', 'middle_name', 'email',
'role', 'phone', 'personal_phone', 'personal_email', 'inn_code', 'birth_date',
'groups', 'phone', 'personal_phone', 'personal_email', 'inn_code', 'birth_date',
'emergency_contact_name', 'emergency_contact_phone', 'emergency_contact_relation',
'user_location_type', 'user_organization', 'photo',
'mattermost_username', 'mattermost_sync_enabled',
+1 -1
View File
@@ -125,7 +125,7 @@
{% if employee.post %}
{{ employee.post.name }}
{% else %}
{{ employee.get_role_display }}
{{ employee.groups.first.name|default:"-" }}
{% endif %}
</td>
<td style="padding:1rem; vertical-align:middle; color:var(--color-muted-foreground);">