Browse Source

feat: serve_image view and semantic image URLs for all image fields (RFC §12)

- IMAGE_FIELDS allowlist and serve_image view: cover CasaLegislativa.logotipo,
  Partido.logo_partido, Parlamentar.fotografia, Dispositivo.imagem.  The view
  validates the (app, model, field) triple, fetches the instance, and issues
  X-Accel-Redirect to nginx — same mechanism as serve_file but without
  FileMetadata involvement (images carry no versioning or access-control
  requirement).

- nginx: /media/CACHE/ added as a public exception before the internal
  /media/ block so sorl-thumbnail cached thumbnails (fotografia via
  {% cropped_thumbnail %}) remain accessible to browsers without going through
  Django.

- get_logotipo_url helper in sapl/utils.py: returns the semantic
  /imagens/base/casalegislativa/<pk>/logotipo/ URL; avoids circular imports
  since sapl/base/views.py imports from sapl/relatorios/views.py.

- LogotipoView updated to redirect to the semantic URL instead of the raw
  /media/ path.

- parliament_info context processor: adds logotipo_url to every template
  context so base.html and 404.html can render the logo without MEDIA_URL
  concatenation.

- 5 HTML templates updated: {% if logotipo %}{{ MEDIA_URL }}{{ logotipo }}
  replaced with {% if logotipo_url %}{{ logotipo_url }}.

- relatorios/views.py (4 sites): logotipo_url added to header_context dicts
  passed to header_ata.html.

- painel/views.py: brasao computed via get_logotipo_url instead of
  casa.logotipo.url (which returns the now-internal /media/ path).

- ImageThumbnailFileInput.get_context: computes semantic_url from IMAGE_FIELDS
  when the instance has a PK; image_thumbnail.html uses it as the src fallback
  so the edit-form preview remains visible after /media/ became internal.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
file-metafields
Edward Ribeiro 2 weeks ago
parent
commit
333bcf743d
  1. 4
      docker/config/nginx/sapl.conf
  2. 47
      sapl/base/views.py
  3. 6
      sapl/context_processors.py
  4. 4
      sapl/painel/views.py
  5. 12
      sapl/relatorios/views.py
  6. 2
      sapl/templates/404.html
  7. 2
      sapl/templates/base.html
  8. 2
      sapl/templates/materia/recibo_proposicao.html
  9. 2
      sapl/templates/protocoloadm/comprovante.html
  10. 2
      sapl/templates/relatorios/header_ata.html
  11. 4
      sapl/templates/widgets/image_thumbnail.html
  12. 6
      sapl/urls.py
  13. 37
      sapl/utils.py

4
docker/config/nginx/sapl.conf

@ -49,6 +49,10 @@ server {
alias /var/interlegis/sapl/collected_static/;
}
location /media/CACHE/ {
alias /var/interlegis/sapl/media/CACHE/;
}
location /media/ {
internal;
alias /var/interlegis/sapl/media/;

47
sapl/base/views.py

@ -1513,8 +1513,8 @@ class LogotipoView(RedirectView):
def get_redirect_url(self, *args, **kwargs):
casa = get_casalegislativa()
logo = casa and casa.logotipo and casa.logotipo.name
return os.path.join(settings.MEDIA_URL, logo) if logo else STATIC_LOGO
url = get_logotipo_url(casa)
return url if url else STATIC_LOGO
def filtro_campos(dicionario):
@ -1664,3 +1664,46 @@ def serve_model_file(request, app_label, model_name, pk, field_name):
raise Http404
return serve_file(request, file_uuid=meta.uuid)
# Image fields served via X-Accel-Redirect — same nginx internal mechanism as
# serve_file but without FileMetadata involvement (images carry no versioning or
# access-control requirement). An explicit allowlist prevents arbitrary ORM
# traversal (RFC §12.3).
IMAGE_FIELDS = frozenset([
('base', 'casalegislativa', 'logotipo'),
('parlamentares', 'partido', 'logo_partido'),
('parlamentares', 'parlamentar', 'fotografia'),
('compilacao', 'dispositivo', 'imagem'),
])
def serve_image(request, app_label, model_name, pk, field_name):
"""
Serve an image field via nginx X-Accel-Redirect (RFC §12.4).
All four image field models are unconditionally public no permission
check is performed. The allowlist is the only gate.
"""
from django.shortcuts import get_object_or_404 as _get_or_404
if (app_label, model_name, field_name) not in IMAGE_FIELDS:
raise Http404
try:
model = apps.get_model(app_label, model_name)
except LookupError:
raise Http404
instance = _get_or_404(model, pk=pk)
field_file = getattr(instance, field_name, None)
if not field_file:
raise Http404
response = HttpResponse()
response['X-Accel-Redirect'] = f'/media/{field_file.name}'
return response
def get_logotipo_url(casa):
from sapl.utils import get_logotipo_url as _get_logotipo_url
return _get_logotipo_url(casa)

6
sapl/context_processors.py

@ -9,10 +9,12 @@ from sapl.utils import mail_service_configured as mail_service_configured_utils
def parliament_info(request):
from sapl.base.views import get_casalegislativa
from sapl.utils import get_logotipo_url
casa = get_casalegislativa()
if casa:
return casa.__dict__
else:
ctx = dict(casa.__dict__)
ctx['logotipo_url'] = get_logotipo_url(casa)
return ctx
return {}

4
sapl/painel/views.py

@ -22,7 +22,7 @@ from sapl.sessao.models import (ExpedienteMateria, OradorExpediente, OrdemDia,
PresencaOrdemDia, RegistroVotacao,
SessaoPlenaria, SessaoPlenariaPresenca,
VotoParlamentar, RegistroLeitura)
from sapl.utils import filiacao_data, get_client_ip, sort_lista_chave
from sapl.utils import filiacao_data, get_client_ip, sort_lista_chave, get_logotipo_url
from .models import Cronometro
@ -555,7 +555,7 @@ def get_dados_painel(request, pk):
brasao = None
if casa and app_config and (bool(casa.logotipo)):
brasao = casa.logotipo.url \
brasao = get_logotipo_url(casa) \
if app_config.mostrar_brasao_painel else None
response = {

12
sapl/relatorios/views.py

@ -51,7 +51,7 @@ from sapl.sessao.views import (get_identificacao_basica, get_mesa_diretora,
from sapl.settings import MEDIA_URL, RATE_LIMITER_RATE
from sapl.settings import STATIC_ROOT
from sapl.utils import LISTA_DE_UFS, TrocaTag, filiacao_data, create_barcode, show_results_filter_set, \
num_materias_por_tipo, parlamentares_ativos, MultiFormatOutputMixin, ratelimit_ip
num_materias_por_tipo, parlamentares_ativos, MultiFormatOutputMixin, ratelimit_ip, get_logotipo_url
from .templates import (pdf_capa_processo_gerar,
pdf_documento_administrativo_gerar, pdf_espelho_gerar,
pdf_etiqueta_protocolo_gerar, pdf_materia_gerar,
@ -1460,8 +1460,8 @@ def resumo_ata_pdf(request, pk):
'decimo_quinto_ordenacao': 'ocorrencias_da_sessao.html',
'decimo_sexto_ordenacao': 'consideracoes_finais.html'
})
header_context = {"casa": casa,
'logotipo': casa.logotipo, 'MEDIA_URL': MEDIA_URL}
header_context = {"casa": casa, 'logotipo': casa.logotipo,
'logotipo_url': get_logotipo_url(casa), 'MEDIA_URL': MEDIA_URL}
html_template = render_to_string('relatorios/relatorio_ata.html', context)
html_header = render_to_string(
@ -1487,6 +1487,7 @@ def cria_relatorio(request, context, html_string, header_info=""):
context.update({'rodape': rodape})
header_context = {"casa": casa, 'logotipo': casa.logotipo,
'logotipo_url': get_logotipo_url(casa),
'MEDIA_URL': MEDIA_URL, 'info': header_info}
html_template = render_to_string(html_string, context)
@ -1713,6 +1714,7 @@ def relatorio_sessao_plenaria_pdf(request, pk):
html_header = render_to_string('relatorios/header_ata.html', {"casa": casa,
"MEDIA_URL": MEDIA_URL,
"logotipo": casa.logotipo,
"logotipo_url": get_logotipo_url(casa),
"info": info})
pdf_file = make_pdf(
@ -1795,8 +1797,8 @@ def relatorio_materia_tramitacao(request, pk):
'rodape': rodape,
'data': dt.today().strftime('%d/%m/%Y'),
'rodape': rodape})
header_context = {"casa": casa,
'logotipo': casa.logotipo, 'MEDIA_URL': MEDIA_URL}
header_context = {"casa": casa, 'logotipo': casa.logotipo,
'logotipo_url': get_logotipo_url(casa), 'MEDIA_URL': MEDIA_URL}
html_template = render_to_string(
'relatorios/relatorio_materia_tramitacao.html', context)

2
sapl/templates/404.html

@ -49,7 +49,7 @@
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="/">
<img src="{% if logotipo %}{{ MEDIA_URL }}{{ logotipo }}{% else %}{% webpack_static 'img/logo.png' %}{% endif %}"
<img src="{% if logotipo_url %}{{ logotipo_url }}{% else %}{% webpack_static 'img/logo.png' %}{% endif %}"
alt="Logo" class="img-responsive visible-md-inline-block visible-lg-inline-block" >
<span class="vcenter">
{# XXX Make better use of translation tags in html blocks ie. actually use the proper blocktrans tag efficiently #}

2
sapl/templates/base.html

@ -117,7 +117,7 @@
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="/">
<img src="{% if logotipo %}{{ MEDIA_URL }}{{ logotipo }}{% else %}{% webpack_static 'img/logo.png' %}{% endif %}"
<img src="{% if logotipo_url %}{{ logotipo_url }}{% else %}{% webpack_static 'img/logo.png' %}{% endif %}"
alt="Logo" class="img-responsive" >
<span class="vcenter">{% if nome %}{{ nome }}{% else %}{% trans 'Câmara/Assembléia não configurada'%}{% endif %}
<br/><small>{{nome_sistema}}</small>

2
sapl/templates/materia/recibo_proposicao.html

@ -24,7 +24,7 @@
<tr>
<td>
<img height="100" width="100"
src="{% if logotipo %}{{ MEDIA_URL }}{{ logotipo }}{% else %}{% webpack_static 'img/logo.png' %}{% endif %}"
src="{% if logotipo_url %}{{ logotipo_url }}{% else %}{% webpack_static 'img/logo.png' %}{% endif %}"
alt="Logotipo"
class="img-responsive visible-lg-inline-block vcenter">
<div>

2
sapl/templates/protocoloadm/comprovante.html

@ -50,7 +50,7 @@
<div class="row">
<div class="col-1">
<img height="90" width="90"
src="{% if logotipo %}{{ MEDIA_URL }}{{ logotipo }}{% else %}{% webpack_static 'img/logo.png' %}{% endif %}"
src="{% if logotipo_url %}{{ logotipo_url }}{% else %}{% webpack_static 'img/logo.png' %}{% endif %}"
alt="Logotipo"
class="img-responsive visible-lg-inline-block vcenter">
</div>

2
sapl/templates/relatorios/header_ata.html

@ -15,7 +15,7 @@
<section id="informations">
<dl>
<dt class="image-header">
<img style="max-height:2cm;max-width:2cm" src="{% if logotipo %}{{ MEDIA_URL }}{{ logotipo }}{% else %}{% webpack_static 'img/logo.png' %}{% endif %}">
<img style="max-height:2cm;max-width:2cm" src="{% if logotipo_url %}{{ logotipo_url }}{% else %}{% webpack_static 'img/logo.png' %}{% endif %}">
</dt>
<dd class="title">
<ul>

4
sapl/templates/widgets/image_thumbnail.html

@ -22,10 +22,10 @@
<div class="controls {{ field_class }}">
<div class="fileupload fileupload-new"
data-provides="fileupload">
{% if widget.value.url %}
{% if widget.semantic_url or widget.value.url %}
<div class="row">
<div class="col-md-12">
<img src="{{ widget.value.url }}"
<img src="{{ widget.semantic_url|default:widget.value.url }}"
height="300" width="300"
alt="{{ widget.value }}" class="img-thumbnail"/>
</div>

6
sapl/urls.py

@ -21,7 +21,7 @@ from django.urls import path
from django.views.generic.base import RedirectView, TemplateView
from django.views.static import serve as view_static_server
from sapl.base.views import serve_file, serve_model_file
from sapl.base.views import serve_file, serve_image, serve_model_file
import sapl.api.urls
import sapl.audiencia.urls
@ -88,6 +88,10 @@ urlpatterns += [
path('<str:app_label>/<str:model_name>/<int:pk>/<str:field_name>/download',
serve_model_file, name='serve_model_file'),
# Semantic image URL: /imagens/<app>/<model>/<pk>/<field>/
path('imagens/<str:app_label>/<str:model_name>/<int:pk>/<str:field_name>/',
serve_image, name='serve_image'),
]

37
sapl/utils.py

@ -334,6 +334,27 @@ class SaplGenericRelation(GenericRelation):
class ImageThumbnailFileInput(ClearableFileInput):
template_name = 'widgets/image_thumbnail.html'
def get_context(self, name, value, attrs):
ctx = super().get_context(name, value, attrs)
if value and hasattr(value, 'instance') and hasattr(value, 'field'):
instance = value.instance
if instance and instance.pk:
field_name = value.field.name
app = instance._meta.app_label
model = instance._meta.model_name
try:
from django.urls import reverse
from sapl.base.views import IMAGE_FIELDS
if (app, model, field_name) in IMAGE_FIELDS:
ctx['widget']['semantic_url'] = reverse(
'serve_image',
kwargs={'app_label': app, 'model_name': model,
'pk': instance.pk, 'field_name': field_name}
)
except Exception:
pass
return ctx
class RangeWidgetOverride(forms.MultiWidget):
@ -1180,6 +1201,22 @@ def from_date_to_datetime_utc(data):
return dt_utc
def get_logotipo_url(casa):
"""Return the semantic /imagens/ URL for casa.logotipo, or None."""
if not (casa and casa.logotipo and casa.pk):
return None
try:
from django.urls import reverse
return reverse('serve_image', kwargs={
'app_label': 'base',
'model_name': 'casalegislativa',
'pk': casa.pk,
'field_name': 'logotipo',
})
except Exception:
return None
class OverwriteStorage(FileSystemStorage):
"""
Solução derivada do gist: https://gist.github.com/fabiomontefuscolo/1584462

Loading…
Cancel
Save