feat: async email slanje s cache PDF fallbackom i admin logom
Prebaci slanje emaila putnog naloga u pozadinski task bez blokiranja UI-a i dodaj fallback kad queue nije dostupna. Kod greške slanja spremi generirane PDF-ove u cache i pošalji notifikaciju s download linkovima. Dodaj EmailDispatchLog model i Django admin pregled za praćenje kome je poslano, što je poslano, status i detalje privitaka. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import TestCase, override_settings
|
||||
|
||||
from modules.fleet.models import GeneratedWorkOrderPdf, Vehicle, VehicleNotification, WorkOrder
|
||||
from modules.fleet.tasks import send_work_order_email_bundle_task
|
||||
|
||||
|
||||
@override_settings(
|
||||
EMAIL_BACKEND='django.core.mail.backends.locmem.EmailBackend',
|
||||
DEFAULT_FROM_EMAIL='no-reply@example.test',
|
||||
)
|
||||
class WorkOrderEmailTaskCachedPdfTests(TestCase):
|
||||
def setUp(self):
|
||||
user_model = get_user_model()
|
||||
self.user = user_model.objects.create_user(
|
||||
username='task-mail-user',
|
||||
email='task-mail-user@example.test',
|
||||
password='test1234',
|
||||
)
|
||||
self.vehicle = Vehicle.objects.create(
|
||||
asset_type='crane',
|
||||
registration_number='TASK-CACHE-001',
|
||||
crane_serial_number='112233',
|
||||
)
|
||||
self.work_order = WorkOrder.objects.create(
|
||||
vehicle=self.vehicle,
|
||||
creator=self.user,
|
||||
display_code='MT010126',
|
||||
purpose='kontrola',
|
||||
)
|
||||
|
||||
@patch('django.core.mail.message.EmailMessage.send', side_effect=Exception('smtp-fail'))
|
||||
def test_failed_email_creates_cached_pdf_links_in_notification(self, _mock_send):
|
||||
result = send_work_order_email_bundle_task(
|
||||
work_order_id=str(self.work_order.pk),
|
||||
requested_by_id=str(self.user.pk),
|
||||
recipients=['client@example.test'],
|
||||
include_work_order_pdf=True,
|
||||
include_service_records_pdf=False,
|
||||
include_invoices_pdf=False,
|
||||
include_images=False,
|
||||
include_monthly_tasks=False,
|
||||
)
|
||||
|
||||
self.assertEqual(result.get('status'), 'failed')
|
||||
|
||||
cached = GeneratedWorkOrderPdf.objects.filter(work_order=self.work_order, pdf_type='work_order', status='ready')
|
||||
self.assertTrue(cached.exists())
|
||||
|
||||
notif = VehicleNotification.objects.filter(
|
||||
recipient=self.user,
|
||||
metadata__entity_type='work_order_email',
|
||||
metadata__stage='failed',
|
||||
).order_by('-created_at').first()
|
||||
self.assertIsNotNone(notif)
|
||||
cached_pdfs = notif.metadata.get('cached_pdfs') if isinstance(notif.metadata, dict) else None
|
||||
self.assertTrue(isinstance(cached_pdfs, list) and len(cached_pdfs) > 0)
|
||||
self.assertIn('download_url', cached_pdfs[0])
|
||||
128
backend/modules/fleet/tests/test_work_order_send_email.py
Normal file
128
backend/modules/fleet/tests/test_work_order_send_email.py
Normal file
@@ -0,0 +1,128 @@
|
||||
import uuid
|
||||
from datetime import date
|
||||
from io import BytesIO
|
||||
|
||||
from PIL import Image
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.test import TestCase, override_settings
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APIClient
|
||||
from unittest.mock import patch
|
||||
|
||||
from modules.fleet.models import ServiceContextNote, Vehicle, WorkOrder, WorkOrderInvoice, WorkOrderPhoto
|
||||
from modules.task_management.models import Task
|
||||
|
||||
|
||||
def _test_image(filename='test.jpg', size=(40, 40), color='blue'):
|
||||
file = BytesIO()
|
||||
image = Image.new('RGB', size=size, color=color)
|
||||
image.save(file, 'JPEG')
|
||||
file.seek(0)
|
||||
return SimpleUploadedFile(
|
||||
name=filename,
|
||||
content=file.getvalue(),
|
||||
content_type='image/jpeg',
|
||||
)
|
||||
|
||||
|
||||
@override_settings(
|
||||
EMAIL_BACKEND='django.core.mail.backends.locmem.EmailBackend',
|
||||
DEFAULT_FROM_EMAIL='no-reply@example.test',
|
||||
)
|
||||
class WorkOrderSendEmailTests(TestCase):
|
||||
def setUp(self):
|
||||
user_model = get_user_model()
|
||||
suffix = uuid.uuid4().hex[:8]
|
||||
self.user = user_model.objects.create_user(
|
||||
username=f'wo-mail-user-{suffix}',
|
||||
email=f'wo-mail-user-{suffix}@example.test',
|
||||
password='test1234',
|
||||
)
|
||||
self.client = APIClient()
|
||||
self.client.force_authenticate(user=self.user)
|
||||
|
||||
self.vehicle = Vehicle.objects.create(
|
||||
asset_type='crane',
|
||||
registration_number='WO-MAIL-001',
|
||||
crane_serial_number='654321',
|
||||
)
|
||||
self.work_order = WorkOrder.objects.create(
|
||||
vehicle=self.vehicle,
|
||||
creator=self.user,
|
||||
display_code='MT010126',
|
||||
purpose='kontrola',
|
||||
)
|
||||
|
||||
def test_send_email_supports_multiple_recipients_and_all_requested_attachments(self):
|
||||
WorkOrderPhoto.objects.create(
|
||||
work_order=self.work_order,
|
||||
image=_test_image('work-order-photo.jpg'),
|
||||
description='Work order image',
|
||||
uploaded_by=self.user,
|
||||
)
|
||||
WorkOrderInvoice.objects.create(
|
||||
work_order=self.work_order,
|
||||
naziv_racuna='Račun 1',
|
||||
image=_test_image('invoice-image.jpg'),
|
||||
created_by=self.user,
|
||||
)
|
||||
note = ServiceContextNote.objects.create(
|
||||
created_by=self.user,
|
||||
recipient=self.user,
|
||||
audience_source='self',
|
||||
work_order=self.work_order,
|
||||
note='Odabrana test bilješka.',
|
||||
)
|
||||
Task.objects.create(
|
||||
title='Mjesečni task',
|
||||
description='Provjera mjesečnog task popisa.',
|
||||
assigned_to=self.user,
|
||||
vehicle=self.vehicle,
|
||||
work_order=self.work_order,
|
||||
scheduled_date=date(2026, 7, 10),
|
||||
)
|
||||
|
||||
with patch('modules.fleet.views.send_work_order_email_bundle_task.delay') as mocked_delay:
|
||||
response = self.client.post(
|
||||
reverse('workorder-send-email', kwargs={'pk': self.work_order.pk}),
|
||||
data={
|
||||
'recipients': ['client@example.test', 'supervisor@example.test'],
|
||||
'subject': 'Test slanje dokumentacije',
|
||||
'message': 'U privitku su svi traženi dokumenti.',
|
||||
'include_work_order_pdf': True,
|
||||
'include_service_records_pdf': True,
|
||||
'include_invoices_pdf': True,
|
||||
'include_images': True,
|
||||
'include_monthly_tasks': True,
|
||||
'month': '2026-07',
|
||||
'service_note_id': str(note.pk),
|
||||
},
|
||||
format='json',
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 202, response.data)
|
||||
self.assertTrue(response.data.get('queued'))
|
||||
self.assertEqual(len(response.data.get('recipients', [])), 2)
|
||||
mocked_delay.assert_called_once()
|
||||
call_kwargs = mocked_delay.call_args.kwargs
|
||||
self.assertEqual(call_kwargs.get('work_order_id'), str(self.work_order.pk))
|
||||
self.assertEqual(call_kwargs.get('requested_by_id'), str(self.user.id))
|
||||
self.assertEqual(call_kwargs.get('month_key'), '2026-07')
|
||||
self.assertEqual(call_kwargs.get('service_note_id'), str(note.pk))
|
||||
|
||||
def test_send_email_keeps_backward_compatible_single_recipient_payload(self):
|
||||
with patch('modules.fleet.views.send_work_order_email_bundle_task.delay') as mocked_delay:
|
||||
response = self.client.post(
|
||||
reverse('workorder-send-email', kwargs={'pk': self.work_order.pk}),
|
||||
data={
|
||||
'recipient': 'single@example.test',
|
||||
'subject': 'Legacy send',
|
||||
},
|
||||
format='json',
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 202, response.data)
|
||||
self.assertTrue(response.data.get('queued'))
|
||||
self.assertEqual(response.data.get('recipients'), ['single@example.test'])
|
||||
mocked_delay.assert_called_once()
|
||||
Reference in New Issue
Block a user