Browse Source

Merge branch '3.1.x' into feat/sessao-relatorio-perf

feat/sessao-relatorio-perf
Edward 2 weeks ago
committed by GitHub
parent
commit
4a19aec802
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 24
      CHANGES.md
  2. 2
      docker/docker-compose.yaml
  3. 19
      sapl/base/migrations/0061_casalegislativa_informacao_geral.py
  4. 4
      sapl/base/models.py
  5. 27
      sapl/base/tests/test_form.py
  6. 13
      sapl/compilacao/utils.py
  7. 44
      sapl/compilacao/views.py
  8. 2
      sapl/settings.py
  9. 2
      sapl/templates/base.html

24
CHANGES.md

@ -1,4 +1,28 @@
3.1.165-RC3 / 2026-09-01
========================
* Corrige SQL injection em DispositivoSearchFragmentFormView (#3856)
* Remove limite de caracteres do campo Informação Geral da Casa (#3848)
* Oculta sessões plenárias não iniciadas da consulta pública (#3845)
* hot-fix: Adiciona MIME Media Types opcionais para CSV.
* hot-fix: Remove alerta errado
* Corrige ordem dos expedientes no PDF do Resumo da Sessão (#3844)
* hot-fix: imprime somente contéudo da ocorrência
* Fix duplicate normas/matérias in author reports and prevent unfiltered queries
* Fix ValueError in audiencia form and AttributeError on HEAD requests
* Fix panel clock source and prevent overlapping polling
* hot-fix: recover epoch from server always
* Corrige polling excessivo e bloqueia não-votantes em /voto-individual/ (#3831)
* Hot-fix: corrige cópia de request.data
* Release: 3.1.165-RC2
* #3821 centralizar geração próximo número para create de matérias legislativas (#3822)
3.1.166-RC0 / 2026-09-01
========================
3.1.165-RC2 / 2026-03-26 3.1.165-RC2 / 2026-03-26
======================== ========================

2
docker/docker-compose.yaml

@ -33,7 +33,7 @@ services:
networks: networks:
- sapl-net - sapl-net
sapl: sapl:
image: interlegis/sapl:3.1.165-RC2 image: interlegis/sapl:3.1.165-RC3
# build: # build:
# context: ../ # context: ../
# dockerfile: ./docker/Dockerfile # dockerfile: ./docker/Dockerfile

19
sapl/base/migrations/0061_casalegislativa_informacao_geral.py

@ -0,0 +1,19 @@
# Generated by Django 2.2.28 on 2026-08-05 12:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('base', '0060_auto_20240812_1628'),
]
operations = [
migrations.AlterField(
model_name='casalegislativa',
name='informacao_geral',
field=models.TextField(blank=True,
verbose_name='Informação Geral'),
),
]

4
sapl/base/models.py

@ -73,8 +73,10 @@ class CasaLegislativa(models.Model):
max_length=100, blank=True, verbose_name=_('HomePage')) max_length=100, blank=True, verbose_name=_('HomePage'))
email = models.EmailField( email = models.EmailField(
max_length=100, blank=True, verbose_name=_('E-mail')) max_length=100, blank=True, verbose_name=_('E-mail'))
# Campo editado via editor de texto rico (TinyMCE), portanto o valor
# armazenado é HTML. Não deve ter max_length: as tags consomem o limite
# e impedem o salvamento de textos curtos.
informacao_geral = models.TextField( informacao_geral = models.TextField(
max_length=100,
blank=True, blank=True,
verbose_name=_('Informação Geral')) verbose_name=_('Informação Geral'))

27
sapl/base/tests/test_form.py

@ -39,3 +39,30 @@ def test_casa_legislativa_form_invalido():
}) })
assert not form.is_valid() assert not form.is_valid()
@pytest.mark.django_db(transaction=False)
def test_casa_legislativa_form_informacao_geral_sem_limite():
# O campo é editado por editor de texto rico, então o valor submetido é
# HTML: as tags não podem consumir um limite de caracteres.
html = ''.join('<p>%s</p>' % linha for linha in
['Câmara Municipal', 'Rua Osvaldo Cruz, 555 - Centro',
'Expediente das 8h às 12h e das 13h30 às 17h30'])
assert len(html) > 100
form = CasaLegislativaForm(data={'codigo': 'codigo',
'nome': 'nome',
'sigla': 'sg',
'endereco': 'endereco',
'cep': '70000-000',
'municipio': 'municipio',
'uf': 'DF',
'telefone': '33333333',
'fax': '33333333',
'endereco_web': '',
'email': '',
'informacao_geral': html
})
assert form.is_valid(), form.errors
assert form.cleaned_data['informacao_geral'] == html

13
sapl/compilacao/utils.py

@ -1,5 +1,7 @@
import sys import sys
from django.utils.translation import ugettext_lazy as _
DISPOSITIVO_SELECT_RELATED = ( DISPOSITIVO_SELECT_RELATED = (
'tipo_dispositivo', 'tipo_dispositivo',
'ta_publicado', 'ta_publicado',
@ -66,3 +68,14 @@ def get_integrations_view_names():
if 'IntegracaoTaView' in str(base): if 'IntegracaoTaView' in str(base):
result.append(v) result.append(v)
return result return result
def valid_int(value):
if value:
try:
int(value)
except (TypeError, ValueError):
raise ValueError(
_('Valor "%(value)s" não é um número inteiro válido.') % {
'value': value})
return value

44
sapl/compilacao/views.py

@ -49,7 +49,7 @@ from sapl.compilacao.models import (STATUS_TA_EDITION, STATUS_TA_PRIVATE,
VeiculoPublicacao, Vide) VeiculoPublicacao, Vide)
from sapl.compilacao.utils import (DISPOSITIVO_SELECT_RELATED, from sapl.compilacao.utils import (DISPOSITIVO_SELECT_RELATED,
DISPOSITIVO_SELECT_RELATED_EDIT, DISPOSITIVO_SELECT_RELATED_EDIT,
get_integrations_view_names) get_integrations_view_names, valid_int)
from sapl.crud.base import RP_DETAIL, RP_LIST, Crud, CrudAux, CrudListView,\ from sapl.crud.base import RP_DETAIL, RP_LIST, Crud, CrudAux, CrudListView,\
make_pagination make_pagination
from sapl.settings import BASE_DIR from sapl.settings import BASE_DIR
@ -3177,14 +3177,14 @@ class DispositivoSearchFragmentFormView(ListView):
result = [] result = []
try: try:
tipo_model = self.request.GET.get('tipo_model', '') tipo_model = valid_int(self.request.GET.get('tipo_model', ''))
limit = int(self.request.GET.get('max_results', 100)) limit = int(self.request.GET.get('max_results', 100))
tipo_ta = self.request.GET.get('tipo_ta', '') tipo_ta = valid_int(self.request.GET.get('tipo_ta', ''))
num_ta = self.request.GET.get('num_ta', '') num_ta = self.request.GET.get('num_ta', '')
ano_ta = self.request.GET.get('ano_ta', '') ano_ta = valid_int(self.request.GET.get('ano_ta', ''))
rotulo = self.request.GET.get('rotulo', '') rotulo = self.request.GET.get('rotulo', '')
str_texto = self.request.GET.get('texto', '') str_texto = self.request.GET.get('texto', '')
texto = str_texto.split(' ') texto_valores = str_texto.split(' ')
tipo_resultado = self.request.GET.get('tipo_resultado', '') tipo_resultado = self.request.GET.get('tipo_resultado', '')
tipo_resultado = '' if tipo_resultado == 'False' else tipo_resultado tipo_resultado = '' if tipo_resultado == 'False' else tipo_resultado
@ -3228,20 +3228,24 @@ class DispositivoSearchFragmentFormView(ListView):
AND_CONTROLS = '''AND td.dispositivo_de_alteracao = true AND_CONTROLS = '''AND td.dispositivo_de_alteracao = true
AND td.dispositivo_de_articulacao = true''' AND td.dispositivo_de_articulacao = true'''
texto = list(map("d.texto ~* '{}'".format, texto)) texto = ['d.texto ~* %s'] * len(texto_valores)
AND_TEXTO_ROTULO = '' AND_TEXTO_ROTULO = ''
texto_rotulo_params = []
if str_texto and rotulo: if str_texto and rotulo:
AND_TEXTO_ROTULO = '''AND ( ({BUSCA_TEXTO} AND d.rotulo ~* '{BUSCA_ROTULO}') OR busca_texto = ' AND '.join(texto)
({BUSCA_TEXTO} AND d.rotulo = '' AND dp.rotulo ~* '{BUSCA_ROTULO}') AND_TEXTO_ROTULO = '''AND ( ({BUSCA_TEXTO} AND d.rotulo ~* %s) OR
({BUSCA_TEXTO} AND d.rotulo = '' AND dp.rotulo ~* %s)
)'''.format( )'''.format(
BUSCA_TEXTO=' AND '.join(texto), BUSCA_TEXTO=busca_texto,
BUSCA_ROTULO=rotulo
) )
texto_rotulo_params = (
texto_valores + [rotulo] + texto_valores + [rotulo])
elif str_texto: elif str_texto:
AND_TEXTO_ROTULO = ' AND %s' % ' AND '.join(texto) AND_TEXTO_ROTULO = ' AND ' + ' AND '.join(texto)
texto_rotulo_params = texto_valores
elif rotulo: elif rotulo:
AND_TEXTO_ROTULO = "AND d.rotulo ~* '{BUSCA_ROTULO}'".format( AND_TEXTO_ROTULO = "AND d.rotulo ~* %s"
BUSCA_ROTULO=rotulo) texto_rotulo_params = [rotulo]
else: else:
AND_TEXTO_ROTULO = '' AND_TEXTO_ROTULO = ''
@ -3288,17 +3292,21 @@ class DispositivoSearchFragmentFormView(ListView):
AND3_TIPO_TA="AND ta.tipo_ta_id = {}".format( AND3_TIPO_TA="AND ta.tipo_ta_id = {}".format(
tipo_ta.id) if tipo_ta else '', tipo_ta.id) if tipo_ta else '',
AND2_ANO="AND ta.ano = {}".format( AND2_ANO="AND ta.ano = %s" if ano_ta else '',
ano_ta) if ano_ta else '',
AND1_NUMERO="AND ta.numero ~* '{}'".format( AND1_NUMERO="AND ta.numero ~* %s" if num_ta else '',
num_ta) if num_ta else '',
AND_TEXTO_ROTULO=AND_TEXTO_ROTULO if AND_TEXTO_ROTULO else '', AND_TEXTO_ROTULO=AND_TEXTO_ROTULO if AND_TEXTO_ROTULO else '',
AND_CONTROLS=AND_CONTROLS if AND_CONTROLS else '' AND_CONTROLS=AND_CONTROLS if AND_CONTROLS else ''
) )
result = Dispositivo.objects.raw(sql) params = texto_rotulo_params.copy()
if num_ta:
params.append(num_ta)
if ano_ta:
params.append(ano_ta)
result = Dispositivo.objects.raw(sql, params)
r = [] r = []
ids = set() ids = set()

2
sapl/settings.py

@ -43,7 +43,7 @@ ALLOWED_HOSTS = ['*']
LOGIN_REDIRECT_URL = '/' LOGIN_REDIRECT_URL = '/'
LOGIN_URL = '/login/?next=' LOGIN_URL = '/login/?next='
SAPL_VERSION = '3.1.165-RC2' SAPL_VERSION = '3.1.165-RC3'
if DEBUG: if DEBUG:
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

2
sapl/templates/base.html

@ -200,7 +200,7 @@
<small> <small>
Desenvolvido pelo <a href="http://www.interlegis.leg.br/">Interlegis</a> em software livre e aberto. Desenvolvido pelo <a href="http://www.interlegis.leg.br/">Interlegis</a> em software livre e aberto.
</small> </small>
<span>Release: 3.1.165-RC2</span> <span>Release: 3.1.165-RC3</span>
</p> </p>
</div> </div>

Loading…
Cancel
Save