diff --git a/docker/config/nginx/sapl.conf b/docker/config/nginx/sapl.conf
index a55731d02..015538c96 100644
--- a/docker/config/nginx/sapl.conf
+++ b/docker/config/nginx/sapl.conf
@@ -4,6 +4,13 @@ upstream sapl_server {
}
+# Se o cliente já manda X-Request-ID, reaproveita; senão, usa $request_id (nginx)
+map $http_x_request_id $req_id {
+ default $http_x_request_id;
+ "" $request_id;
+}
+
+
server {
listen 80;
@@ -30,7 +37,9 @@ server {
return 204;
}
+ proxy_set_header X-Request-ID $req_id;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass http://sapl_server;
@@ -45,10 +54,11 @@ server {
}
location / {
+ proxy_set_header X-Request-ID $req_id;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_redirect off;
-
proxy_pass http://sapl_server;
}
diff --git a/sapl/audiencia/migrations/0020_auto_20251201_1450.py b/sapl/audiencia/migrations/0020_auto_20251201_1450.py
new file mode 100644
index 000000000..6afbd9b0e
--- /dev/null
+++ b/sapl/audiencia/migrations/0020_auto_20251201_1450.py
@@ -0,0 +1,28 @@
+# Generated by Django 2.2.28 on 2025-12-01 17:50
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('audiencia', '0019_auto_20240711_1400'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='audienciapublica',
+ name='ano',
+ field=models.PositiveSmallIntegerField(choices=[(2026, 2026), (2025, 2025), (2024, 2024), (2023, 2023), (2022, 2022), (2021, 2021), (2020, 2020), (2019, 2019), (2018, 2018), (2017, 2017), (2016, 2016), (2015, 2015), (2014, 2014), (2013, 2013), (2012, 2012), (2011, 2011), (2010, 2010), (2009, 2009), (2008, 2008), (2007, 2007), (2006, 2006), (2005, 2005), (2004, 2004), (2003, 2003), (2002, 2002), (2001, 2001), (2000, 2000), (1999, 1999), (1998, 1998), (1997, 1997), (1996, 1996), (1995, 1995), (1994, 1994), (1993, 1993), (1992, 1992), (1991, 1991), (1990, 1990), (1989, 1989), (1988, 1988), (1987, 1987), (1986, 1986), (1985, 1985), (1984, 1984), (1983, 1983), (1982, 1982), (1981, 1981), (1980, 1980), (1979, 1979), (1978, 1978), (1977, 1977), (1976, 1976), (1975, 1975), (1974, 1974), (1973, 1973), (1972, 1972), (1971, 1971), (1970, 1970), (1969, 1969), (1968, 1968), (1967, 1967), (1966, 1966), (1965, 1965), (1964, 1964), (1963, 1963), (1962, 1962), (1961, 1961), (1960, 1960), (1959, 1959), (1958, 1958), (1957, 1957), (1956, 1956), (1955, 1955), (1954, 1954), (1953, 1953), (1952, 1952), (1951, 1951), (1950, 1950), (1949, 1949), (1948, 1948), (1947, 1947), (1946, 1946), (1945, 1945), (1944, 1944), (1943, 1943), (1942, 1942), (1941, 1941), (1940, 1940), (1939, 1939), (1938, 1938), (1937, 1937), (1936, 1936), (1935, 1935), (1934, 1934), (1933, 1933), (1932, 1932), (1931, 1931), (1930, 1930), (1929, 1929), (1928, 1928), (1927, 1927), (1926, 1926), (1925, 1925), (1924, 1924), (1923, 1923), (1922, 1922), (1921, 1921), (1920, 1920), (1919, 1919), (1918, 1918), (1917, 1917), (1916, 1916), (1915, 1915), (1914, 1914), (1913, 1913), (1912, 1912), (1911, 1911), (1910, 1910), (1909, 1909), (1908, 1908), (1907, 1907), (1906, 1906), (1905, 1905), (1904, 1904), (1903, 1903), (1902, 1902), (1901, 1901), (1900, 1900), (1899, 1899), (1898, 1898), (1897, 1897), (1896, 1896), (1895, 1895), (1894, 1894), (1893, 1893), (1892, 1892), (1891, 1891), (1890, 1890)], verbose_name='Ano'),
+ ),
+ migrations.AlterField(
+ model_name='audienciapublica',
+ name='nome',
+ field=models.CharField(max_length=250, verbose_name='Nome da Audiência Pública'),
+ ),
+ migrations.AlterField(
+ model_name='audienciapublica',
+ name='tema',
+ field=models.CharField(max_length=250, verbose_name='Tema da Audiência Pública'),
+ ),
+ ]
diff --git a/sapl/audiencia/models.py b/sapl/audiencia/models.py
index e5d011762..6b74cffd0 100755
--- a/sapl/audiencia/models.py
+++ b/sapl/audiencia/models.py
@@ -65,9 +65,9 @@ class AudienciaPublica(models.Model):
ano = models.PositiveSmallIntegerField(verbose_name=_('Ano'),
choices=RANGE_ANOS)
nome = models.CharField(
- max_length=100, verbose_name=_('Nome da Audiência Pública'))
+ max_length=250, verbose_name=_('Nome da Audiência Pública'))
tema = models.CharField(
- max_length=100, verbose_name=_('Tema da Audiência Pública'))
+ max_length=250, verbose_name=_('Tema da Audiência Pública'))
data = models.DateField(verbose_name=_('Data'))
hora_inicio = models.CharField(
max_length=5, verbose_name=_('Horário Início(hh:mm)'))
diff --git a/sapl/logging/filters.py b/sapl/logging/filters.py
new file mode 100644
index 000000000..1950559ef
--- /dev/null
+++ b/sapl/logging/filters.py
@@ -0,0 +1,21 @@
+# sapl/logging/filters.py
+import logging
+import contextvars
+
+_request_id = contextvars.ContextVar("request_id", default="-")
+
+
+def set_request_id(value: str):
+ _request_id.set(value)
+
+
+def get_request_id() -> str:
+ return _request_id.get()
+
+
+class RequestIdFilter(logging.Filter):
+ def filter(self, record: logging.LogRecord) -> bool:
+ # garante que SEMPRE existe
+ if not hasattr(record, "request_id"):
+ record.request_id = get_request_id()
+ return True
diff --git a/sapl/middleware/__init__.py b/sapl/middleware/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/sapl/middleware.py b/sapl/middleware/check_password.py
similarity index 100%
rename from sapl/middleware.py
rename to sapl/middleware/check_password.py
diff --git a/sapl/endpoint_restriction_middleware.py b/sapl/middleware/endpoint_restriction.py
similarity index 100%
rename from sapl/endpoint_restriction_middleware.py
rename to sapl/middleware/endpoint_restriction.py
diff --git a/sapl/middleware/request_id.py b/sapl/middleware/request_id.py
new file mode 100644
index 000000000..48be59cfb
--- /dev/null
+++ b/sapl/middleware/request_id.py
@@ -0,0 +1,24 @@
+import uuid
+from sapl.logging.filters import set_request_id
+
+HEADER_NAME = "HTTP_X_REQUEST_ID"
+RESPONSE_HEADER = "X-Request-ID"
+
+
+def _new_id():
+ return uuid.uuid4().hex
+
+
+class RequestIdMiddleware:
+ def __init__(self, get_response):
+ self.get_response = get_response
+
+ def __call__(self, request):
+ # recebe `request_id` do nginx ou do cliente senão cria um
+ request_id = request.META.get(HEADER_NAME) or _new_id()
+ request_id = str(request_id)[:64]
+ request.request_id = request_id
+ set_request_id(request_id)
+ response = self.get_response(request)
+ response[RESPONSE_HEADER] = request_id
+ return response
diff --git a/sapl/painel/views.py b/sapl/painel/views.py
index a4c9e2da3..cea4e2870 100644
--- a/sapl/painel/views.py
+++ b/sapl/painel/views.py
@@ -93,7 +93,7 @@ def votacao_aberta(request):
return votacoes_abertas.first(), None
-def votacao(context,context_vars):
+def votacao(context, context_vars):
logger = logging.getLogger(__name__)
parlamentar = context_vars['votante'].parlamentar
parlamentar_presente = False
@@ -204,7 +204,7 @@ def votante_view(request):
username = request.user.username if request.user.is_authenticated else 'AnonymousUser'
# Pega o votante relacionado ao usuário
- template_name = 'painel/voto_nominal.html'
+ template_name = 'painel/voto_individual.html'
context = {}
context_vars = {}
@@ -237,13 +237,15 @@ def votante_view(request):
if request.method == 'POST':
if context_vars['ordem_dia']:
try:
- logger.info("user=" + username + ". Tentando obter objeto VotoParlamentar para parlamentar={} e ordem={}."
+ logger.info("user=" + username + ". Tentando obter objeto VotoParlamentar para parlamentar={} e "
+ "ordem={}. "
.format(context_vars['parlamentar'], context_vars['ordem_dia']))
voto = VotoParlamentar.objects.get(
parlamentar=context_vars['parlamentar'],
ordem=context_vars['ordem_dia'])
except ObjectDoesNotExist:
- logger.error("user=" + username + ". Erro ao obter VotoParlamentar para parlamentar={} e ordem={}. Criando objeto."
+ logger.error("user=" + username + ". Erro ao obter VotoParlamentar para parlamentar={} e ordem={}. "
+ "Criando objeto. "
.format(context_vars['parlamentar'], context_vars['ordem_dia']))
voto = VotoParlamentar.objects.create(
parlamentar=context_vars['parlamentar'],
diff --git a/sapl/parlamentares/migrations/0045_auto_20251201_1531.py b/sapl/parlamentares/migrations/0045_auto_20251201_1531.py
new file mode 100644
index 000000000..2b04bc989
--- /dev/null
+++ b/sapl/parlamentares/migrations/0045_auto_20251201_1531.py
@@ -0,0 +1,23 @@
+# Generated by Django 2.2.28 on 2025-12-01 18:31
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('parlamentares', '0044_adiciona_cargos_mesa'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='parlamentar',
+ name='nome_completo',
+ field=models.CharField(max_length=80, verbose_name='Nome Completo'),
+ ),
+ migrations.AlterField(
+ model_name='parlamentar',
+ name='nome_parlamentar',
+ field=models.CharField(max_length=80, verbose_name='Nome Parlamentar'),
+ ),
+ ]
diff --git a/sapl/parlamentares/models.py b/sapl/parlamentares/models.py
index de451b555..eecc2f314 100644
--- a/sapl/parlamentares/models.py
+++ b/sapl/parlamentares/models.py
@@ -206,9 +206,9 @@ class Parlamentar(models.Model):
on_delete=models.PROTECT,
verbose_name=_('Situação Militar'))
nome_completo = models.CharField(
- max_length=50, verbose_name=_('Nome Completo'))
+ max_length=80, verbose_name=_('Nome Completo'))
nome_parlamentar = models.CharField(
- max_length=50,
+ max_length=80,
verbose_name=_('Nome Parlamentar'))
sexo = models.CharField(
max_length=1, verbose_name=_('Sexo'), choices=SEXO_CHOICE)
diff --git a/sapl/relatorios/forms.py b/sapl/relatorios/forms.py
index c6edc7d49..23145f899 100644
--- a/sapl/relatorios/forms.py
+++ b/sapl/relatorios/forms.py
@@ -70,16 +70,16 @@ class RelatorioDocumentosAcessoriosFilterSet(django_filters.FilterSet):
)
-def ordem_or_expediente(queryset, name, value):
- if value is None:
- return queryset
- value = getattr(value, "pk", value)
- ordem_q = f"ordem__materia__{name}"
- expediente_q = f"expediente__materia__{name}"
- return queryset.filter(Q(**{ordem_q: value}) | Q(**{expediente_q: value}))
+class RelatorioVotacoesNominaisFilterSet(django_filters.FilterSet):
+ def ordem_or_expediente(self, queryset, name, value):
+ if value is None:
+ return queryset
+ value = getattr(value, "pk", value)
+ ordem_q = f"ordem__materia__{name}"
+ expediente_q = f"expediente__materia__{name}"
+ return queryset.filter(Q(**{ordem_q: value}) | Q(**{expediente_q: value}))
-class RelatorioVotacoesNominaisFilterSet(django_filters.FilterSet):
tipo_id = django_filters.ModelChoiceFilter(
queryset=TipoMateriaLegislativa.objects.all(),
method='ordem_or_expediente',
diff --git a/sapl/sessao/forms.py b/sapl/sessao/forms.py
index 73ac911f0..491cdeaca 100644
--- a/sapl/sessao/forms.py
+++ b/sapl/sessao/forms.py
@@ -22,7 +22,8 @@ from sapl.materia.models import (MateriaLegislativa, StatusTramitacao,
from sapl.parlamentares.models import Mandato, Parlamentar
from sapl.protocoloadm.models import TipoDocumentoAdministrativo,\
DocumentoAdministrativo
-from sapl.sessao.models import Correspondencia
+from sapl.sessao.models import Correspondencia, AbstractOrdemDia
+
from sapl.utils import (autor_label, autor_modal,
choice_anos_com_sessaoplenaria,
FileFieldCheckMixin,
@@ -563,6 +564,22 @@ class SessaoPlenariaFilterSet(django_filters.FilterSet):
)
+class AdicionarVariasMateriasForm(forms.Form):
+ check_all = forms.BooleanField(
+ label='Marcar/Desmarcar Todos',
+ required=False,
+ widget=forms.CheckboxInput(
+ attrs={'onchange':'checkAll(this)'}),
+ )
+
+ tipo_votacao = forms.ChoiceField(required=False,
+ choices= AbstractOrdemDia.TIPO_VOTACAO_CHOICES,
+ initial=False,
+ widget=forms.RadioSelect(
+ attrs={'onchange':'marcaTipoVotacao()'}),
+ )
+
+
class AdicionarVariasMateriasFilterSet(MateriaLegislativaFilterSet):
o = MateriaPesquisaOrderingFilter()
diff --git a/sapl/sessao/views.py b/sapl/sessao/views.py
index 7c76d3196..ab30c2c6a 100755
--- a/sapl/sessao/views.py
+++ b/sapl/sessao/views.py
@@ -51,7 +51,7 @@ from sapl.settings import TIME_ZONE, RATE_LIMITER_RATE
from sapl.utils import show_results_filter_set, remover_acentos, get_client_ip, \
MultiFormatOutputMixin, PautaMultiFormatOutputMixin, ratelimit_ip
-from .forms import (AdicionarVariasMateriasFilterSet, BancadaForm,
+from .forms import (AdicionarVariasMateriasFilterSet, AdicionarVariasMateriasForm, BancadaForm,
ExpedienteForm, JustificativaAusenciaForm, OcorrenciaSessaoForm, ListMateriaForm,
MesaForm, OradorExpedienteForm, OradorForm, PautaSessaoFilterSet,
PresencaForm, ResumoOrdenacaoForm, SessaoPlenariaFilterSet,
@@ -4190,6 +4190,8 @@ class AdicionarVariasMateriasExpediente(PermissionRequiredForAppCrudMixin,
qr = self.request.GET.copy()
+ form = AdicionarVariasMateriasForm
+ context['form'] = form
context['filter_url'] = ('&' + qr.urlencode()) if len(qr) > 0 else ''
context['pk_sessao'] = self.kwargs['pk']
diff --git a/sapl/settings.py b/sapl/settings.py
index 20ff48ad7..559757d4f 100644
--- a/sapl/settings.py
+++ b/sapl/settings.py
@@ -138,11 +138,12 @@ HAYSTACK_CONNECTIONS = {
}
MIDDLEWARE = [
+ 'sapl.middleware.request_id.RequestIdMiddleware',
'django_prometheus.middleware.PrometheusBeforeMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware',
- 'sapl.endpoint_restriction_middleware.EndpointRestrictionMiddleware',
+ 'sapl.middleware.endpoint_restriction.EndpointRestrictionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
@@ -150,7 +151,7 @@ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
'waffle.middleware.WaffleMiddleware',
- 'sapl.middleware.CheckWeakPasswordMiddleware',
+ 'sapl.middleware.check_password.CheckWeakPasswordMiddleware',
'django_prometheus.middleware.PrometheusAfterMiddleware',
]
if DEBUG:
@@ -421,32 +422,37 @@ LOGGING = {
'require_debug_true': {
'()': 'django.utils.log.RequireDebugTrue',
},
+ 'request_id': {
+ "()": 'sapl.logging.filters.RequestIdFilter',
+ },
},
'formatters': {
'verbose': {
- 'format': '%(levelname)s %(asctime)s ' + host + ' %(pathname)s %(name)s:%(funcName)s:%(lineno)d %(message)s'
+ 'format': '%(levelname)s %(asctime)s [%(request_id)s] ' + host + '%(pathname)s %(name)s:%(funcName)s:%('
+ 'lineno)d %(message)s '
},
'simple': {
- 'format': '%(levelname)s %(asctime)s - %(message)s'
+ 'format': '%(levelname)s %(asctime)s [%(request_id)s] - %(message)s'
},
},
'handlers': {
'console': {
'level': 'INFO',
'class': 'logging.StreamHandler',
- 'filters': ['require_debug_true'],
+ 'filters': ['request_id', 'require_debug_true'],
'formatter': 'simple',
},
'console_verbose': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
- 'filters': ['require_debug_true'],
+ 'filters': ['request_id', 'require_debug_true'],
'formatter': 'verbose',
},
'applogfile': {
'level': 'INFO',
'class': 'logging.handlers.RotatingFileHandler',
'filename': 'sapl.log',
+ 'filters': ['request_id'],
'maxBytes': 1024 * 1024 * 15, # 15MB
'backupCount': 10,
'formatter': 'verbose',
diff --git a/sapl/templates/norma/layouts.yaml b/sapl/templates/norma/layouts.yaml
index a44445bfb..733c3eb8d 100644
--- a/sapl/templates/norma/layouts.yaml
+++ b/sapl/templates/norma/layouts.yaml
@@ -32,7 +32,7 @@ AnexoNormaJuridica:
NormaJuridicaCreate:
{% trans 'Identificação Básica' %}:
- - orgao tipo:5 numero:2 ano:2
+ - orgao tipo:5 ano:2 numero:2
- data esfera_federacao complemento
- tipo_materia numero_materia ano_materia
- data_publicacao veiculo_publicacao data_vigencia pagina_inicio_publicacao pagina_fim_publicacao
diff --git a/sapl/templates/painel/voto_nominal.html b/sapl/templates/painel/voto_individual.html
similarity index 97%
rename from sapl/templates/painel/voto_nominal.html
rename to sapl/templates/painel/voto_individual.html
index ecd915c25..c35daa5fa 100644
--- a/sapl/templates/painel/voto_nominal.html
+++ b/sapl/templates/painel/voto_individual.html
@@ -132,9 +132,6 @@
{% render_bundle 'painel' 'js' %}
{% endblock extra_js %}
diff --git a/sapl/templates/sessao/blocos_ata/materias_expediente.html b/sapl/templates/sessao/blocos_ata/materias_expediente.html
index 63cbb8291..8168ccb84 100644
--- a/sapl/templates/sessao/blocos_ata/materias_expediente.html
+++ b/sapl/templates/sessao/blocos_ata/materias_expediente.html
@@ -24,7 +24,7 @@
Abstenções: {{ m.voto_abstencoes }},
{% endif %}
{% endif %}
- Resultado: {{ m.resultado }}
+ Resultado: {{ m.resultado }}
{% if m.resultado_observacao %} - Obs.: {{ m.resultado_observacao }} {% endif %}
{% if m.voto_nominal %}
Votos Nominais :
diff --git a/sapl/templates/sessao/blocos_ata/materias_ordem_dia.html b/sapl/templates/sessao/blocos_ata/materias_ordem_dia.html
index 8f14181cc..92ee3789b 100644
--- a/sapl/templates/sessao/blocos_ata/materias_ordem_dia.html
+++ b/sapl/templates/sessao/blocos_ata/materias_ordem_dia.html
@@ -25,7 +25,7 @@
Abstenções: {{ m.voto_abstencoes }},
{% endif %}
{% endif %}
- Resultado: {{ m.resultado }}
+ Resultado: {{ m.resultado }}
{% if m.resultado_observacao %} - Obs.: {{ m.resultado_observacao }} {% endif %}
{% if m.voto_nominal %}
Votos Nominais :