fix: invalidate stale service records PDF cache on service record changes

Ensure admin/API deletes and updates of service records, photos, and
attachments invalidate the cached service-records PDF. Also harden cached
PDF lookup/download paths so a missing file is treated as stale cache
instead of a 500.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
mariomitte
2026-08-31 07:32:05 +02:00
parent 9bd5f53675
commit 2826ee9bab
3 changed files with 151 additions and 4 deletions

View File

@@ -532,10 +532,30 @@ def _get_cached_pdf(work_order, pdf_type):
)
for candidate in candidates:
if str(candidate.filename or '').strip() == expected_filename:
return candidate
file_name = getattr(candidate.file, 'name', '')
storage = getattr(candidate.file, 'storage', None)
if file_name and storage:
try:
if storage.exists(file_name):
return candidate
except (FileNotFoundError, OSError, ValueError):
pass
candidate.file = None
candidate.is_active = False
candidate.status = 'failed'
candidate.error_message = 'PDF datoteka nije dostupna.'
candidate.save(update_fields=['file', 'is_active', 'status', 'error_message', 'updated_at'])
return None
def _mark_generated_pdf_failed(generated_pdf, error_message):
generated_pdf.file = None
generated_pdf.is_active = False
generated_pdf.status = 'failed'
generated_pdf.error_message = error_message
generated_pdf.save(update_fields=['file', 'is_active', 'status', 'error_message', 'updated_at'])
def _cleanup_expired_generated_pdfs():
now = timezone.now()
expired = GeneratedWorkOrderPdf.objects.filter(
@@ -546,10 +566,11 @@ def _cleanup_expired_generated_pdfs():
for item in expired:
if item.file:
item.file.delete(save=False)
item.file = None
item.is_active = False
item.status = 'failed'
item.error_message = 'PDF cache istekao.'
item.save(update_fields=['is_active', 'status', 'error_message', 'updated_at'])
item.save(update_fields=['file', 'is_active', 'status', 'error_message', 'updated_at'])
def _parse_amount_decimal(value):
@@ -572,10 +593,11 @@ def _invalidate_work_order_pdf_cache(work_order, *, pdf_types=None):
for cached in cache_qs:
if cached.file:
cached.file.delete(save=False)
cached.file = None
cached.is_active = False
cached.status = 'failed'
cached.error_message = 'PDF cache invalidiran zbog promjene podataka.'
cached.save(update_fields=['is_active', 'status', 'error_message', 'updated_at'])
cached.save(update_fields=['file', 'is_active', 'status', 'error_message', 'updated_at'])
def _upsert_additional_cost_row_from_invoice(invoice):
@@ -607,7 +629,11 @@ def _upsert_additional_cost_row_from_invoice(invoice):
def _cached_pdf_file_response(generated_pdf, *, default_filename):
generated_pdf.file.open('rb')
try:
generated_pdf.file.open('rb')
except (FileNotFoundError, OSError, ValueError):
_mark_generated_pdf_failed(generated_pdf, 'PDF datoteka nije dostupna.')
raise DRFValidationError({"detail": "PDF nije dostupan ili je obrisan."})
filename = generated_pdf.filename or default_filename
response = FileResponse(generated_pdf.file, content_type='application/pdf')
response['Content-Disposition'] = f'attachment; filename="{filename}"'
@@ -3666,6 +3692,18 @@ class WorkOrderViewSet(viewsets.ModelViewSet):
).exclude(file='').exclude(file__isnull=True).first()
if generated_pdf is None:
raise DRFValidationError({"detail": "PDF nije dostupan ili je istekao."})
file_name = getattr(generated_pdf.file, 'name', '')
storage = getattr(generated_pdf.file, 'storage', None)
if not file_name or storage is None:
_mark_generated_pdf_failed(generated_pdf, 'PDF datoteka nije dostupna.')
raise DRFValidationError({"detail": "PDF nije dostupan ili je obrisan."})
try:
if not storage.exists(file_name):
_mark_generated_pdf_failed(generated_pdf, 'PDF datoteka nije dostupna.')
raise DRFValidationError({"detail": "PDF nije dostupan ili je obrisan."})
except (FileNotFoundError, OSError, ValueError):
_mark_generated_pdf_failed(generated_pdf, 'PDF datoteka nije dostupna.')
raise DRFValidationError({"detail": "PDF nije dostupan ili je obrisan."})
return _cached_pdf_file_response(generated_pdf, default_filename=_pdf_filename(work_order, generated_pdf.pdf_type))
@action(detail=True, methods=['get'], url_path='pdf-preview')