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:
+10
-3
@@ -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',
|
||||
),
|
||||
]
|
||||
@@ -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):
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);">
|
||||
|
||||
Reference in New Issue
Block a user