Игорь Надеин 30e6643b6e Исправлена админка для редактирования дат created_at/updated_at
- Добавлены явные поля в EmployeeChangeForm для дат с виджетами datetime-local
- Переопределен метод save_model для обхода auto_now/auto_now_add
- Используется прямое обновление через queryset для изменения неизменяемых полей
- Только superuser может редактировать эти поля
2025-11-20 12:57:33 +03:00

342 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# backend/employees/admin.py
from django import forms
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.forms import ReadOnlyPasswordHashField, AdminPasswordChangeForm
from django.contrib.auth.models import Group, Permission
from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin
from simple_history.admin import SimpleHistoryAdmin
from .models import (
Employee,
EmployeeAction,
Department,
DepartmentRole,
EmployeeDepartment,
Position,
Skill,
)
# =========================
# Формы для кастомного User (Employee)
# =========================
class EmployeeCreationForm(forms.ModelForm):
"""
Создание сотрудника в админке: логин — email, без username.
"""
password1 = forms.CharField(label="Пароль", widget=forms.PasswordInput)
password2 = forms.CharField(label="Подтверждение пароля", widget=forms.PasswordInput)
class Meta:
model = Employee
fields = ("email", "first_name", "last_name", "phone_number")
def clean_password2(self):
p1 = self.cleaned_data.get("password1") or ""
p2 = self.cleaned_data.get("password2") or ""
if not p1:
raise forms.ValidationError("Пароль обязателен")
if p1 != p2:
raise forms.ValidationError("Пароли не совпадают")
return p2
def save(self, commit=True):
user = super().save(commit=False)
user.set_password(self.cleaned_data["password1"])
# При создании через админку обычно активируем и помечаем e-mail подтверждённым
if not user.is_active:
user.is_active = True
if not user.email_verified:
user.email_verified = True
if commit:
user.save()
return user
class EmployeeChangeForm(forms.ModelForm):
"""
Редактирование сотрудника: пароль только для чтения (смена пароля — отдельной кнопкой).
"""
password = ReadOnlyPasswordHashField(
label="Хэш пароля",
help_text=(
"Пароль хранится в виде хэша — посмотреть его невозможно. "
"Используйте кнопку «Изменить пароль» выше."
),
required=False,
)
# Добавляем явные поля для дат, чтобы superuser мог их редактировать
created_at = forms.DateTimeField(
label="Дата создания",
required=False,
widget=forms.DateTimeInput(attrs={'type': 'datetime-local'}),
help_text="Только для superuser"
)
updated_at = forms.DateTimeField(
label="Дата обновления",
required=False,
widget=forms.DateTimeInput(attrs={'type': 'datetime-local'}),
help_text="Только для superuser"
)
class Meta:
model = Employee
fields = (
"email",
"password",
"first_name",
"last_name",
"patronymic",
"gender",
"birth_date",
"avatar",
"phone_number",
"telegram",
"whatsapp",
"wechat",
"position",
"skills",
"is_active",
"email_verified",
"is_staff",
"is_superuser",
"groups",
"user_permissions",
"last_login",
"created_at",
"updated_at",
)
def clean_password(self):
# Возвращаем исходное значение (хэш), даже если пользователь ничего не ввёл
return self.initial.get("password")
def save(self, commit=True):
user = super().save(commit=False)
# Если superuser изменил даты, применяем их
if self.cleaned_data.get('created_at'):
user.created_at = self.cleaned_data['created_at']
if self.cleaned_data.get('updated_at'):
user.updated_at = self.cleaned_data['updated_at']
if commit:
user.save()
return user
# =========================
# Инлайны
# =========================
class EmployeeDepartmentInline(admin.TabularInline):
"""
Принадлежности сотрудника к отделам (видно из карточки сотрудника).
"""
model = EmployeeDepartment
fk_name = "employee"
extra = 0
autocomplete_fields = ("department", "role")
fields = ("department", "role", "is_active", "date_from", "date_to")
show_change_link = True
class DepartmentMembershipInline(admin.TabularInline):
"""
Состав отдела — список участников прямо в карточке отдела.
"""
model = EmployeeDepartment
fk_name = "department"
extra = 0
autocomplete_fields = ("employee", "role")
fields = ("employee", "role", "is_active", "date_from", "date_to")
show_change_link = True
# =========================
# Админки моделей
# =========================
@admin.register(Employee)
class EmployeeAdmin(DjangoUserAdmin):
"""
Кастомный UserAdmin для модели Employee.
Включает стандартную кнопку «Изменить пароль».
"""
add_form = EmployeeCreationForm
form = EmployeeChangeForm
model = Employee
# Включаем стандартную форму смены пароля и шаблон,
# который показывает ссылку «Изменить пароль» на форме изменения пользователя.
change_password_form = AdminPasswordChangeForm
change_form_template = "admin/employees/employee/change_form.html"
list_display = (
"email",
"last_name",
"first_name",
"phone_number",
"position",
"is_active",
"email_verified",
"is_staff",
)
list_filter = ("is_active", "email_verified", "is_staff", "is_superuser", "position", "groups")
search_fields = ("email", "first_name", "last_name", "phone_number")
ordering = ("last_name", "first_name")
list_select_related = ("position",)
filter_horizontal = ("groups", "user_permissions", "skills")
def get_readonly_fields(self, request, obj=None):
"""
Только superuser может редактировать даты создания/обновления.
"""
if request.user.is_superuser:
return ("last_login",)
return ("last_login", "created_at", "updated_at")
fieldsets = (
(None, {"fields": ("email", "password")}),
("Личная информация", {
"fields": (
"first_name",
"last_name",
"patronymic",
"gender",
"birth_date",
"avatar",
)
}),
("Контакты", {
"fields": ("phone_number", "telegram", "whatsapp", "wechat")
}),
("Работа", {
"fields": ("position", "skills")
}),
("Права и статусы", {
"fields": (
"is_active",
"email_verified",
"is_staff",
"is_superuser",
"groups",
"user_permissions",
)
}),
("Служебное", {"fields": ("last_login", "created_at", "updated_at")}),
)
add_fieldsets = (
(None, {
"classes": ("wide",),
"fields": (
"email",
"first_name",
"last_name",
"phone_number",
"whatsapp",
"position",
"password1",
"password2",
"is_active",
"email_verified",
"is_staff",
"is_superuser",
"groups",
"user_permissions",
),
}),
)
inlines = [EmployeeDepartmentInline,]
def save_model(self, request, obj, form, change):
"""
Переопределяем сохранение, чтобы superuser мог менять created_at/updated_at.
"""
# Если superuser изменил даты, временно отключаем auto_now
if request.user.is_superuser:
if 'created_at' in form.changed_data or 'updated_at' in form.changed_data:
# Сохраняем с update_fields, чтобы обойти auto_now
update_fields = []
if 'created_at' in form.changed_data:
obj.created_at = form.cleaned_data['created_at']
update_fields.append('created_at')
if 'updated_at' in form.changed_data:
obj.updated_at = form.cleaned_data['updated_at']
update_fields.append('updated_at')
# Сначала сохраняем объект без дат
super().save_model(request, obj, form, change)
# Потом обновляем даты напрямую через queryset
if update_fields:
Employee.objects.filter(pk=obj.pk).update(
**{field: getattr(obj, field) for field in update_fields}
)
return
super().save_model(request, obj, form, change)
@admin.register(Position)
class PositionAdmin(SimpleHistoryAdmin):
list_display = ("name", "description")
search_fields = ("name", "description")
filter_horizontal = ("groups",)
@admin.register(EmployeeAction)
class EmployeeActionAdmin(SimpleHistoryAdmin):
list_display = ("employee", "action", "date")
list_filter = ("action",)
search_fields = ("employee__first_name", "employee__last_name", "comment")
autocomplete_fields = ("employee",)
date_hierarchy = "date"
@admin.register(Department)
class DepartmentAdmin(admin.ModelAdmin):
list_display = ("name", "head", "created_at")
list_filter = ("head",)
search_fields = ("name",)
autocomplete_fields = ("head",)
readonly_fields = ("created_at", "head_appointed_at")
inlines = [DepartmentMembershipInline]
@admin.register(DepartmentRole)
class DepartmentRoleAdmin(admin.ModelAdmin):
list_display = ("name", "department")
list_filter = ("department",)
search_fields = ("name", "department__name")
autocomplete_fields = ("department",)
filter_horizontal = ("scoped_permissions",)
@admin.register(EmployeeDepartment)
class EmployeeDepartmentAdmin(admin.ModelAdmin):
list_display = ("employee", "department", "role", "is_active", "date_from", "date_to")
list_filter = ("is_active", "department", "role")
search_fields = ("employee__first_name", "employee__last_name", "department__name")
autocomplete_fields = ("employee", "department", "role")
date_hierarchy = "date_from"
@admin.register(Skill)
class SkillAdmin(admin.ModelAdmin):
list_display = ("name",)
search_fields = ("name",)
# Полезно иметь Permission в админке
@admin.register(Permission)
class PermissionAdmin(admin.ModelAdmin):
list_display = ("name", "codename", "content_type")
list_filter = ("content_type",)
search_fields = ("name", "codename")