FIX PAGES AND ADD PASSWORD CHANGE
This commit is contained in:
+94
-4
@@ -1,15 +1,19 @@
|
||||
from django import forms
|
||||
from django.contrib import admin
|
||||
from django.contrib import admin, messages
|
||||
from django.contrib.auth import password_validation
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||
from django.utils.html import format_html
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from unfold.admin import ModelAdmin, TabularInline
|
||||
|
||||
from core.models import Directory, PortalAccess
|
||||
from core.security import filter_portal_permissions_for_user, is_sensitive_portal_perm
|
||||
from core.security import filter_portal_permissions_for_user, log_security_event
|
||||
from .models import Employee, PermissionGroup, EmployeeDefaults, EmployeePermissionOverride
|
||||
from .models import PasswordSetupInvite
|
||||
from .services.password_invite import can_offer_ad_password_sync
|
||||
from .models import EmployeeHrRequest, LeaveEntitlement
|
||||
from apps.visit_manager.models import WorkSchedule, AttendanceLog
|
||||
|
||||
@@ -65,6 +69,25 @@ class EmployeeAdminForm(forms.ModelForm):
|
||||
label=_('Локация (офис/склад)'),
|
||||
help_text=_('Заполняется для location_type=office или warehouse'),
|
||||
)
|
||||
new_password = forms.CharField(
|
||||
label=_('Новый пароль'),
|
||||
required=False,
|
||||
strip=False,
|
||||
widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}, render_value=False),
|
||||
help_text=_('Оставьте пустым, чтобы не менять текущий пароль портала.'),
|
||||
)
|
||||
new_password_confirm = forms.CharField(
|
||||
label=_('Подтверждение пароля'),
|
||||
required=False,
|
||||
strip=False,
|
||||
widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}, render_value=False),
|
||||
)
|
||||
sync_ad_password = forms.BooleanField(
|
||||
label=_('Также сменить пароль в Active Directory'),
|
||||
required=False,
|
||||
initial=False,
|
||||
help_text=_('Доступно для офисных сотрудников с привязкой к AD.'),
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -75,10 +98,46 @@ class EmployeeAdminForm(forms.ModelForm):
|
||||
.select_related('location_region')
|
||||
.order_by('location_type', 'address')
|
||||
)
|
||||
offer_ad = bool(self.instance and self.instance.pk and can_offer_ad_password_sync(self.instance))
|
||||
self.fields['sync_ad_password'].disabled = not offer_ad
|
||||
if not offer_ad:
|
||||
self.fields['sync_ad_password'].help_text = _(
|
||||
'Синхронизация с AD недоступна: нужен офисный сотрудник с заполненным DN.'
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
cleaned = super().clean()
|
||||
password = (cleaned.get('new_password') or '')
|
||||
confirm = (cleaned.get('new_password_confirm') or '')
|
||||
if password or confirm:
|
||||
if password != confirm:
|
||||
self.add_error('new_password_confirm', _('Пароли не совпадают.'))
|
||||
elif password:
|
||||
try:
|
||||
password_validation.validate_password(password, self.instance)
|
||||
except DjangoValidationError as exc:
|
||||
self.add_error('new_password', exc)
|
||||
if cleaned.get('sync_ad_password') and password:
|
||||
if not (self.instance and self.instance.pk and can_offer_ad_password_sync(self.instance)):
|
||||
self.add_error(
|
||||
'sync_ad_password',
|
||||
_('Смена пароля в AD недоступна для этого сотрудника.'),
|
||||
)
|
||||
return cleaned
|
||||
|
||||
def save(self, commit=True):
|
||||
obj = super().save(commit=False)
|
||||
password = self.cleaned_data.get('new_password') or ''
|
||||
if password:
|
||||
obj.set_password(password)
|
||||
if commit:
|
||||
obj.save()
|
||||
self.save_m2m()
|
||||
return obj
|
||||
|
||||
class Meta:
|
||||
model = Employee
|
||||
fields = '__all__'
|
||||
exclude = ('password',)
|
||||
|
||||
class WorkScheduleInline(TabularInline):
|
||||
model = WorkSchedule
|
||||
@@ -171,7 +230,10 @@ class EmployeeAdmin(ModelAdmin):
|
||||
|
||||
fieldsets = (
|
||||
(_('Аутентификация'), {
|
||||
'fields': ('username', 'email', 'groups', 'is_active'),
|
||||
'fields': (
|
||||
'username', 'email', 'groups', 'is_active',
|
||||
'new_password', 'new_password_confirm', 'sync_ad_password',
|
||||
),
|
||||
'classes': ('tab',),
|
||||
}),
|
||||
(_('ФИО'), {
|
||||
@@ -246,6 +308,34 @@ class EmployeeAdmin(ModelAdmin):
|
||||
if previous:
|
||||
obj.groups.set(previous.groups.all())
|
||||
super().save_model(request, obj, form, change)
|
||||
password = (form.cleaned_data or {}).get('new_password') or ''
|
||||
if not password:
|
||||
return
|
||||
from django.utils import timezone
|
||||
PasswordSetupInvite.objects.filter(
|
||||
employee=obj, used_at__isnull=True,
|
||||
).update(used_at=timezone.now())
|
||||
log_security_event(
|
||||
'employee.password_set_via_admin',
|
||||
actor=request.user,
|
||||
target=str(obj.pk),
|
||||
details=f'username={obj.username}',
|
||||
)
|
||||
self.message_user(request, _('Пароль портала обновлён.'), messages.SUCCESS)
|
||||
if form.cleaned_data.get('sync_ad_password') and can_offer_ad_password_sync(obj):
|
||||
from apps.ad_connector.services.sync_service import sync_ad_password
|
||||
try:
|
||||
ok = sync_ad_password(obj, password)
|
||||
except Exception:
|
||||
ok = False
|
||||
if ok:
|
||||
self.message_user(request, _('Пароль в Active Directory обновлён.'), messages.SUCCESS)
|
||||
else:
|
||||
self.message_user(
|
||||
request,
|
||||
_('Пароль портала сохранён, но сменить пароль в AD не удалось.'),
|
||||
messages.WARNING,
|
||||
)
|
||||
|
||||
# def permissions_summary(self, obj):
|
||||
# perms = obj.effective_permissions()
|
||||
|
||||
@@ -332,3 +332,58 @@ class TwoFactorTests(TestCase):
|
||||
r = self.client.get('/api/attendance/')
|
||||
self.assertEqual(r.status_code, 302)
|
||||
self.assertIn('/2fa/', r['Location'])
|
||||
|
||||
|
||||
class EmployeeAdminPasswordTests(TestCase):
|
||||
"""Админ может задать пароль сотрудника из карточки Unfold."""
|
||||
|
||||
def setUp(self):
|
||||
self.employee = make_employee('pwd.target', password='OldPass123')
|
||||
|
||||
def _form(self, **overrides):
|
||||
from apps.employees.admin import EmployeeAdminForm
|
||||
|
||||
emp = self.employee
|
||||
data = {
|
||||
'username': emp.username,
|
||||
'email': emp.email or '',
|
||||
'last_name': emp.last_name,
|
||||
'first_name': emp.first_name,
|
||||
'hire_date': emp.hire_date.isoformat(),
|
||||
'date_joined': emp.date_joined.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'is_active': True,
|
||||
'mattermost_sync_status': emp.mattermost_sync_status,
|
||||
'ad_sync_status': emp.ad_sync_status,
|
||||
'default_page_size': emp.default_page_size,
|
||||
}
|
||||
data.update(overrides)
|
||||
return EmployeeAdminForm(data, instance=emp)
|
||||
|
||||
def test_blank_password_keeps_current(self):
|
||||
form = self._form(new_password='', new_password_confirm='')
|
||||
self.assertTrue(form.is_valid(), form.errors)
|
||||
form.save()
|
||||
self.employee.refresh_from_db()
|
||||
self.assertTrue(self.employee.check_password('OldPass123'))
|
||||
|
||||
def test_mismatch_rejected(self):
|
||||
form = self._form(new_password='GoodPass123!', new_password_confirm='other')
|
||||
self.assertFalse(form.is_valid())
|
||||
self.assertIn('new_password_confirm', form.errors)
|
||||
|
||||
def test_sets_hashed_password(self):
|
||||
form = self._form(new_password='GoodPass123!', new_password_confirm='GoodPass123!')
|
||||
self.assertTrue(form.is_valid(), form.errors)
|
||||
form.save()
|
||||
self.employee.refresh_from_db()
|
||||
self.assertTrue(self.employee.check_password('GoodPass123!'))
|
||||
self.assertFalse(self.employee.check_password('OldPass123'))
|
||||
|
||||
def test_admin_change_view_shows_password_fields(self):
|
||||
admin_user = make_employee('pwd.admin', password='AdminPass123', is_staff=True, is_superuser=True)
|
||||
login_2fa(self.client, admin_user)
|
||||
url = f'/admin/employees/employee/{self.employee.pk}/change/'
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, 'new_password')
|
||||
self.assertContains(response, 'Новый пароль')
|
||||
|
||||
Reference in New Issue
Block a user