Files
ACORE/apps/it_manager/tests.py
T
lokses be7e5853cb it_manager: поиск, фильтры и инвентаризация оборудования.
Добавлены фильтры списка, редактирование инв. номеров/заметок/дат, история выдачи в карточке и тесты.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-07 13:23:00 +03:00

623 lines
27 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.
"""Тести IT-порталу: доступ через групу IT, інвентаризація обладнання
(зі списанням історії видачі), черга заявок, журнал робіт."""
from datetime import date
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from django.test import Client, TestCase
from apps.it_manager.models import (
Equipment, EquipmentAssignment, EquipmentModel, Ticket, WorkLog,
)
from core.models.directories import Directory
from core.test_utils import login_2fa, grant_all_locations
Employee = get_user_model()
_employee_seq = 0
def _dir_id(type_code, code):
"""Знаходить Directory.pk за (тип, код) — довідники засіяні data-міграціями конверсії choices."""
return Directory.objects.get(directory_type__code=type_code, code=code).id
def make_employee(**overrides):
global _employee_seq
_employee_seq += 1
defaults = {
'username': f'itman.user{_employee_seq}',
'email': f'itman.user{_employee_seq}@company.local',
'first_name': 'Тест',
'last_name': 'Співробітник',
'hire_date': date(2026, 1, 1),
'user_location_type': 'office',
'mattermost_sync_enabled': False,
'ad_sync_enabled': False,
}
defaults.update(overrides)
return Employee.objects.create(**defaults)
def make_equipment_model(**overrides):
defaults = {
'equipment_type_id': _dir_id('equipment_type', 'laptop'),
'name': 'ThinkPad T14',
}
defaults.update(overrides)
obj, _ = EquipmentModel.objects.get_or_create(**defaults)
return obj
def make_equipment(**overrides):
defaults = {
'equipment_model': make_equipment_model(),
'serial_number': f'SN-{Equipment.objects.count() + 1}',
}
defaults.update(overrides)
return Equipment.objects.create(**defaults)
class ItManagerAccessTests(TestCase):
"""Сторінка й усі мутації доступні лише групі IT (або суперюзеру)."""
def setUp(self):
self.outsider = make_employee()
self.client = Client()
self.client.force_login(self.outsider)
def test_page_forbidden_for_non_it(self):
r = self.client.get('/it-manager/')
self.assertEqual(r.status_code, 302)
self.assertEqual(r['Location'], '/dashboard/')
def test_equipment_create_forbidden_for_non_it(self):
r = self.client.post('/it-manager/equipment/bulk-create/', {
'equipment_model_id': '1', 'serial_numbers': 'X1',
})
self.assertEqual(r.status_code, 403)
def test_worklog_create_forbidden_for_non_it(self):
r = self.client.post('/it-manager/worklog/create/', {
'work_type': 'repair', 'description': 'test',
})
self.assertEqual(r.status_code, 403)
class ItManagerEquipmentTests(TestCase):
def setUp(self):
self.it_user = make_employee(is_staff=False)
self.it_user.groups.add(Group.objects.get(name='IT'))
grant_all_locations(self.it_user)
self.client = Client()
login_2fa(self.client, self.it_user)
def test_page_accessible_for_it_group(self):
r = self.client.get('/it-manager/')
self.assertEqual(r.status_code, 200)
def test_bulk_create_equipment(self):
model = make_equipment_model()
# масовий ввід: одна модель + список серійників (з дублем)
r = self.client.post('/it-manager/equipment/bulk-create/', {
'equipment_model_id': str(model.pk),
'serial_numbers': 'SN-100\nSN-101\nSN-101\nSN-102',
}, HTTP_HX_REQUEST='true')
self.assertEqual(r.status_code, 200)
units = Equipment.objects.filter(equipment_model=model)
self.assertEqual(units.count(), 3) # дубль SN-101 пропущено
self.assertEqual(
sorted(units.values_list('serial_number', flat=True)),
['SN-100', 'SN-101', 'SN-102'],
)
self.assertTrue(all(u.status.code == 'stock' for u in units))
def test_bulk_assign_equipment(self):
model = make_equipment_model()
units = [
Equipment.objects.create(equipment_model=model, serial_number=f'BA-{i}')
for i in range(3)
]
emp = make_employee()
r = self.client.post('/it-manager/equipment/bulk-assign/', {
'equipment_ids': [str(units[0].pk), str(units[1].pk)],
'assign_target': 'employee',
'assigned_to_id': str(emp.id),
}, HTTP_HX_REQUEST='true')
self.assertEqual(r.status_code, 200)
units[0].refresh_from_db(); units[1].refresh_from_db(); units[2].refresh_from_db()
self.assertEqual(units[0].assigned_to_id, emp.id)
self.assertEqual(units[0].status.code, 'assigned')
self.assertIsNone(units[2].assigned_to_id) # не відмічений — не чіпаємо
self.assertEqual(EquipmentAssignment.objects.filter(equipment__in=units[:2]).count(), 2)
def test_assign_equipment_creates_and_closes_history(self):
equipment = make_equipment()
emp1 = make_employee()
emp2 = make_employee()
self.client.post(f'/it-manager/equipment/{equipment.id}/update/', {
'status': 'assigned', 'assign_target': 'employee', 'assigned_to_id': emp1.id,
})
equipment.refresh_from_db()
self.assertEqual(equipment.assigned_to_id, emp1.id)
self.assertEqual(equipment.status.code, 'assigned')
self.assertEqual(EquipmentAssignment.objects.filter(equipment=equipment, returned_at__isnull=True).count(), 1)
self.client.post(f'/it-manager/equipment/{equipment.id}/update/', {
'status': 'assigned', 'assign_target': 'employee', 'assigned_to_id': emp2.id,
})
equipment.refresh_from_db()
self.assertEqual(equipment.assigned_to_id, emp2.id)
first_assignment = EquipmentAssignment.objects.get(equipment=equipment, assigned_to=emp1)
self.assertIsNotNone(first_assignment.returned_at)
self.assertEqual(
EquipmentAssignment.objects.filter(equipment=equipment, assigned_to=emp2, returned_at__isnull=True).count(), 1,
)
def test_unassign_equipment_closes_history_without_new_entry(self):
equipment = make_equipment()
emp1 = make_employee()
self.client.post(f'/it-manager/equipment/{equipment.id}/update/', {
'status': 'assigned', 'assign_target': 'employee', 'assigned_to_id': emp1.id,
})
self.client.post(f'/it-manager/equipment/{equipment.id}/update/', {
'status': 'stock', 'assign_target': '',
})
equipment.refresh_from_db()
self.assertIsNone(equipment.assigned_to_id)
self.assertEqual(equipment.status.code, 'stock')
self.assertEqual(EquipmentAssignment.objects.filter(equipment=equipment, returned_at__isnull=True).count(), 0)
def test_bulk_assign_to_location_only(self):
from apps.locations.models.base import Location
from core.models.directories import DirectoryType
region_type, _ = DirectoryType.objects.get_or_create(
code='location_region', defaults={'name': 'Регион'},
)
region, _ = Directory.objects.get_or_create(
directory_type=region_type, code='eq-reg', defaults={'name': 'Тест'},
)
location = Location.objects.create(
address='Аптека тест', location_type='pharmacy',
location_region=region, is_active=True,
)
model = make_equipment_model(name='RB952Ui')
unit = Equipment.objects.create(equipment_model=model, serial_number='RT-LOC-1')
r = self.client.post('/it-manager/equipment/bulk-assign/', {
'equipment_ids': [str(unit.pk)],
'assign_target': 'pharmacy',
'location_id': str(location.pk),
}, HTTP_HX_REQUEST='true')
self.assertEqual(r.status_code, 200)
unit.refresh_from_db()
self.assertEqual(unit.location_id, location.pk)
self.assertIsNone(unit.assigned_to_id)
self.assertEqual(unit.status.code, 'assigned')
hist = EquipmentAssignment.objects.get(equipment=unit, returned_at__isnull=True)
self.assertEqual(hist.location_id, location.pk)
self.assertIsNone(hist.assigned_to_id)
def test_update_assign_to_pharmacy(self):
from apps.locations.models.base import Location
from core.models.directories import DirectoryType
region_type, _ = DirectoryType.objects.get_or_create(
code='location_region', defaults={'name': 'Регион'},
)
region, _ = Directory.objects.get_or_create(
directory_type=region_type, code='eq-reg2', defaults={'name': 'Тест2'},
)
location = Location.objects.create(
address='А12', location_type='pharmacy',
location_region=region, is_active=True,
)
equipment = make_equipment(serial_number='RT-UPD-1')
self.client.post(f'/it-manager/equipment/{equipment.id}/update/', {
'status': 'assigned',
'assign_target': 'pharmacy',
'location_id': str(location.pk),
})
equipment.refresh_from_db()
self.assertEqual(equipment.location_id, location.pk)
self.assertIsNone(equipment.assigned_to_id)
self.assertEqual(equipment.status.code, 'assigned')
self.assertEqual(
EquipmentAssignment.objects.filter(equipment=equipment, returned_at__isnull=True).count(), 1,
)
# повернення на склад
self.client.post(f'/it-manager/equipment/{equipment.id}/update/', {
'status': 'stock',
'assign_target': '',
})
equipment.refresh_from_db()
self.assertIsNone(equipment.location_id)
self.assertEqual(equipment.status.code, 'stock')
self.assertEqual(
EquipmentAssignment.objects.filter(equipment=equipment, returned_at__isnull=True).count(), 0,
)
def test_bulk_create_with_location_writes_history(self):
from apps.locations.models.base import Location
from core.models.directories import DirectoryType
region_type, _ = DirectoryType.objects.get_or_create(
code='location_region', defaults={'name': 'Регион'},
)
region, _ = Directory.objects.get_or_create(
directory_type=region_type, code='eq-reg3', defaults={'name': 'Тест3'},
)
location = Location.objects.create(
address='Склад-офіс', location_type='office',
location_region=region, is_active=True,
)
model = make_equipment_model()
r = self.client.post('/it-manager/equipment/bulk-create/', {
'equipment_model_id': str(model.pk),
'serial_numbers': 'SN-LOC-A',
'assign_target': 'office',
'location_id': str(location.pk),
}, HTTP_HX_REQUEST='true')
self.assertEqual(r.status_code, 200)
unit = Equipment.objects.get(serial_number='SN-LOC-A')
self.assertEqual(unit.location_id, location.pk)
self.assertEqual(unit.status.code, 'assigned')
self.assertEqual(
EquipmentAssignment.objects.filter(equipment=unit, location=location).count(), 1,
)
def test_search_by_serial_and_inventory(self):
make_equipment(serial_number='SRCH-SN-AA', inventory_number='INV-1001')
make_equipment(serial_number='OTHER-99', inventory_number='INV-2002')
r = self.client.get('/it-manager/', {'tab': 'equipment', 'q': 'SRCH-SN'})
self.assertEqual(r.status_code, 200)
serials = [e.serial_number for e in r.context['equipment_page']]
self.assertEqual(serials, ['SRCH-SN-AA'])
r = self.client.get('/it-manager/', {'tab': 'equipment', 'q': 'INV-2002'})
self.assertEqual(r.status_code, 200)
serials = [e.serial_number for e in r.context['equipment_page']]
self.assertEqual(serials, ['OTHER-99'])
def test_filter_by_status_and_location(self):
from apps.locations.models.base import Location
from core.models.directories import DirectoryType
region_type, _ = DirectoryType.objects.get_or_create(
code='location_region', defaults={'name': 'Регион'},
)
region, _ = Directory.objects.get_or_create(
directory_type=region_type, code='eq-filt-reg', defaults={'name': 'Фильтр'},
)
loc_a = Location.objects.create(
address='Лок А', location_type='pharmacy',
location_region=region, is_active=True,
)
loc_b = Location.objects.create(
address='Лок Б', location_type='pharmacy',
location_region=region, is_active=True,
)
stock = make_equipment(serial_number='FILT-STOCK')
assigned = make_equipment(
serial_number='FILT-ASGN',
status_id=_dir_id('equipment_status', 'assigned'),
location=loc_a,
)
make_equipment(
serial_number='FILT-OTHER',
status_id=_dir_id('equipment_status', 'assigned'),
location=loc_b,
)
r = self.client.get('/it-manager/', {'tab': 'equipment', 'status': 'stock'})
self.assertEqual(r.status_code, 200)
serials = [e.serial_number for e in r.context['equipment_page']]
self.assertIn('FILT-STOCK', serials)
self.assertNotIn('FILT-ASGN', serials)
r = self.client.get('/it-manager/', {
'tab': 'equipment', 'status': 'assigned', 'location_id': str(loc_a.pk),
})
self.assertEqual(r.status_code, 200)
serials = [e.serial_number for e in r.context['equipment_page']]
self.assertEqual(serials, ['FILT-ASGN'])
self.assertEqual(stock.status.code, 'stock')
self.assertEqual(assigned.location_id, loc_a.pk)
def test_update_inventory_number_and_notes(self):
equipment = make_equipment(serial_number='INV-EDIT-1')
r = self.client.post(f'/it-manager/equipment/{equipment.id}/update/', {
'status': 'stock',
'assign_target': '',
'inventory_number': 'INV-EDIT-99',
'notes': 'Проверено на складе',
'purchase_date': '2025-06-01',
'warranty_until': '2027-06-01',
}, HTTP_HX_REQUEST='true')
self.assertEqual(r.status_code, 200)
equipment.refresh_from_db()
self.assertEqual(equipment.inventory_number, 'INV-EDIT-99')
self.assertEqual(equipment.notes, 'Проверено на складе')
self.assertEqual(str(equipment.purchase_date), '2025-06-01')
self.assertEqual(str(equipment.warranty_until), '2027-06-01')
other = make_equipment(serial_number='INV-EDIT-2', inventory_number='INV-TAKEN')
r = self.client.post(f'/it-manager/equipment/{equipment.id}/update/', {
'status': 'stock',
'assign_target': '',
'inventory_number': other.inventory_number,
'notes': equipment.notes,
}, HTTP_HX_REQUEST='true')
self.assertEqual(r.status_code, 200)
self.assertContains(r, 'уже используется')
equipment.refresh_from_db()
self.assertEqual(equipment.inventory_number, 'INV-EDIT-99')
def test_bulk_create_with_inventory_numbers(self):
model = make_equipment_model()
r = self.client.post('/it-manager/equipment/bulk-create/', {
'equipment_model_id': str(model.pk),
'serial_numbers': 'SN-INV-1\nSN-INV-2',
'inventory_numbers': 'INV-A1\nINV-A2',
}, HTTP_HX_REQUEST='true')
self.assertEqual(r.status_code, 200)
u1 = Equipment.objects.get(serial_number='SN-INV-1')
u2 = Equipment.objects.get(serial_number='SN-INV-2')
self.assertEqual(u1.inventory_number, 'INV-A1')
self.assertEqual(u2.inventory_number, 'INV-A2')
r = self.client.post('/it-manager/equipment/bulk-create/', {
'equipment_model_id': str(model.pk),
'serial_numbers': 'SN-INV-3\nSN-INV-4',
'inventory_numbers': 'INV-ONLY-ONE',
}, HTTP_HX_REQUEST='true')
self.assertEqual(r.status_code, 200)
self.assertContains(r, 'должно совпадать')
self.assertFalse(Equipment.objects.filter(serial_number='SN-INV-3').exists())
def test_assignment_history_visible_in_card(self):
equipment = make_equipment(serial_number='HIST-UI-1')
emp = make_employee(first_name='Иван', last_name='Историков')
self.client.post(f'/it-manager/equipment/{equipment.id}/update/', {
'status': 'assigned',
'assign_target': 'employee',
'assigned_to_id': emp.id,
})
r = self.client.get('/it-manager/', {'tab': 'equipment', 'q': 'HIST-UI-1'})
self.assertEqual(r.status_code, 200)
self.assertContains(r, 'История перемещений')
self.assertContains(r, 'Историков')
self.assertContains(r, 'открыта')
class ItManagerTicketTests(TestCase):
def setUp(self):
self.it_user = make_employee(is_staff=False)
self.it_user.groups.add(Group.objects.get(name='IT'))
grant_all_locations(self.it_user)
self.requester = make_employee()
self.client = Client()
login_2fa(self.client, self.it_user)
def test_resolve_ticket_stamps_resolved_at(self):
ticket = Ticket.objects.create(
title='Не працює принтер', description='...',
ticket_type_id=_dir_id('ticket_type', 'repair'),
requester=self.requester,
)
self.client.post(f'/it-manager/tickets/{ticket.id}/update/', {
'status': 'resolved', 'assignee_id': self.it_user.id,
})
ticket.refresh_from_db()
self.assertEqual(ticket.status.code, 'resolved')
self.assertEqual(ticket.assignee_id, self.it_user.id)
self.assertIsNotNone(ticket.resolved_at)
def test_add_comment(self):
ticket = Ticket.objects.create(
title='Питання', description='...',
ticket_type_id=_dir_id('ticket_type', 'consultation'),
requester=self.requester,
)
r = self.client.post(f'/it-manager/tickets/{ticket.id}/comment/', {
'text': 'Дивимось', 'is_internal': 'on',
})
self.assertEqual(r.status_code, 200)
self.assertEqual(ticket.comments.count(), 1)
self.assertTrue(ticket.comments.first().is_internal)
class ItManagerWorkLogTests(TestCase):
def setUp(self):
self.it_user = make_employee(is_staff=False)
self.it_user.groups.add(Group.objects.get(name='IT'))
grant_all_locations(self.it_user)
self.client = Client()
login_2fa(self.client, self.it_user)
def test_create_worklog(self):
r = self.client.post('/it-manager/worklog/create/', {
'work_type': 'maintenance', 'description': 'Профілактика серверної',
'hours_spent': '2.5',
})
self.assertEqual(r.status_code, 200)
log = WorkLog.objects.get(description='Профілактика серверної')
self.assertEqual(log.technician_id, self.it_user.id)
self.assertEqual(float(log.hours_spent), 2.5)
self.assertContains(r, 'Запись о выполненной работе сохранена')
def test_create_worklog_requires_description(self):
before = WorkLog.objects.count()
r = self.client.post('/it-manager/worklog/create/', {
'work_type': 'maintenance', 'description': ' ',
})
self.assertEqual(r.status_code, 200)
self.assertEqual(WorkLog.objects.count(), before)
self.assertContains(r, 'Укажите описание работ')
def test_create_worklog_linked_to_ticket(self):
from apps.it_manager.models import Ticket
from core.models.directories import Directory, DirectoryType
ttype, _ = DirectoryType.objects.get_or_create(code='ticket_type', defaults={'name': 'Тип заявки'})
ptype, _ = DirectoryType.objects.get_or_create(code='ticket_priority', defaults={'name': 'Приоритет'})
stype, _ = DirectoryType.objects.get_or_create(code='ticket_status', defaults={'name': 'Статус'})
ticket_type, _ = Directory.objects.get_or_create(
directory_type=ttype, code='incident', defaults={'name': 'Инцидент'},
)
priority, _ = Directory.objects.get_or_create(
directory_type=ptype, code='medium', defaults={'name': 'Средний'},
)
status, _ = Directory.objects.get_or_create(
directory_type=stype, code='in_progress', defaults={'name': 'В работе'},
)
ticket = Ticket.objects.create(
title='Не працює каса',
ticket_type=ticket_type,
priority=priority,
status=status,
requester=self.it_user,
description='Каса не відкривається',
)
r = self.client.post('/it-manager/worklog/create/', {
'work_type': 'repair',
'description': 'Перезапуск касового ПЗ',
'ticket_id': ticket.pk,
'hours_spent': '1',
})
self.assertEqual(r.status_code, 200)
log = WorkLog.objects.get(description='Перезапуск касового ПЗ')
self.assertEqual(log.ticket_id, ticket.pk)
self.assertContains(r, 'Работа привязана к заявке')
self.assertContains(r, ticket.number)
class ItManagerWorkLogFilterEditTests(TestCase):
"""Фильтр по сотруднику и inline-редактирование для руководителя."""
def setUp(self):
from core.models.directories import DirectoryType
dept_type, _ = DirectoryType.objects.get_or_create(
code='department', defaults={'name': 'Відділи', 'hierarchy_type': 'strict'},
)
self.dept = Directory.objects.create(
directory_type=dept_type, code='it-wl', name='IT-відділ-wl',
)
self.head = make_employee(
last_name='Керівник', first_name='ІТ', username='it.head.wl',
department=self.dept, user_location_type='office',
)
self.tech_a = make_employee(
last_name='Альфа', first_name='Тех', username='tech.a.wl',
department=self.dept, manager=self.head, user_location_type='office',
)
self.tech_b = make_employee(
last_name='Бета', first_name='Тех', username='tech.b.wl',
department=self.dept, manager=self.head, user_location_type='office',
)
it_group = Group.objects.get(name='IT')
for u in (self.head, self.tech_a, self.tech_b):
u.groups.add(it_group)
grant_all_locations(u)
self.log_a = WorkLog.objects.create(
date=date(2026, 9, 1),
technician=self.tech_a,
work_type_id=_dir_id('worklog_type', 'maintenance'),
description='Робота А',
hours_spent=1,
)
self.log_b = WorkLog.objects.create(
date=date(2026, 9, 2),
technician=self.tech_b,
work_type_id=_dir_id('worklog_type', 'repair'),
description='Робота Б',
hours_spent=2,
)
self.client = Client()
def test_filter_by_technician(self):
login_2fa(self.client, self.tech_a)
r = self.client.get(f'/it-manager/?tab=worklog&worklog_technician={self.tech_a.pk}')
self.assertEqual(r.status_code, 200)
self.assertContains(r, 'Робота А')
self.assertNotContains(r, 'Робота Б')
self.assertContains(r, 'name="worklog_technician"')
self.assertContains(r, 'name="worklog_month"')
self.assertContains(r, 'data-it-active-tab="worklog"')
self.assertContains(r, 'id="it-panel-equipment"')
self.assertRegex(r.content.decode(), r'id="it-panel-equipment"[^>]*style="display:none;"')
self.assertRegex(r.content.decode(), r'id="it-panel-worklog"(?! style="display:none;")')
def test_filter_by_month(self):
WorkLog.objects.create(
date=date(2026, 8, 15),
technician=self.tech_a,
work_type_id=_dir_id('worklog_type', 'maintenance'),
description='Серпень робота',
)
login_2fa(self.client, self.tech_a)
r = self.client.get('/it-manager/?tab=worklog&worklog_month=2026-09')
self.assertEqual(r.status_code, 200)
self.assertContains(r, 'Робота А')
self.assertContains(r, 'Робота Б')
self.assertNotContains(r, 'Серпень робота')
r_all = self.client.get('/it-manager/?tab=worklog&worklog_month=all')
self.assertContains(r_all, 'Серпень робота')
def test_filter_month_and_technician(self):
login_2fa(self.client, self.tech_a)
r = self.client.get(
f'/it-manager/?tab=worklog&worklog_month=2026-09&worklog_technician={self.tech_b.pk}'
)
self.assertEqual(r.status_code, 200)
self.assertNotContains(r, 'Робота А')
self.assertContains(r, 'Робота Б')
def test_head_can_inline_update(self):
login_2fa(self.client, self.head)
r = self.client.post(f'/it-manager/worklog/{self.log_a.pk}/update/', {
'date': '2026-09-03',
'technician_id': self.tech_b.pk,
'work_type': 'software',
'description': 'Виправлено керівником',
'result': 'Ок',
'hours_spent': '3.5',
'location_id': '',
})
self.assertEqual(r.status_code, 200)
self.assertContains(r, 'Сохранено')
self.log_a.refresh_from_db()
self.assertEqual(self.log_a.description, 'Виправлено керівником')
self.assertEqual(self.log_a.technician_id, self.tech_b.pk)
self.assertEqual(float(self.log_a.hours_spent), 3.5)
self.assertEqual(self.log_a.work_type.code, 'software')
def test_non_head_cannot_inline_update(self):
login_2fa(self.client, self.tech_a)
r = self.client.post(f'/it-manager/worklog/{self.log_a.pk}/update/', {
'date': '2026-09-03',
'technician_id': self.tech_a.pk,
'work_type': 'maintenance',
'description': 'Спроба змінити',
'hours_spent': '9',
})
self.assertEqual(r.status_code, 403)
self.log_a.refresh_from_db()
self.assertEqual(self.log_a.description, 'Робота А')
def test_head_sees_editable_controls(self):
login_2fa(self.client, self.head)
r = self.client.get('/it-manager/?tab=worklog')
self.assertEqual(r.status_code, 200)
self.assertContains(r, f'worklog-form-{self.log_a.pk}')
self.assertContains(r, 'править записи прямо в таблице')