Files
Inventory/inventory/test_workflow.py
2026-09-29 08:43:05 +02:00

267 lines
16 KiB
Python

from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.http import QueryDict
from django.test import TestCase
from django.urls import reverse
from .device_types import applicable_fields
from .api import DeviceSerializer
from .forms import DeviceForm
from .models import AllInOneSpecification, ChoiceValue, ComputerSpecification, DesktopSpecification, Device, DeviceTask, InspectionRecord, LaptopSpecification, MonitorSpecification, PrinterSpecification, RepairRecord, UserProfile, WorkflowEvent
from .query import device_queryset
from .services import update_device_field
from .workflow import advance_device, complete_task, create_task
class WorkflowTests(TestCase):
@classmethod
def setUpTestData(cls):
cls.status, _ = ChoiceValue.objects.get_or_create(kind="status", label="Eingang")
for label in ("Defekt", "Fertig", "Ausgegeben"):
ChoiceValue.objects.get_or_create(kind="status", label=label)
cls.laptop, _ = ChoiceValue.objects.get_or_create(kind="device_type", label="Laptop")
cls.printer, _ = ChoiceValue.objects.get_or_create(kind="device_type", label="Drucker")
cls.user = User.objects.create_user("editor", password="test-password")
cls.user.profile.role = UserProfile.Role.EDITOR
cls.user.profile.save()
cls.viewer = User.objects.create_user("viewer", password="test-password")
def setUp(self):
self.device = Device.objects.create(status=self.status, device_type=self.laptop)
def test_flow_blocks_open_tasks_and_requires_recipient_for_issue(self):
with self.assertRaises(ValidationError):
advance_device(self.device.pk, Device.Stage.ISSUED, user=self.user,
expected_revision=self.device.revision)
self.device = advance_device(self.device.pk, Device.Stage.INSPECTION, user=self.user,
expected_revision=self.device.revision)
task = create_task(self.device.pk, title="Akku prüfen", user=self.user)
with self.assertRaises(ValidationError):
advance_device(self.device.pk, Device.Stage.STORAGE, user=self.user,
expected_revision=self.device.revision)
complete_task(self.device.pk, task.pk, user=self.user)
self.device = advance_device(self.device.pk, Device.Stage.STORAGE, user=self.user,
expected_revision=self.device.revision)
with self.assertRaises(ValidationError):
advance_device(self.device.pk, Device.Stage.ISSUED, user=self.user,
expected_revision=self.device.revision)
recipient = ChoiceValue.objects.create(kind="recipient", label="Verein")
self.device.recipient = recipient
self.device.save()
self.device = advance_device(self.device.pk, Device.Stage.ISSUED, user=self.user,
expected_revision=self.device.revision, note="Abgeholt")
self.assertEqual(WorkflowEvent.objects.filter(device=self.device).count(), 3)
self.assertEqual(self.device.tasks.get().state, DeviceTask.State.DONE)
self.assertEqual(self.device.workflow_events.first().note, "Abgeholt")
self.assertEqual(self.device.status.label, "Ausgegeben")
def test_stale_transition_and_viewer_cannot_change_workflow(self):
self.client.force_login(self.viewer)
response = self.client.post(reverse("inventory:device-advance", args=[self.device.pk]),
{"stage": "INSPECTION", "expected_revision": self.device.revision})
self.assertEqual(response.status_code, 403)
self.assertFalse(WorkflowEvent.objects.exists())
with self.assertRaises(ValidationError):
advance_device(self.device.pk, Device.Stage.INSPECTION, user=self.user,
expected_revision=self.device.revision + 1)
def test_printer_has_no_computer_fields_in_form_and_inline_write_is_rejected(self):
self.assertNotIn("ram_gb", applicable_fields(self.printer))
self.device.device_type = self.printer
self.device.save()
form = DeviceForm(instance=self.device)
self.assertNotIn("ram_gb", applicable_fields(self.printer))
self.assertIn("ram_gb", form.fields)
with self.assertRaises(ValidationError):
update_device_field(self.device, "ram_gb", "8", self.user)
self.client.force_login(self.user)
response = self.client.get(reverse("inventory:device-detail", args=[self.device.pk]))
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'data-hardware-field="ram_gb" hidden')
self.assertContains(response, "Aufgaben")
listing = self.client.get(reverse("inventory:device-list"), {"type": self.printer.pk})
self.assertEqual(listing.status_code, 200)
self.assertNotContains(listing, 'data-column="ram_gb"')
def test_task_board_and_form_work_for_editor(self):
self.client.force_login(self.user)
response = self.client.post(reverse("inventory:device-task-add", args=[self.device.pk]), {
"title": "Gehäuse prüfen", "due_date": "2026-10-01", "assigned_to": self.user.pk,
})
self.assertEqual(response.status_code, 302)
task = DeviceTask.objects.get(device=self.device)
self.assertEqual(task.assigned_to, self.user)
response = self.client.get(reverse("inventory:task-list"))
self.assertContains(response, "Gehäuse prüfen")
self.client.post(reverse("inventory:device-task-complete", args=[self.device.pk, task.pk]))
task.refresh_from_db()
self.assertEqual(task.state, DeviceTask.State.DONE)
def test_changing_to_printer_preserves_old_hardware_in_legacy_data(self):
self.device.ram_gb = 16
self.device.cpu = "Core i5"
self.device.save()
update_device_field(self.device, "device_type", str(self.printer.pk), self.user)
self.device.refresh_from_db()
self.assertIsNone(self.device.ram_gb)
self.assertEqual(self.device.cpu, "")
self.assertEqual(self.device.custom_values["legacy_hardware"]["ram_gb"], "16")
self.assertFalse(LaptopSpecification.objects.filter(device=self.device).exists())
self.assertTrue(PrinterSpecification.objects.filter(device=self.device).exists())
def test_detail_form_switches_type_and_saves_only_printer_fields(self):
self.device.ram_gb = 16
self.device.save()
self.client.force_login(self.user)
response = self.client.post(reverse("inventory:device-detail", args=[self.device.pk]), {
"expected_revision": self.device.revision,
"status": self.status.pk,
"device_type": self.printer.pk,
"date": "28.09.2026",
"print_technology": "Laser",
"duplex": "YES",
"color_printing": "NO",
"ports": "USB",
"ram_gb": "999",
})
self.assertEqual(response.status_code, 302)
self.device.refresh_from_db()
self.assertEqual(self.device.device_type, self.printer)
self.assertEqual(self.device.print_technology, "Laser")
self.assertEqual(self.device.ports, "USB")
self.assertIsNone(self.device.ram_gb)
self.assertEqual(self.device.custom_values["legacy_hardware"]["ram_gb"], "16")
response = self.client.post(reverse("inventory:device-detail", args=[self.device.pk]), {
"expected_revision": self.device.revision,
"status": "",
"device_type": self.laptop.pk,
"ram_gb": "32",
})
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'data-hardware-field="ram_gb"')
self.assertEqual(set(response.context["hardware_fields"]), applicable_fields(self.laptop))
def test_type_specific_tables_and_direct_workflow_board(self):
self.device.ram_gb = 8
self.device.save(update_fields=["ram_gb"])
self.assertEqual(LaptopSpecification.objects.get(device=self.device).ram_gb, 8)
printer = Device.objects.create(status=self.status, device_type=self.printer, wifi=Device.TriState.YES, print_technology="Laser", duplex=Device.TriState.YES)
self.assertTrue(PrinterSpecification.objects.filter(device=printer).exists())
self.assertEqual(PrinterSpecification.objects.get(device=printer).print_technology, "Laser")
self.assertFalse(ComputerSpecification.objects.filter(device=printer).exists())
screen, _ = ChoiceValue.objects.get_or_create(kind="device_type", label="Bildschirm")
monitor = Device.objects.create(status=self.status, device_type=screen, display_inches="24.0", resolution="1920x1080", ports="HDMI, DisplayPort")
self.assertTrue(MonitorSpecification.objects.filter(device=monitor).exists())
self.assertEqual(MonitorSpecification.objects.get(device=monitor).ports, "HDMI, DisplayPort")
self.assertFalse(ComputerSpecification.objects.filter(device=monitor).exists())
self.assertIn("resolution", DeviceForm(instance=monitor).fields)
self.assertIn("ports", DeviceForm(instance=monitor).fields)
self.assertNotIn("ram_gb", applicable_fields(screen))
self.assertIn("print_technology", DeviceForm(instance=printer).fields)
self.assertEqual(DeviceSerializer(monitor).data["ports"], "HDMI, DisplayPort")
desktop_type, _ = ChoiceValue.objects.get_or_create(kind="device_type", label="Desktop")
desktop = Device.objects.create(status=self.status, device_type=desktop_type, ram_gb=32, ports="USB-C, HDMI")
self.assertEqual(DesktopSpecification.objects.get(device=desktop).ports, "USB-C, HDMI")
self.assertIn(desktop, device_queryset(QueryDict("ram_min=32")))
self.assertNotIn(self.device, device_queryset(QueryDict("ram_min=32")))
aio_type, _ = ChoiceValue.objects.get_or_create(kind="device_type", label="AiO")
aio = Device.objects.create(status=self.status, device_type=aio_type, display_inches="27.0")
self.assertEqual(str(AllInOneSpecification.objects.get(device=aio).display_inches), "27.0")
self.client.force_login(self.user)
board = self.client.get(reverse("inventory:workflow-board"))
self.assertContains(board, f"#{self.device.number}")
self.assertContains(board, "Neue Aufgabe")
listing = self.client.get(reverse("inventory:device-list"))
self.assertContains(listing, reverse("inventory:device-detail", args=[self.device.pk]))
def test_board_quick_actions_return_to_board(self):
self.client.force_login(self.user)
board_url = reverse("inventory:workflow-board")
response = self.client.post(reverse("inventory:device-task-add", args=[self.device.pk]), {
"title": "Gerät prüfen", "return_to": "workflow",
})
self.assertRedirects(response, board_url)
task = DeviceTask.objects.get(device=self.device)
self.assertEqual(task.title, "Gerät prüfen")
response = self.client.post(reverse("inventory:device-advance", args=[self.device.pk]), {
"stage": Device.Stage.INSPECTION,
"expected_revision": self.device.revision,
"return_to": "workflow",
})
self.assertRedirects(response, board_url)
self.device.refresh_from_db()
self.assertEqual(self.device.stage, Device.Stage.INSPECTION)
board = self.client.get(board_url)
self.assertContains(board, "Gerät prüfen")
self.assertContains(board, f"#{self.device.number}")
def test_stage_forms_capture_inspection_repair_and_recipient(self):
self.client.force_login(self.user)
self.device = advance_device(self.device.pk, Device.Stage.INSPECTION, user=self.user,
expected_revision=self.device.revision)
board_url = reverse("inventory:workflow-board")
board = self.client.get(board_url)
self.assertContains(board, "Festgestellte Mängel")
self.assertContains(board, 'name="ram_gb"')
inspection_url = reverse("inventory:device-inspection-save", args=[self.device.pk])
response = self.client.post(inspection_url, {
"defects": "Akku defekt", "model": "ThinkPad", "cpu": "Core i5", "ram_gb": "16",
"ssd_gb": "512", "battery": "NO", "wifi": "YES", "lan": "YES",
"expected_revision": self.device.revision, "return_to": "workflow",
})
self.assertRedirects(response, board_url)
self.device.refresh_from_db()
self.assertEqual(self.device.ram_gb, 16)
self.assertEqual(self.device.model, "ThinkPad")
self.assertEqual(InspectionRecord.objects.get(device=self.device).defects, "Akku defekt")
stale = self.client.post(inspection_url, {
"defects": "Falscher Befund", "battery": "UNKNOWN", "wifi": "UNKNOWN", "lan": "UNKNOWN",
"expected_revision": self.device.revision - 1,
})
self.assertEqual(stale.status_code, 302)
self.assertEqual(InspectionRecord.objects.count(), 1)
self.device = advance_device(self.device.pk, Device.Stage.REPAIR, user=self.user,
expected_revision=self.device.revision)
response = self.client.post(reverse("inventory:device-repair-add", args=[self.device.pk]), {
"work_done": "Akku ersetzt", "expected_revision": self.device.revision, "return_to": "workflow",
})
self.assertRedirects(response, board_url)
self.assertEqual(RepairRecord.objects.get(device=self.device).work_done, "Akku ersetzt")
self.device.refresh_from_db()
self.device = advance_device(self.device.pk, Device.Stage.STORAGE, user=self.user,
expected_revision=self.device.revision)
response = self.client.post(reverse("inventory:device-storage-assign", args=[self.device.pk]), {
"recipient": "Verein Nord", "expected_revision": self.device.revision,
"return_to": "workflow",
})
self.assertRedirects(response, board_url)
self.device.refresh_from_db()
self.assertEqual(self.device.recipient.label, "Verein Nord")
board = self.client.get(board_url)
self.assertContains(board, "Verein Nord")
def test_inspection_respects_device_type_and_viewer_is_read_only(self):
printer = Device.objects.create(status=self.status, device_type=self.printer)
printer = advance_device(printer.pk, Device.Stage.INSPECTION, user=self.user,
expected_revision=printer.revision)
self.client.force_login(self.user)
board = self.client.get(reverse("inventory:workflow-board"))
self.assertContains(board, "Festgestellte Mängel")
self.assertNotContains(board, 'name="ram_gb"')
response = self.client.post(reverse("inventory:device-inspection-save", args=[printer.pk]), {
"defects": "Papierstau", "ram_gb": "64", "wifi": "YES", "lan": "NO",
"duplex": "UNKNOWN", "color_printing": "UNKNOWN",
"expected_revision": printer.revision,
})
self.assertEqual(response.status_code, 302)
printer.refresh_from_db()
self.assertIsNone(printer.ram_gb)
self.assertEqual(printer.wifi, Device.TriState.YES)
self.client.force_login(self.viewer)
response = self.client.post(reverse("inventory:device-repair-add", args=[printer.pk]), {
"work_done": "Unberechtigt", "expected_revision": printer.revision,
})
self.assertEqual(response.status_code, 403)
self.assertFalse(RepairRecord.objects.exists())