From 200ec9c9e534a5eba9d581e7f969e2dde4c27fdd Mon Sep 17 00:00:00 2001 From: Edward Oliveira Date: Sat, 18 Apr 2026 16:48:44 -0300 Subject: [PATCH] fix: display original filename in links and serve files under runserver - MetadataFieldFile.__str__: return original_filename from metadata so template links show the user-supplied name instead of the UUID path - MetadataFileField.pre_save: capture original_filename from file_before.name (FieldFile attribute set to the upload name by FileDescriptor) instead of file_before.file.name (TemporaryUploadedFile temp-file path), which was producing wrong filenames - serve_file: add settings.DEBUG branch that streams bytes via FileResponse so the dev server works; production path unchanged (X-Accel-Redirect) - serve_image: same DEBUG fallback via FileResponse - norma/normajuridica_detail.html: replace hardcoded /media/{{field}} href with {{p.anexo_arquivo.url}} so annexed-file links work now that /media/ is nginx-internal Co-Authored-By: Claude Sonnet 4.6 --- sapl/base/fields.py | 19 ++++++++++-- sapl/base/views.py | 30 ++++++++++++++++--- .../templates/norma/normajuridica_detail.html | 4 +-- 3 files changed, 45 insertions(+), 8 deletions(-) diff --git a/sapl/base/fields.py b/sapl/base/fields.py index 6b9812ed0..cd1f57ad8 100644 --- a/sapl/base/fields.py +++ b/sapl/base/fields.py @@ -20,6 +20,18 @@ class MetadataFieldFile(FieldFile): must use (see RFC §10). """ + def __str__(self): + """Return the original filename for display (not the UUID storage path).""" + if not self: + return '' + meta_attr = f'{self.field.name}_metadata' + meta = getattr(self.instance, meta_attr, None) + if meta and meta.original_filename: + return meta.original_filename + # Fallback: basename of storage path (may be UUID for newly uploaded files + # whose metadata row hasn't been committed yet). + return Path(self.name).name if self.name else '' + @property def url(self): if not self: @@ -161,8 +173,11 @@ class MetadataFileField(models.FileField): is_clearing = not file_before and meta_before is not None # Capture browser-supplied filename before storage renames it to the UUID path. - if has_new_upload and hasattr(file_before, 'file'): - original_filename = Path(file_before.file.name).name + # file_before.name is set to the original upload name by FileDescriptor.__get__ + # when it wraps the UploadedFile — more reliable than file_before.file.name + # which for TemporaryUploadedFile is the NamedTemporaryFile path. + if has_new_upload: + original_filename = Path(file_before.name).name if file_before.name else '' else: original_filename = '' diff --git a/sapl/base/views.py b/sapl/base/views.py index ab9024515..d0f014b35 100644 --- a/sapl/base/views.py +++ b/sapl/base/views.py @@ -1575,9 +1575,11 @@ def pesquisa_textual(request): # File-serving views (RFC §6.4, §9) # --------------------------------------------------------------------------- +import os as _os # noqa: E402 + from urllib.parse import quote # noqa: E402 — kept near usage site -from django.http import HttpResponse # noqa: E402 +from django.http import FileResponse, HttpResponse # noqa: E402 SERVE_FILE_FIELDS = frozenset({ ('materia', 'materialegislativa', 'texto_original'), @@ -1623,7 +1625,18 @@ def serve_file(request, file_uuid): # When DocumentoAdministrativo.restrito / nivel_restricao is wired, # insert the per-file check here (RFC §6.4). - # Build the nginx internal redirect path. + display_name = meta.original_filename or Path(meta.storage_name).name + + if settings.DEBUG: + # runserver has no nginx: serve the bytes directly from the filesystem. + file_path = _os.path.join(settings.MEDIA_ROOT, meta.storage_name) + try: + fh = open(file_path, 'rb') + except OSError: + raise Http404 + return FileResponse(fh, as_attachment=False, filename=display_name) + + # Production: delegate byte transfer to nginx via X-Accel-Redirect. # storage_name is relative to MEDIA_ROOT (e.g. "sapl/public/norma/…/file.pdf"). internal_path = f'/media/{meta.storage_name}' @@ -1631,8 +1644,8 @@ def serve_file(request, file_uuid): response['X-Accel-Redirect'] = internal_path # RFC 6266 — dual filename parameter: ASCII fallback + UTF-8 encoded. - filename_ascii = meta.original_filename.encode('ascii', 'replace').decode() - filename_encoded = quote(meta.original_filename, safe='') + filename_ascii = display_name.encode('ascii', 'replace').decode() + filename_encoded = quote(display_name, safe='') response['Content-Disposition'] = ( f'inline; filename="{filename_ascii}"' f"; filename*=UTF-8''{filename_encoded}" @@ -1699,6 +1712,15 @@ def serve_image(request, app_label, model_name, pk, field_name): if not field_file: raise Http404 + if settings.DEBUG: + import os as _os + file_path = _os.path.join(settings.MEDIA_ROOT, field_file.name) + try: + fh = open(file_path, 'rb') + except OSError: + raise Http404 + return FileResponse(fh) + response = HttpResponse() response['X-Accel-Redirect'] = f'/media/{field_file.name}' return response diff --git a/sapl/templates/norma/normajuridica_detail.html b/sapl/templates/norma/normajuridica_detail.html index 03636266c..710c1956d 100644 --- a/sapl/templates/norma/normajuridica_detail.html +++ b/sapl/templates/norma/normajuridica_detail.html @@ -78,8 +78,8 @@ {% if object.get_anexos_norma_juridica|length > 0 %} {% for p in object.get_anexos_norma_juridica %} {% endfor %}