Browse Source

Merge branch '3.1.x' into cria-script-remove-proposicoes-por-autor

pull/3703/head
Edward 2 months ago
committed by GitHub
parent
commit
7fe026d6a2
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 69
      CHANGES.md
  2. 176
      docker/Dockerfile
  3. 2
      docker/config/nginx/nginx.conf
  4. 17
      docker/docker-compose.yaml
  5. 50
      docker/gunicorn_start.sh
  6. 3
      docker/simple_gunicorn.sh
  7. 147
      docker/start.sh
  8. 0
      docker/startup_scripts/create_admin.py
  9. 0
      docker/startup_scripts/genkey.py
  10. 80
      docker/startup_scripts/gunicorn.conf.py
  11. 18
      docker/startup_scripts/solr_cli.py
  12. 289
      docker/startup_scripts/start.sh
  13. 0
      docker/startup_scripts/wait-for-pg.sh
  14. 0
      docker/startup_scripts/wait-for-solr.sh
  15. 74
      frontend/src/__apps/compilacao/js/old/compilacao_edit.js
  16. 6
      frontend/src/__global/js/tinymce/index.js
  17. 3
      frontend/src/__global/scss/layouts/_globals.scss
  18. 768
      frontend/webpack-stats.json
  19. 14525
      package-lock.json
  20. 4
      package.json
  21. 25
      release.sh
  22. 16
      requirements/dev-requirements.txt
  23. 16
      requirements/requirements.txt
  24. 19
      requirements/test-requirements.txt
  25. 17
      sapl/api/views_comissoes.py
  26. 18
      sapl/audiencia/migrations/0019_auto_20240711_1400.py
  27. 1
      sapl/base/apps.py
  28. 2
      sapl/base/email_utils.py
  29. 34
      sapl/base/forms.py
  30. 18
      sapl/base/migrations/0060_auto_20240812_1628.py
  31. 2
      sapl/base/models.py
  32. 18
      sapl/base/receivers.py
  33. 2
      sapl/base/search_indexes.py
  34. 7
      sapl/base/templatetags/common_tags.py
  35. 26
      sapl/base/templatetags/menus.py
  36. 21
      sapl/base/views.py
  37. 2
      sapl/compilacao/forms.py
  38. 23
      sapl/compilacao/models.py
  39. 2
      sapl/compilacao/urls.py
  40. 22
      sapl/context_processors.py
  41. 80
      sapl/crud/base.py
  42. 2
      sapl/lexml/forms.py
  43. 2
      sapl/lexml/models.py
  44. 11
      sapl/materia/forms.py
  45. 33
      sapl/materia/migrations/0086_auto_20240711_1400.py
  46. 26
      sapl/materia/migrations/0087_update_viewdb_materiaemtramitacao.py
  47. 51
      sapl/materia/models.py
  48. 2
      sapl/materia/urls.py
  49. 54
      sapl/materia/views.py
  50. 21
      sapl/middleware.py
  51. 24
      sapl/norma/forms.py
  52. 46
      sapl/norma/migrations/0045_auto_20240711_1405.py
  53. 37
      sapl/norma/models.py
  54. 19
      sapl/norma/views.py
  55. 5
      sapl/parlamentares/forms.py
  56. 1
      sapl/parlamentares/models.py
  57. 30
      sapl/parlamentares/urls.py
  58. 12
      sapl/parlamentares/views.py
  59. 4
      sapl/protocoloadm/forms.py
  60. 23
      sapl/protocoloadm/migrations/0045_auto_20240711_1405.py
  61. 14
      sapl/protocoloadm/views.py
  62. 73
      sapl/relatorios/forms.py
  63. 8
      sapl/relatorios/urls.py
  64. 102
      sapl/relatorios/views.py
  65. 2
      sapl/rules/group_geral.py
  66. 13
      sapl/sessao/models.py
  67. 142
      sapl/sessao/views.py
  68. 137
      sapl/settings.py
  69. 13
      sapl/static/sapl/frontend/css/chunk-vendors.045ec640.css
  70. BIN
      sapl/static/sapl/frontend/css/chunk-vendors.045ec640.css.gz
  71. 13
      sapl/static/sapl/frontend/css/chunk-vendors.299c587b.css
  72. BIN
      sapl/static/sapl/frontend/css/chunk-vendors.299c587b.css.gz
  73. BIN
      sapl/static/sapl/frontend/css/compilacao.0baf3580.css.gz
  74. 2
      sapl/static/sapl/frontend/css/global.042f6737.css
  75. BIN
      sapl/static/sapl/frontend/css/global.042f6737.css.gz
  76. BIN
      sapl/static/sapl/frontend/css/global.45591136.css.gz
  77. BIN
      sapl/static/sapl/frontend/css/painel.e2b9504e.css.gz
  78. BIN
      sapl/static/sapl/frontend/fonts/fa-brands-400.5d18d427.ttf
  79. BIN
      sapl/static/sapl/frontend/fonts/fa-brands-400.5d18d427.ttf.gz
  80. BIN
      sapl/static/sapl/frontend/fonts/fa-brands-400.87587a68.woff2
  81. BIN
      sapl/static/sapl/frontend/fonts/fa-brands-400.9a905705.ttf
  82. BIN
      sapl/static/sapl/frontend/fonts/fa-brands-400.9a905705.ttf.gz
  83. BIN
      sapl/static/sapl/frontend/fonts/fa-brands-400.b6033b54.woff2
  84. BIN
      sapl/static/sapl/frontend/fonts/fa-regular-400.3580b4a9.woff2
  85. BIN
      sapl/static/sapl/frontend/fonts/fa-regular-400.3ccdbd3d.woff2
  86. BIN
      sapl/static/sapl/frontend/fonts/fa-regular-400.67a0fb74.ttf
  87. BIN
      sapl/static/sapl/frontend/fonts/fa-regular-400.67a0fb74.ttf.gz
  88. BIN
      sapl/static/sapl/frontend/fonts/fa-regular-400.81482cd4.ttf
  89. BIN
      sapl/static/sapl/frontend/fonts/fa-regular-400.81482cd4.ttf.gz
  90. BIN
      sapl/static/sapl/frontend/fonts/fa-solid-900.0b0cc8a6.woff2
  91. BIN
      sapl/static/sapl/frontend/fonts/fa-solid-900.69d3141a.ttf
  92. BIN
      sapl/static/sapl/frontend/fonts/fa-solid-900.69d3141a.ttf.gz
  93. BIN
      sapl/static/sapl/frontend/fonts/fa-solid-900.6a8db53d.ttf
  94. BIN
      sapl/static/sapl/frontend/fonts/fa-solid-900.6a8db53d.ttf.gz
  95. BIN
      sapl/static/sapl/frontend/fonts/fa-solid-900.fd0b155c.woff2
  96. BIN
      sapl/static/sapl/frontend/fonts/fa-v4compatibility.2c070fd2.ttf
  97. BIN
      sapl/static/sapl/frontend/fonts/fa-v4compatibility.2c070fd2.ttf.gz
  98. BIN
      sapl/static/sapl/frontend/fonts/fa-v4compatibility.e4efb16c.ttf
  99. BIN
      sapl/static/sapl/frontend/fonts/fa-v4compatibility.e4efb16c.ttf.gz
  100. BIN
      sapl/static/sapl/frontend/img/down_arrow_select.jpg.gz

69
CHANGES.md

@ -1,4 +1,73 @@
3.1.164-RC1 / 2025-08-05
========================
* Adiciona botoes de impressao na pesquisa de doc adm
* Fix: conserta erro em historico proposicao
* Desabilita detalhe de Justificativa Ausência para acesso público
* Adiciona homepage do parlamentar a perfil público
3.1.164-RC0 / 2025-07-09
========================
* Fix: nome casa duplicado
* feat: #3771 (#3772)
* Adiciona metadados para Google crawler
* Remove version e monta mais um volume
* Fix: log de aplicação
* feat: #3769 (#3770)
* fix: #3767 (#3768)
* Release: 3.1.163
3.1.163 / 2025-05-21
====================
* Fix fancycompleter incompatibility when calling shell_plus
3.1.163-RC24 / 2025-05-19
=========================
* Implementa relatórios CSV, XLSX e JSON para Pauta de Sessão (#3744)
* feat: adiciona pydevd-pycharm para facilitar debug usando pycharm ide (#3717)
* Ajusta link do texto integral da Norma Jurídica nos formatos CSV, XLSX e JSON. (#3743)
* Força mudança de senha para senhas fracas
* Fix Chamado #643964 - Multiple definitions in dictionary at byte 0xc854c for key /Info
* Ajusta link do texto original da Matéria nos formatos CSV, XLSX e JSON. (#3742)
* fix: aplica um lazy loading no sortable do jquery ui pelo evento mouseenter. (#3737)
* fix: corrige condição para mostrar btn para TAs.
* fix: add migrate resultante do commit anterior
* fix: torna cep opcional no model CasaLegislativa
3.1.163-RC23 / 2024-07-31
=========================
* Otimiza recuperação de normas relacionadas (#3734)
* hot-fix: resolve imcompatibilidade de regex com versão de produção do postgresql 9.6
3.1.163-RC21 / 2024-06-25
=========================
* fix: descomenta código pertencente ao commit anterior
* fix: elimina dupla extração de assinaturas eletrônicas
* fix: remove listagem pública de OperadorAutor
* fix: altera campo de 'resultado' ao registrar leitura em bloco
* bump version gunicorn in setup.py
* rebuild frontend
* fix dependabot alerts #61 - bump version axios
* fix: ajuste no conjunto inicial de permissões na construção de classe crud
* Impl Mixin para gerar arquivos de pesq em diversos formatos (#3710)
* Fix estatistica norma view (#3707)
* bump version gunicorn, pillow
* Build(deps): Bump pillow from 10.0.1 to 10.3.0 in /requirements (#3709)
* fix: corrige display de data e hora de protocolor manual
* fix: corrige carga de permissões públicas do crud
* Add blank space when SAPN
* fix: corrige bug na pesquisa de impressos etiquetas (#3695)
* feat: adiciona o turno na info de materias na pauta de sessao (#3694)
* fix: invert lógica do sapln_switch
* fix: Update docker-compose.yaml
* Update CHANGES.md
3.1.163-RC20 / 2023-12-04 3.1.163-RC20 / 2023-12-04
========================= =========================

176
docker/Dockerfile

@ -1,73 +1,123 @@
FROM python:3.9-slim-buster # ---------- 1) BUILDER ----------
FROM python:3.12-slim-bookworm AS builder
# Setup env
ENV LANG C.UTF-8 ENV LANG=C.UTF-8 LC_ALL=C.UTF-8 PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 \
ENV LC_ALL C.UTF-8 DEBIAN_FRONTEND=noninteractive \
ENV PYTHONDONTWRITEBYTECODE 1 VENV_DIR=/opt/venv \
ENV PYTHONUNBUFFERED=1 PIP_NO_CACHE_DIR=on
ENV DEBIAN_FRONTEND noninteractive
# Dev headers e toolchain só no builder
ENV BUILD_PACKAGES apt-utils apt-file libpq-dev graphviz-dev build-essential git pkg-config \ RUN apt-get update && apt-get install -y --no-install-recommends \
python3-dev libxml2-dev libjpeg-dev libssl-dev libffi-dev libxslt1-dev \ build-essential git pkg-config \
libcairo2-dev software-properties-common python3-setuptools python3-pip libpq-dev libxml2-dev libjpeg-dev libssl-dev libffi-dev libxslt1-dev \
libcairo2-dev libpango1.0-dev libgdk-pixbuf-2.0-dev libharfbuzz-dev \
## NAO EH PRA TIRAR O vim DA LISTA DE COMANDOS INSTALADOS!!! libfreetype6-dev zlib1g-dev \
ENV RUN_PACKAGES graphviz python3-lxml python3-magic postgresql-client python3-psycopg2 \ && rm -rf /var/lib/apt/lists/*
poppler-utils curl jq bash vim python3-venv tzdata nodejs \
fontconfig ttf-dejavu python nginx # Venv independente do sistema
RUN python -m venv "${VENV_DIR}" \
RUN mkdir -p /var/interlegis/sapl && "${VENV_DIR}/bin/pip" install --upgrade pip setuptools wheel
WORKDIR /build
# Copie APENAS os requirements primeiro para maximizar cache
COPY requirements/ ./requirements/
# Instale os requisitos de produção
# ATENÇÃO: se seu código importa prompt_toolkit em runtime, inclua em requirements.txt:
# prompt_toolkit>=3,<4
RUN "${VENV_DIR}/bin/pip" install -r requirements/requirements.txt
# Opcional: verificação de conflitos (falha cedo se faltar algo)
RUN "${VENV_DIR}/bin/pip" check || true
# ---------- 2) RUNTIME ----------
FROM python:3.12-slim-bookworm AS runtime
ARG WITH_GRAPHVIZ=1
ARG WITH_POPPLER=1
ARG WITH_PSQL_CLIENT=1
ARG WITH_NGINX=1
ENV LANG=C.UTF-8 LC_ALL=C.UTF-8 PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 \
DEBIAN_FRONTEND=noninteractive \
VENV_DIR=/opt/venv \
PATH=/opt/venv/bin:$PATH \
PIP_NO_CACHE_DIR=on
# Pacotes de runtime (sem *-dev)
# Removi python3/python3-venv (já temos o Python da base)
RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends \
curl jq bash tzdata fontconfig tini libmagic1 \
libcairo2 libpango-1.0-0 libpangocairo-1.0-0 libgdk-pixbuf-2.0-0 \
libharfbuzz0b libfreetype6 libjpeg62-turbo zlib1g fonts-dejavu-core; \
if [ "$WITH_GRAPHVIZ" = "1" ]; then apt-get install -y --no-install-recommends graphviz; fi; \
if [ "$WITH_POPPLER" = "1" ]; then apt-get install -y --no-install-recommends poppler-utils; fi; \
if [ "$WITH_PSQL_CLIENT" = "1" ]; then apt-get install -y --no-install-recommends postgresql-client; fi; \
if [ "$WITH_NGINX" = "1" ]; then apt-get install -y --no-install-recommends nginx; fi; \
rm -rf /var/lib/apt/lists/*
# Usuários/grupos (idempotente)
RUN useradd --system --no-create-home --shell /usr/sbin/nologin sapl || true \
&& groupadd -r nginx || true \
&& usermod -aG nginx www-data || true \
&& usermod -aG nginx sapl || true
# Estrutura de diretórios
RUN mkdir -p /var/interlegis/sapl /var/interlegis/sapl/data /var/interlegis/sapl/media /var/interlegis/sapl/run \
&& chown -R root:nginx /var/interlegis/sapl /var/interlegis/sapl/run \
&& chmod -R g+rwX /var/interlegis/sapl \
&& chmod 2775 /var/interlegis/sapl /var/interlegis/sapl/run \
&& find /var/interlegis/sapl -type d -exec chmod g+s {} +
WORKDIR /var/interlegis/sapl/ WORKDIR /var/interlegis/sapl/
ADD . /var/interlegis/sapl/ # Traga o venv pré-instalado
COPY --from=builder ${VENV_DIR} ${VENV_DIR}
RUN apt-get update && \
apt-get upgrade -y && \ # Código da aplicação (depois do venv para aproveitar cache)
apt-get install -y --no-install-recommends $BUILD_PACKAGES $RUN_PACKAGES && \ COPY . /var/interlegis/sapl/
fc-cache -fv && \
pip3 install --no-cache-dir --upgrade pip setuptools && \ # Nginx (somente se instalado)
rm -f /etc/nginx/conf.d/* && \ RUN if [ "$WITH_NGINX" = "1" ]; then \
pip install --no-cache-dir -r /var/interlegis/sapl/requirements/dev-requirements.txt --upgrade setuptools && \ rm -f /etc/nginx/conf.d/*; \
SUDO_FORCE_REMOVE=yes apt-get purge -y --auto-remove $BUILD_PACKAGES && \ cp docker/config/nginx/sapl.conf /etc/nginx/conf.d/sapl.conf; \
apt-get autoremove && apt-get clean && rm -rf /var/lib/apt/lists/* cp docker/config/nginx/nginx.conf /etc/nginx/nginx.conf; \
fi
WORKDIR /var/interlegis/sapl/
ADD . /var/interlegis/sapl/ # Scripts + gunicorn.conf no diretório da app
RUN install -m 755 docker/startup_scripts/start.sh /var/interlegis/sapl/start.sh \
COPY docker/start.sh $HOME && install -m 755 docker/startup_scripts/wait-for-pg.sh /var/interlegis/sapl/wait-for-pg.sh \
COPY docker/solr_cli.py $HOME && install -m 755 docker/startup_scripts/wait-for-solr.sh /var/interlegis/sapl/wait-for-solr.sh \
COPY docker/wait-for-pg.sh $HOME && install -m 644 docker/startup_scripts/solr_cli.py /var/interlegis/sapl/solr_cli.py \
COPY docker/wait-for-solr.sh $HOME && install -m 644 docker/startup_scripts/create_admin.py /var/interlegis/sapl/create_admin.py \
COPY docker/create_admin.py $HOME && install -m 644 docker/startup_scripts/genkey.py /var/interlegis/sapl/genkey.py \
COPY docker/genkey.py $HOME && install -m 644 docker/startup_scripts/gunicorn.conf.py /var/interlegis/sapl/gunicorn.conf.py
COPY docker/gunicorn_start.sh $HOME
# (Se possível, evite copiar .env no build. Use secrets/variáveis em runtime.)
COPY docker/config/nginx/sapl.conf /etc/nginx/conf.d
COPY docker/config/nginx/nginx.conf /etc/nginx/nginx.conf
COPY docker/config/env_dockerfile /var/interlegis/sapl/sapl/.env COPY docker/config/env_dockerfile /var/interlegis/sapl/sapl/.env
RUN python3 manage.py collectstatic --noinput --clear # Logs (só se nginx estiver presente)
RUN if [ "$WITH_NGINX" = "1" ]; then \
# Remove .env(fake) e sapl.db da imagem ln -sf /dev/stdout /var/log/nginx/access.log; \
RUN rm -rf /var/interlegis/sapl/sapl/.env && \ ln -sf /dev/stderr /var/log/nginx/error.log; \
rm -rf /var/interlegis/sapl/sapl.db fi \
&& mkdir -p /var/log/sapl/ \
RUN chmod +x /var/interlegis/sapl/start.sh && \ && ln -sf /var/interlegis/sapl/sapl.log /var/log/sapl/sapl.log
chmod +x /var/interlegis/sapl/wait-for-solr.sh && \
chmod +x /var/interlegis/sapl/wait-for-pg.sh && \
ln -sf /dev/stdout /var/log/nginx/access.log && \
ln -sf /dev/stderr /var/log/nginx/error.log && \
mkdir /var/log/sapl/ && touch /var/interlegis/sapl/sapl.log && \
ln -s /var/interlegis/sapl/sapl.log /var/log/sapl/sapl.log
# Debian não possui usuário 'nginx' necessário para o Debian
RUN useradd --no-create-home nginx
ENV DEBIAN_FRONTEND teletype # Cache de fontes e collectstatic
# NÃO atualizamos pip aqui (já veio pronto do builder)
RUN fc-cache -fv \
&& python manage.py collectstatic --noinput --clear \
&& rm -f /var/interlegis/sapl/sapl/.env /var/interlegis/sapl/sapl.db || true
EXPOSE 80/tcp 443/tcp ENV DEBIAN_FRONTEND=teletype
VOLUME ["/var/interlegis/sapl/data", "/var/interlegis/sapl/media"] EXPOSE 80 443
VOLUME ["/var/interlegis/sapl/data", "/var/interlegis/sapl/media", "/var/log/sapl/"]
ENTRYPOINT ["/usr/bin/tini","--"]
CMD ["/var/interlegis/sapl/start.sh"] CMD ["/var/interlegis/sapl/start.sh"]

2
docker/config/nginx/nginx.conf

@ -1,4 +1,4 @@
user nginx; user www-data nginx;
worker_processes 1; worker_processes 1;
error_log /var/log/nginx/error.log warn; error_log /var/log/nginx/error.log warn;

17
docker/docker-compose.yaml

@ -1,4 +1,3 @@
version: "3.7"
services: services:
sapldb: sapldb:
image: postgres:10.5-alpine image: postgres:10.5-alpine
@ -10,7 +9,9 @@ services:
POSTGRES_PASSWORD: sapl POSTGRES_PASSWORD: sapl
POSTGRES_USER: sapl POSTGRES_USER: sapl
POSTGRES_DB: sapl POSTGRES_DB: sapl
PGDATA : /var/lib/postgresql/data/ PGDATA: /var/lib/postgresql/data/
TZ: UTC
PG_TZ: UTC
volumes: volumes:
- sapldb_data:/var/lib/postgresql/data/ - sapldb_data:/var/lib/postgresql/data/
ports: ports:
@ -32,10 +33,10 @@ services:
networks: networks:
- sapl-net - sapl-net
sapl: sapl:
image: interlegis/sapl:3.1.163-RC20 # image: interlegis/sapl:3.1.164-RC1
# build: build:
# context: ../ context: ../
# dockerfile: ./docker/Dockerfile dockerfile: ./docker/Dockerfile
container_name: sapl container_name: sapl
labels: labels:
NAME: "sapl" NAME: "sapl"
@ -52,7 +53,9 @@ services:
EMAIL_HOST_PASSWORD: senhasmtp EMAIL_HOST_PASSWORD: senhasmtp
USE_SOLR: 'True' USE_SOLR: 'True'
SOLR_COLLECTION: sapl SOLR_COLLECTION: sapl
SOLR_URL: http://solr:solr@saplsolr:8983 SOLR_URL: http://admin:solr@saplsolr:8983
SOLR_USER: solr
SOLR_PASSWORD: solr
IS_ZK_EMBEDDED: 'True' IS_ZK_EMBEDDED: 'True'
ENABLE_SAPN: 'False' ENABLE_SAPN: 'False'
TZ: America/Sao_Paulo TZ: America/Sao_Paulo

50
docker/gunicorn_start.sh

@ -1,50 +0,0 @@
#!/usr/bin/env bash
##
##
## PARA USO EXCLUSIVO DO CONTAINER DOCKER DO SAPL!!!
## EVITE USAR PARA CHAMADA DIRETAS
##
##
# As seen in http://tutos.readthedocs.org/en/latest/source/ndg.html
SAPL_DIR="/var/interlegis/sapl"
# Seta um novo diretório foi passado como raiz para o SAPL
# caso esse tenha sido passado como parâmetro
if [ "$1" ]
then
SAPL_DIR="$1"
fi
NAME="SAPL" # Name of the application (*)
DJANGODIR=/var/interlegis/sapl/ # Django project directory (*)
SOCKFILE=/var/interlegis/sapl/run/gunicorn.sock # we will communicate using this unix socket (*)
USER=`whoami` # the user to run as (*)
GROUP=`whoami` # the group to run as (*)
NUM_WORKERS=3 # how many worker processes should Gunicorn spawn (*)
# NUM_WORKERS = 2 * CPUS + 1
TIMEOUT=300
MAX_REQUESTS=100 # number of requests before restarting worker
DJANGO_SETTINGS_MODULE=sapl.settings # which settings file should Django use (*)
DJANGO_WSGI_MODULE=sapl.wsgi # WSGI module name (*)
echo "Starting $NAME as `whoami` on base dir $SAPL_DIR"
# Create the run directory if it doesn't exist
RUNDIR=$(dirname $SOCKFILE)
test -d $RUNDIR || mkdir -p $RUNDIR
# Start your Django Unicorn
# Programs meant to be run under supervisor should not daemonize themselves (do not use --daemon)
exec gunicorn ${DJANGO_WSGI_MODULE}:application \
--name $NAME \
--log-level debug \
--timeout $TIMEOUT \
--workers $NUM_WORKERS \
--max-requests $MAX_REQUESTS \
--user $USER \
--access-logfile /var/log/sapl/access.log \
--error-logfile /var/log/sapl/error.log \
--bind=unix:$SOCKFILE

3
docker/simple_gunicorn.sh

@ -12,4 +12,5 @@ export PYTHONPATH=$DJANGODIR:$PYTHONPATH
# Get eth0 IP and filter out the netmask portion (/24, e.g.) # Get eth0 IP and filter out the netmask portion (/24, e.g.)
IP=`ip addr | grep 'inet .* eth0' | awk '{print $2}' | sed 's/\/[0-9]*//'` IP=`ip addr | grep 'inet .* eth0' | awk '{print $2}' | sed 's/\/[0-9]*//'`
gunicorn --bind $IP:8000 sapl.wsgi:application #gunicorn --bind $IP:8000 sapl.wsgi:application
gunicorn -c gunicorn.conf.py sapl.wsgi:application

147
docker/start.sh

@ -1,147 +0,0 @@
#!/usr/bin/env bash
create_env() {
echo "[ENV FILE] creating .env file..."
# check if file exists
if [ -f "/var/interlegis/sapl/data/secret.key" ]; then
KEY=`cat /var/interlegis/sapl/data/secret.key`
else
KEY=`python3 genkey.py`
echo $KEY > data/secret.key
fi
FILENAME="/var/interlegis/sapl/sapl/.env"
if [ -z "${DATABASE_URL:-}" ]; then
DATABASE_URL="postgresql://sapl:sapl@sapldb:5432/sapl"
fi
# ALWAYS replace the content of .env variable
# If want to conditionally create only if absent then use IF below
# if [ ! -f $FILENAME ]; then
touch $FILENAME
# explicitly use '>' to erase any previous content
echo "SECRET_KEY="$KEY > $FILENAME
# now only appends
echo "DATABASE_URL = "$DATABASE_URL >> $FILENAME
echo "DEBUG = ""${DEBUG-False}" >> $FILENAME
echo "EMAIL_USE_TLS = ""${USE_TLS-True}" >> $FILENAME
echo "EMAIL_PORT = ""${EMAIL_PORT-587}" >> $FILENAME
echo "EMAIL_HOST = ""${EMAIL_HOST-''}" >> $FILENAME
echo "EMAIL_HOST_USER = ""${EMAIL_HOST_USER-''}" >> $FILENAME
echo "EMAIL_HOST_PASSWORD = ""${EMAIL_HOST_PASSWORD-''}" >> $FILENAME
echo "EMAIL_SEND_USER = ""${EMAIL_HOST_USER-''}" >> $FILENAME
echo "DEFAULT_FROM_EMAIL = ""${EMAIL_HOST_USER-''}" >> $FILENAME
echo "SERVER_EMAIL = ""${EMAIL_HOST_USER-''}" >> $FILENAME
echo "USE_SOLR = ""${USE_SOLR-False}" >> $FILENAME
echo "SOLR_COLLECTION = ""${SOLR_COLLECTION-sapl}" >> $FILENAME
echo "SOLR_URL = ""${SOLR_URL-http://localhost:8983}" >> $FILENAME
echo "IS_ZK_EMBEDDED = ""${IS_ZK_EMBEDDED-False}" >> $FILENAME
echo "ENABLE_SAPN = ""${ENABLE_SAPN-False}" >> $FILENAME
echo "[ENV FILE] done."
}
create_env
/bin/bash wait-for-pg.sh $DATABASE_URL
yes yes | python3 manage.py migrate
## SOLR
USE_SOLR="${USE_SOLR:=False}"
SOLR_URL="${SOLR_URL:=http://localhost:8983}"
SOLR_COLLECTION="${SOLR_COLLECTION:=sapl}"
NUM_SHARDS=${NUM_SHARDS:=1}
RF=${RF:=1}
MAX_SHARDS_PER_NODE=${MAX_SHARDS_PER_NODE:=1}
IS_ZK_EMBEDDED="${IS_ZK_EMBEDDED:=False}"
if [ "${USE_SOLR-False}" == "True" ] || [ "${USE_SOLR-False}" == "true" ]; then
echo "Solr configurations"
echo "==================="
echo "URL: $SOLR_URL"
echo "COLLECTION: $SOLR_COLLECTION"
echo "NUM_SHARDS: $NUM_SHARDS"
echo "REPLICATION FACTOR: $RF"
echo "MAX SHARDS PER NODE: $MAX_SHARDS_PER_NODE"
echo "ASSUME ZK EMBEDDED: $IS_ZK_EMBEDDED"
echo "========================================="
echo "running Solr script"
/bin/bash wait-for-solr.sh $SOLR_URL
CHECK_SOLR_RETURN=$?
if [ $CHECK_SOLR_RETURN == 1 ]; then
echo "Connecting to Solr..."
if [ "${IS_ZK_EMBEDDED-False}" == "True" ] || [ "${IS_ZK_EMBEDDED-False}" == "true" ]; then
ZK_EMBEDDED="--embedded_zk"
echo "Assuming embedded ZooKeeper instalation..."
fi
python3 solr_cli.py -u $SOLR_URL -c $SOLR_COLLECTION -s $NUM_SHARDS -rf $RF -ms $MAX_SHARDS_PER_NODE $ZK_EMBEDDED &
# Enable SOLR switch on, creating if it doesn't exist on database
./manage.py waffle_switch SOLR_SWITCH on --create
else
echo "Solr is offline, not possible to connect."
# Disable Solr switch off, creating if it doesn't exist on database
./manage.py waffle_switch SOLR_SWITCH off --create
fi
else
echo "Solr support is not initialized."
# Disable Solr switch off, creating if it doesn't exist on database
./manage.py waffle_switch SOLR_SWITCH off --create
fi
## Enable/Disable SAPN
if [ "${ENABLE_SAPN-False}" == "True" ] || [ "${ENABLE_SAPN-False}" == "true" ]; then
echo "Enabling SAPN"
./manage.py waffle_switch SAPLN_SWITCH on --create
else
echo "Enabling SAPL"
./manage.py waffle_switch SAPLN_SWITCH off --create
fi
echo "Creating admin user..."
user_created=$(python3 create_admin.py 2>&1)
echo $user_created
cmd=$(echo $user_created | grep 'ADMIN_USER_EXISTS')
user_exists=$?
cmd=$(echo $user_created | grep 'MISSING_ADMIN_PASSWORD')
lack_pwd=$?
if [ $user_exists -eq 0 ]; then
echo "[SUPERUSER CREATION] User admin already exists. Not creating"
fi
if [ $lack_pwd -eq 0 ]; then
echo "[SUPERUSER] Environment variable $ADMIN_PASSWORD for superuser admin was not set. Leaving container"
# return -1
fi
# Backfilling AuditLog's JSON field
time ./manage.py backfill_auditlog &
echo "-------------------------------------"
echo "| ███████╗ █████╗ ██████╗ ██╗ |"
echo "| ██╔════╝██╔══██╗██╔══██╗██║ |"
echo "| ███████╗███████║██████╔╝██║ |"
echo "| ╚════██║██╔══██║██╔═══╝ ██║ |"
echo "| ███████║██║ ██║██║ ███████╗ |"
echo "| ╚══════╝╚═╝ ╚═╝╚═╝ ╚══════╝ |"
echo "-------------------------------------"
/bin/sh gunicorn_start.sh &
/usr/sbin/nginx -g "daemon off;"

0
docker/create_admin.py → docker/startup_scripts/create_admin.py

0
docker/genkey.py → docker/startup_scripts/genkey.py

80
docker/startup_scripts/gunicorn.conf.py

@ -0,0 +1,80 @@
# /var/interlegis/sapl/gunicorn.conf.py
import os
import pathlib
import multiprocessing
# ---- SAPL app configuration ----
NAME = "SAPL"
DJANGODIR = "/var/interlegis/sapl"
SOCKFILE = f"unix:{DJANGODIR}/run/gunicorn.sock"
USER = "sapl"
GROUP = "nginx"
NUM_WORKERS = int(os.getenv("WEB_CONCURRENCY", "3"))
THREADS = int(os.getenv("GUNICORN_THREADS", "8"))
TIMEOUT = int(os.getenv("GUNICORN_TIMEOUT", "300"))
MAX_REQUESTS = 1000
WORKER_CLASS = "gthread"
DJANGO_SETTINGS = "sapl.settings"
WSGI_APP = "sapl.wsgi:application"
# ---- gunicorn settings ----
# Equivalent of: --name
proc_name = NAME
# Equivalent of: --bind=unix:...
# For quick testing via browser, you can switch to: bind = "0.0.0.0:8000"
bind = f"unix:{SOCKFILE}"
umask = 0o007
user = USER
group = GROUP
# Ensure imports work like in your script’s working dir
chdir = DJANGODIR
# Allow starting with just: gunicorn -c gunicorn.conf.py
wsgi_app = WSGI_APP
# Logs
loglevel = "debug"
accesslog = "/var/log/sapl/access.log"
errorlog = "/var/log/sapl/error.log"
# errorlog = "-" # send to stderr (so you see it in docker logs or terminal)
# accesslog = "-" # send to stdout
capture_output = True # capture print/tracebacks from app
# Worker/process lifecycle
workers = NUM_WORKERS
worker_class = WORKER_CLASS
threads = THREADS
timeout = TIMEOUT
graceful_timeout = 30
keepalive = 10
backlog = 2048
max_requests = MAX_REQUESTS
max_requests_jitter = 100
# Environment (same as exporting before running)
raw_env = [
f"DJANGO_SETTINGS_MODULE={DJANGO_SETTINGS}",
# If you’re using ReportLab and seeing segfaults with PDFs, keep this:
# "RL_NOACCEL=1",
]
# If you previously enabled preload and saw segfaults with native libs, keep it off:
preload_app = False
# Create the run/ directory for the UNIX socket (your script did this)
def on_starting(server):
pathlib.Path(SOCKFILE).parent.mkdir(parents=True, exist_ok=True)
# Close DB connections after fork (safer when using preload or certain DB drivers)
def post_fork(server, worker):
try:
from django import db
db.connections.close_all()
except Exception:
# Django not initialized yet or not available
pass

18
docker/solr_cli.py → docker/startup_scripts/solr_cli.py

@ -20,6 +20,8 @@ from kazoo.client import KazooClient
# #
logging.basicConfig() logging.basicConfig()
logging.captureWarnings(True)
logger = logging.getLogger(__name__)
SECURITY_FILE_TEMPLATE = """ SECURITY_FILE_TEMPLATE = """
{ {
@ -49,6 +51,7 @@ def solr_hash_password(password: str, salt: str = None):
salt (optional): base64 salt string salt (optional): base64 salt string
returns: sha256 hash of password and salt (both base64 strings) returns: sha256 hash of password and salt (both base64 strings)
""" """
logger.debug("Generating Solr password")
m = sha256() m = sha256()
if salt is None: if salt is None:
salt = secrets.token_bytes(32) salt = secrets.token_bytes(32)
@ -67,32 +70,32 @@ def solr_hash_password(password: str, salt: str = None):
def create_security_file(username, password): def create_security_file(username, password):
print("Creating security.json file...") logger.info("Creating security.json file...")
with open("security.json", "w") as f: with open("security.json", "w") as f:
cypher, salt = solr_hash_password(password) cypher, salt = solr_hash_password(password)
f.write(SECURITY_FILE_TEMPLATE % (username, cypher, salt, username)) f.write(SECURITY_FILE_TEMPLATE % (username, cypher, salt, username))
print("file created!") logger.info("file created!")
def upload_security_file(zk_host): def upload_security_file(zk_host):
zk_port = 9983 # embedded ZK port zk_port = 9983 # embedded ZK port
print(f"Uploading security file to Solr, ZK server={zk_host}:{zk_port}...") logger.info(f"Uploading security file to Solr, ZK server={zk_host}:{zk_port}...")
try: try:
with open('security.json', 'r') as f: with open('security.json', 'r') as f:
data = f.read() data = f.read()
zk = KazooClient(hosts=f"{zk_host}:{zk_port}") zk = KazooClient(hosts=f"{zk_host}:{zk_port}")
zk.start() zk.start()
print("Uploading security.json file...") logger.info("Uploading security.json file...")
if zk.exists('/security.json'): if zk.exists('/security.json'):
zk.set("/security.json", str.encode(data)) zk.set("/security.json", str.encode(data))
else: else:
zk.create("/security.json", str.encode(data)) zk.create("/security.json", str.encode(data))
data, stat = zk.get('/security.json') data, stat = zk.get('/security.json')
print("file uploaded!") logger.info("file uploaded!")
print(data.decode('utf-8')) logger.info(data.decode('utf-8'))
zk.stop() zk.stop()
except Exception as e: except Exception as e:
print(e) logger.error(e)
sys.exit(-1) sys.exit(-1)
@ -250,6 +253,7 @@ def setup_embedded_zk(solr_url):
_, solr_user, solr_pwd, solr_host, solr_port = match.groups() _, solr_user, solr_pwd, solr_host, solr_port = match.groups()
if solr_user and solr_pwd and solr_host: if solr_user and solr_pwd and solr_host:
print(f"Creating Solr user {solr_user} with password {solr_pwd}")
create_security_file(solr_user, solr_pwd) create_security_file(solr_user, solr_pwd)
upload_security_file(solr_host) upload_security_file(solr_host)
else: else:

289
docker/startup_scripts/start.sh

@ -0,0 +1,289 @@
#!/usr/bin/env bash
set -Eeuo pipefail
IFS=$'\n\t'
DATA_DIR="/var/interlegis/sapl/data"
APP_DIR="/var/interlegis/sapl/sapl"
ENV_FILE="$APP_DIR/.env"
SECRET_FILE="$DATA_DIR/secret.key"
mkdir -p "$DATA_DIR" "$APP_DIR"
log() { printf '[%s] %s\n' "$(date -Is)" "$*"; }
err() { printf '[%s] ERROR: %s\n' "$(date -Is)" "$*" >&2; }
cleanup() { jobs -p | xargs -r kill 2>/dev/null || true; }
trap cleanup TERM INT EXIT
# --- new function ---
configure_pg_timezone() {
: "${DATABASE_URL:=postgresql://sapl:sapl@sapldb:5432/sapl}"
: "${DB_TIMEZONE:=America/Sao_Paulo}"
: "${DB_NAME:=}"
: "${DB_ROLE:=}"
log "Checking database/role timezone defaults…"
# Detect DB and role if not provided
if [[ -z "$DB_NAME" ]]; then
DB_NAME="$(psql "$DATABASE_URL" -At -v ON_ERROR_STOP=1 -c 'select current_database();')"
fi
if [[ -z "$DB_ROLE" ]]; then
DB_ROLE="$(psql "$DATABASE_URL" -At -v ON_ERROR_STOP=1 -c 'select current_user;')"
fi
# What is the effective timezone for this DB/role right now?
current_tz="$(psql "$DATABASE_URL" -At -v ON_ERROR_STOP=1 -c 'show time zone;')"
current_tz_lower="${current_tz,,}"
# Consider these as already UTC
if [[ "$current_tz_lower" == "utc" || "$current_tz_lower" == "etc/utc" ]]; then
log "Timezone already UTC for DB='$DB_NAME' ROLE='$DB_ROLE' (SHOW TIME ZONE => $current_tz). Skipping ALTERs."
return
fi
log "Timezone is '$current_tz' (not UTC). Applying persistent defaults…"
# Persist at database level (requires DB owner or superuser)
if psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -q \
-c "ALTER DATABASE \"$DB_NAME\" SET timezone TO '$DB_TIMEZONE';"; then
log "ALTER DATABASE \"$DB_NAME\" SET timezone TO '$DB_TIMEZONE' applied."
else
err "ALTER DATABASE \"$DB_NAME\" failed. Need DB owner or superuser."
exit 1
fi
# Persist at role level (requires superuser)
if psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -q \
-c "ALTER ROLE \"$DB_ROLE\" SET timezone TO '$DB_TIMEZONE';"; then
log "ALTER ROLE \"$DB_ROLE\" SET timezone TO '$DB_TIMEZONE' applied."
else
err "ALTER ROLE \"$DB_ROLE\" failed. Need superuser privileges."
exit 1
fi
# Re-check (new session shows the new default)
verify_tz="$(psql "$DATABASE_URL" -At -v ON_ERROR_STOP=1 -c 'show time zone;')"
log "SHOW TIME ZONE now => $verify_tz (new sessions will inherit the defaults)."
}
create_secret() {
if [[ -f "$SECRET_FILE" ]]; then
SECRET_KEY="$(<"$SECRET_FILE")"
else
log "Generating SECRET_KEY..."
SECRET_KEY="$(python3 genkey.py)"
umask 177
printf '%s\n' "$SECRET_KEY" > "$SECRET_FILE"
chmod 600 "$SECRET_FILE"
fi
export SECRET_KEY
}
write_env_file() {
: "${DATABASE_URL:=postgresql://sapl:sapl@sapldb:5432/sapl}"
: "${DEBUG:=False}"
: "${EMAIL_USE_TLS:=True}"
: "${EMAIL_PORT:=587}"
: "${EMAIL_HOST:=}"
: "${EMAIL_HOST_USER:=}"
: "${EMAIL_HOST_PASSWORD:=}"
: "${DEFAULT_FROM_EMAIL:=$EMAIL_HOST_USER}"
: "${SERVER_EMAIL:=$EMAIL_HOST_USER}"
: "${USE_SOLR:=False}"
: "${SOLR_COLLECTION:=sapl}"
: "${SOLR_URL:=http://localhost:8983}"
: "${IS_ZK_EMBEDDED:=False}"
: "${NUM_SHARDS:=1}"
: "${RF:=1}"
: "${MAX_SHARDS_PER_NODE:=1}"
: "${ENABLE_SAPN:=False}"
tmp="$(mktemp)"
{
printf 'SECRET_KEY=%s\n' "$SECRET_KEY"
printf 'DATABASE_URL=%s\n' "$DATABASE_URL"
printf 'DEBUG=%s\n' "$DEBUG"
printf 'EMAIL_USE_TLS=%s\n' "$EMAIL_USE_TLS"
printf 'EMAIL_PORT=%s\n' "$EMAIL_PORT"
printf 'EMAIL_HOST=%s\n' "$EMAIL_HOST"
printf 'EMAIL_HOST_USER=%s\n' "$EMAIL_HOST_USER"
printf 'EMAIL_HOST_PASSWORD=%s\n' "$EMAIL_HOST_PASSWORD"
printf 'EMAIL_SEND_USER=%s\n' "$EMAIL_HOST_USER"
printf 'DEFAULT_FROM_EMAIL=%s\n' "$DEFAULT_FROM_EMAIL"
printf 'SERVER_EMAIL=%s\n' "$SERVER_EMAIL"
printf 'USE_SOLR=%s\n' "$USE_SOLR"
printf 'SOLR_COLLECTION=%s\n' "$SOLR_COLLECTION"
printf 'SOLR_URL=%s\n' "$SOLR_URL"
printf 'IS_ZK_EMBEDDED=%s\n' "$IS_ZK_EMBEDDED"
printf 'NUM_SHARDS=%s\n' "$NUM_SHARDS"
printf 'RF=%s\n' "$RF"
printf 'MAX_SHARDS_PER_NODE=%s\n' "$MAX_SHARDS_PER_NODE"
printf 'ENABLE_SAPN=%s\n' "$ENABLE_SAPN"
} > "$tmp"
chmod 600 "$tmp"
mv -f "$tmp" "$ENV_FILE"
log "[ENV] wrote $ENV_FILE"
}
wait_for_pg() {
: "${DATABASE_URL:=postgresql://sapl:sapl@sapldb:5432/sapl}"
log "Waiting for Postgres..."
/bin/bash wait-for-pg.sh "$DATABASE_URL"
}
migrate_db() {
log "Running Django migrations..."
python3 manage.py migrate --noinput
}
# In start.sh (near your other helpers)
configure_solr() {
# respect envs, with sane defaults
local USE="${USE_SOLR:-False}"
local URL="${SOLR_URL:-http://admin:solr@localhost:8983}"
local COL="${SOLR_COLLECTION:-sapl}"
local SHARDS="${NUM_SHARDS:-1}"
local RF="${RF:-1}"
local MS="${MAX_SHARDS_PER_NODE:-1}"
local IS_ZK="${IS_ZK_EMBEDDED:-False}"
# total wait time before we give up (seconds)
local WAIT_TIMEOUT="${SOLR_WAIT_TIMEOUT:-30}"
# per probe max seconds
local PROBE_TIMEOUT="${SOLR_PROBE_TIMEOUT:-3}"
# sleep between probes
local SLEEP_SECS="${SOLR_WAIT_INTERVAL:-2}"
# feature flag OFF by default unless we confirm Solr
./manage.py waffle_switch SOLR_SWITCH off --create || true
# Fast exit if disabled
if [[ "${USE,,}" != "true" ]]; then
echo "[SOLR] USE_SOLR=$USE → skipping Solr initialization."
return 0
fi
echo "[SOLR] Best-effort wait (<= ${WAIT_TIMEOUT}s): $URL, collection=$COL"
local deadline=$((SECONDS + WAIT_TIMEOUT))
while (( SECONDS < deadline )); do
# Try a cheap SolrCloud endpoint; swap for /solr/admin/info/system if you prefer
if curl -fsS --max-time "${PROBE_TIMEOUT}" \
"${URL%/}/solr/admin/collections?action=LIST" >/dev/null; then
echo "[SOLR] Reachable. Kicking off background configuration…"
# optional flag if ZK is embedded
local ZK_FLAG=""
if [[ "${IS_ZK,,}" == "true" ]]; then
ZK_FLAG="--embedded_zk"
fi
(
set -Eeuo pipefail
python3 solr_cli.py \
-u "$URL" -c "$COL" -s "$SHARDS" -rf "$RF" -ms "$MS" $ZK_FLAG
./manage.py waffle_switch SOLR_SWITCH on --create
echo "[SOLR] Configuration done, SOLR_SWITCH=on."
) >/var/log/sapl/solr_init.log 2>&1 & disown
return 0
fi
sleep "${SLEEP_SECS}"
done
echo "[SOLR] Not reachable within ${WAIT_TIMEOUT}s. Proceeding without Solr (SOLR_SWITCH=off)."
return 0
}
configure_sapn() {
if [[ "${ENABLE_SAPN,,}" == "true" ]]; then
log "Enabling SAPN"
python3 manage.py waffle_switch SAPN_SWITCH on --create
else
log "Disabling SAPN"
python3 manage.py waffle_switch SAPN_SWITCH off --create
fi
}
create_admin() {
log "Creating admin user..."
out="$(python3 create_admin.py 2>&1 || true)"
printf '%s\n' "$out"
if grep -q 'MISSING_ADMIN_PASSWORD' <<<"$out"; then
err "[SUPERUSER] ADMIN_PASSWORD not set. Exiting."
exit 1
fi
}
fix_logging_and_socket_perms() {
local APP_DIR="/var/interlegis/sapl"
local LOG_FILE="$APP_DIR/sapl.log"
# dirs
mkdir -p "$APP_DIR/run"
chown -R root:nginx "$APP_DIR"
chmod 2775 "$APP_DIR" "$APP_DIR/run"
chmod -R g+rwX "$APP_DIR"
# new files/sockets → 660
umask 0007
# ensure log file is owned by sapl and writable
install -Dm0660 /dev/null "$LOG_FILE"
chown sapl:nginx "$LOG_FILE"
# stale socket cleanup (if any)
rm -f "$APP_DIR/run/gunicorn.sock" 2>/dev/null || true
}
setup_cache_dir() {
# if you later move cache under /var/interlegis/sapl/cache, this line can read an env var
local CACHE_DIR="${DJANGO_CACHE_DIR:-/var/tmp/django_cache}"
mkdir -p "$CACHE_DIR"
chown -R sapl:nginx "$CACHE_DIR"
chmod -R 2775 "$CACHE_DIR"
find "$CACHE_DIR" -type d -exec chmod g+s {} +
# keep your global umask; 0007 ensures new files are rw for owner+group
umask 0007
}
start_services() {
log "Starting gunicorn..."
gunicorn -c gunicorn.conf.py &
log "Starting nginx..."
exec /usr/sbin/nginx -g "daemon off;"
}
main() {
create_secret
write_env_file
wait_for_pg
configure_pg_timezone
migrate_db
configure_solr || true
configure_sapn
create_admin
setup_cache_dir
fix_logging_and_socket_perms
cat <<'BANNER'
-------------------------------------
| ███████╗ █████╗ ██████╗ ██╗ |
| ██╔════╝██╔══██╗██╔══██╗██║ |
| ███████╗███████║██████╔╝██║ |
| ╚════██║██╔══██║██╔═══╝ ██║ |
| ███████║██║ ██║██║ ███████╗ |
| ╚══════╝╚═╝ ╚═╝╚═╝ ╚══════╝ |
-------------------------------------
BANNER
start_services
}
main "$@"

0
docker/wait-for-pg.sh → docker/startup_scripts/wait-for-pg.sh

0
docker/wait-for-solr.sh → docker/startup_scripts/wait-for-solr.sh

74
frontend/src/__apps/compilacao/js/old/compilacao_edit.js

@ -1,4 +1,3 @@
const _$ = window.$ const _$ = window.$
window.DispositivoEdit = function () { window.DispositivoEdit = function () {
@ -500,38 +499,47 @@ window.DispositivoEdit = function () {
} }
instance.reloadFunctionsDraggables = function () { instance.reloadFunctionsDraggables = function () {
_$('.dpt-alts').sortable({ const dptAlts = _$('.dpt-alts')
revert: true, if (dptAlts.length > 0) {
distance: 15, dptAlts.sortable({
start: function (event, ui) { connectWith: '.dpt-alts',
}, items: '.sorting-initialize',
stop: function (event, ui) { revert: true,
const pk = ui.item.attr('pk') distance: 15,
const bloco_pk = ui.item.closest('.dpt-alts').closest('.dpt').attr('pk') start: function (event, ui) {
},
const url = pk + '/refresh?action=json_drag_move_dpt_alterado&index=' + ui.item.index() + '&bloco_pk=' + bloco_pk stop: function (event, ui) {
_$.get(url).done(function (data) { const pk = ui.item.attr('pk')
// console.log(pk + ' - ' + bloco_pk) const bloco_pk = ui.item.closest('.dpt-alts').closest('.dpt').attr('pk')
// reloadFunctionsForObjectsOfCompilacao();
}) const url = pk + '/refresh?action=json_drag_move_dpt_alterado&index=' + ui.item.index() + '&bloco_pk=' + bloco_pk
} _$.get(url).done(function (data) {
}) // handle data if needed
})
}
})
dptAlts.find('.dpt').one('mouseenter', function () {
$(this).addClass('sorting-initialize')
dptAlts.sortable('refresh')
})
_$('.dpt-alts .dpt').draggable({ _$('.dpt-alts .dpt').draggable({
connectToSortable: '.dpt-alts', connectToSortable: '.dpt-alts',
revert: 'invalid', revert: 'invalid',
zIndex: 1, zIndex: 1,
distance: 15, distance: 15,
drag: function (event, ui) { drag: function (event, ui) {
// _$('.dpt-comp-selected').removeClass('dpt-comp-selected'); _$('.dpt-alts').addClass('drag')
_$('.dpt-alts').addClass('drag') },
}, stop: function (event, ui) {
stop: function (event, ui) { _$('.dpt-alts').removeClass('drag')
_$('.dpt-alts').removeClass('drag') }
} })
})
_$('.dpt-alts').disableSelection() dptAlts.disableSelection()
} else {
console.warn("No '.dpt-alts' elements found to make sortable/draggable.")
}
} }
instance.scrollTo = function (dpt) { instance.scrollTo = function (dpt) {
try { try {
@ -570,7 +578,9 @@ window.DispositivoEdit = function () {
instance.triggerBtnDptEdit(href[1]) instance.triggerBtnDptEdit(href[1])
} }
_$('main').click(function (event) { _$('main').click(function (event) {
if (event.target === this || event.target === this.firstElementChild) { instance.clearEditSelected() } if (event.target === this || event.target === this.firstElementChild) {
instance.clearEditSelected()
}
}) })
instance.waitHide() instance.waitHide()
} }

6
frontend/src/__global/js/tinymce/index.js

@ -20,11 +20,11 @@ window.initTextRichEditor = function (elements, readonly = false, paste_as_text
language: 'pt_BR', language: 'pt_BR',
branding: false, branding: false,
forced_root_block: 'p', forced_root_block: 'p',
toolbar: "...| removeformat | ...",
paste_as_text, paste_as_text,
plugins: 'table lists advlist link code', plugins: 'table lists advlist link code',
toolbar: 'undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link | code', toolbar: 'undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link | code | removeformat ',
menubar: 'file edit view insert format table' menubar: 'file edit view insert format table',
license_key: 'gpl'
} }
if (readonly) { if (readonly) {
configTinymce.readonly = 1 configTinymce.readonly = 1

3
frontend/src/__global/scss/layouts/_globals.scss

@ -187,4 +187,7 @@ small {
a[href]:after { a[href]:after {
content: none !important; content: none !important;
} }
.btn-vlibras {
display: none !important;
}
} }

768
frontend/webpack-stats.json

File diff suppressed because it is too large

14525
package-lock.json

File diff suppressed because it is too large

4
package.json

@ -9,7 +9,7 @@
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^6.1.2", "@fortawesome/fontawesome-free": "^6.1.2",
"axios": "^0.27.2", "axios": "^1.7.2",
"bootstrap": "^4.6.2", "bootstrap": "^4.6.2",
"bootstrap-vue": "^2.22.0", "bootstrap-vue": "^2.22.0",
"diff": "^5.1.0", "diff": "^5.1.0",
@ -21,7 +21,7 @@
"moment": "^2.29.4", "moment": "^2.29.4",
"moment-locales-webpack-plugin": "^1.2.0", "moment-locales-webpack-plugin": "^1.2.0",
"popper.js": "^1.16.1", "popper.js": "^1.16.1",
"tinymce": "^6.1.2", "tinymce": "^7.2.0",
"vue": "^2.7.9" "vue": "^2.7.9"
}, },
"devDependencies": { "devDependencies": {

25
release.sh

@ -5,7 +5,6 @@
## ##
## IMPORTANT: requires gh and git-extras commands installed ## IMPORTANT: requires gh and git-extras commands installed
## Currently only runs on MacOS because of sed issue on lines 41 to 47 (see double quotes after -i)
## ##
# TODO: verificar porque só pega versões superiores (3.1.200 ao invés de 3.1.200-RC9) # TODO: verificar porque só pega versões superiores (3.1.200 ao invés de 3.1.200-RC9)
@ -44,14 +43,19 @@ function change_files {
echo "Updating from "$OLD_VERSION" to "$FINAL_VERSION"" echo "Updating from "$OLD_VERSION" to "$FINAL_VERSION""
sed -E -i "" "s|$OLD_VERSION|$FINAL_VERSION|g" docker/docker-compose.yaml if [[ "$OSTYPE" == "darwin"* ]]; then
# MacOS (BSD sed)
sed -E -i "" "s|$OLD_VERSION|$FINAL_VERSION|g" setup.py sed -E -i "" "s|$OLD_VERSION|$FINAL_VERSION|g" docker/docker-compose.yaml
sed -E -i "" "s|$OLD_VERSION|$FINAL_VERSION|g" setup.py
sed -E -i "" "s|$OLD_VERSION|$FINAL_VERSION|g" sapl/templates/base.html sed -E -i "" "s|$OLD_VERSION|$FINAL_VERSION|g" sapl/templates/base.html
sed -E -i "" "s|$OLD_VERSION|$FINAL_VERSION|g" sapl/settings.py
sed -E -i "" "s|$OLD_VERSION|$FINAL_VERSION|g" sapl/settings.py else
# Linux (GNU sed)
sed -i -E "s|$OLD_VERSION|$FINAL_VERSION|g" docker/docker-compose.yaml
sed -i -E "s|$OLD_VERSION|$FINAL_VERSION|g" setup.py
sed -i -E "s|$OLD_VERSION|$FINAL_VERSION|g" sapl/templates/base.html
sed -i -E "s|$OLD_VERSION|$FINAL_VERSION|g" sapl/settings.py
fi
} }
function set_major_version { function set_major_version {
@ -72,9 +76,6 @@ function set_rc_version {
fi fi
FINAL_VERSION=$NEXT_RC_VERSION FINAL_VERSION=$NEXT_RC_VERSION
## DEBUG
# echo "OLD_VERSION: $OLD_VERSION"
# echo "FINAL_VERSION: $FINAL_VERSION"
} }
# Function to display Yes/No prompt with colored message # Function to display Yes/No prompt with colored message

16
requirements/dev-requirements.txt

@ -1,9 +1,11 @@
-r test-requirements.txt -r test-requirements.txt
autopep8==1.2.4 autopep8==2.3.2
beautifulsoup4==4.9.1 beautifulsoup4==4.13.5
django-debug-toolbar==1.11.1 django-debug-toolbar==3.2.4
ipdb==0.13.3 ipdb==0.13.13
pdbpp==0.9.2 fancycompleter==0.11.1
pip-review==0.4 pdbpp==0.11.7
pipdeptree==0.10.1 pip-review==1.3.0
pipdeptree==2.28.0
pydevd-pycharm~=203.7148.7

16
requirements/requirements.txt

@ -2,11 +2,9 @@ django==2.2.28
django-haystack==3.1.1 django-haystack==3.1.1
django-filter==2.4.0 django-filter==2.4.0
djangorestframework==3.12.4 djangorestframework==3.12.4
dj-database-url==0.5.0
django-braces==1.14.0 django-braces==1.14.0
django-crispy-forms==1.7.2 django-crispy-forms==1.7.2
django-contrib-postgres==0.0.1 django-contrib-postgres==0.0.1
django-floppyforms==1.8.0
django-extra-views==0.12.0 django-extra-views==0.12.0
django-model-utils==3.1.2 django-model-utils==3.1.2
django-extensions==2.1.4 django-extensions==2.1.4
@ -17,14 +15,18 @@ drf-spectacular==0.18.2
django-ratelimit==3.0.1 django-ratelimit==3.0.1
easy-thumbnails==2.8.5 easy-thumbnails==2.8.5
python-decouple==3.1 python-decouple==3.1
psycopg2-binary==2.8.6 dj-database-url==0.5.0
psycopg2-binary==2.9.9
pyyaml==6.0.1 pyyaml==6.0.1
pytz==2019.3 pytz==2019.3
python-magic==0.4.15 python-magic==0.4.15
unipath==1.1 unipath==1.1
WeasyPrint==51 Pillow==10.3.0
Pillow==10.0.1 rlPyCairo==0.3.0
gunicorn==19.9.0 reportlab==4.2.0
WeasyPrint==66
trml2pdf==0.6
gunicorn==23.0.0
more-itertools==8.2.0 more-itertools==8.2.0
pysolr==3.6.0 pysolr==3.6.0
PyPDF4==1.27.0 PyPDF4==1.27.0
@ -36,6 +38,6 @@ kazoo==2.8.0
django-prometheus==2.2.0 django-prometheus==2.2.0
asn1crypto==1.5.1 asn1crypto==1.5.1
XlsxWriter==3.2.0
git+https://github.com/interlegis/trml2pdf
git+https://github.com/interlegis/django-admin-bootstrapped git+https://github.com/interlegis/django-admin-bootstrapped

19
requirements/test-requirements.txt

@ -1,11 +1,10 @@
-r requirements.txt -r requirements.txt
coverage==4.4 coverage==7.6.1
django-webtest==1.9.7 django-webtest==1.9.8
flake8==2.6.2 flake8==7.1.1
isort==4.2.5 isort==5.13.2
model-bakery==1.1.0 model-bakery==1.5.0
pep8==1.7.0 pycodestyle==2.12.1
pytest==5.4.3 pytest==8.3.3
pytest-cov==2.10.0 pytest-cov==5.0.0
pytest-django==3.8.0 WebTest==3.0.6
webtest==2.0.21

17
sapl/api/views_comissoes.py

@ -3,6 +3,10 @@ from django.apps.registry import apps
from drfautoapi.drfautoapi import ApiViewSetConstrutor, \ from drfautoapi.drfautoapi import ApiViewSetConstrutor, \
customize, wrapper_queryset_response_for_drf_action customize, wrapper_queryset_response_for_drf_action
from sapl.comissoes.models import Comissao
from rest_framework.decorators import action
from sapl.materia.models import MateriaEmTramitacao
ApiViewSetConstrutor.build_class( ApiViewSetConstrutor.build_class(
@ -10,3 +14,16 @@ ApiViewSetConstrutor.build_class(
apps.get_app_config('comissoes') apps.get_app_config('comissoes')
] ]
) )
@customize(Comissao)
class _ComissaoViewSet:
@action(detail=True)
def materiaemtramitacao(self, request, *args, **kwargs):
return self.get_materiaemtramitacao(**kwargs)
@wrapper_queryset_response_for_drf_action(model=MateriaEmTramitacao)
def get_materiaemtramitacao(self, **kwargs):
return self.get_queryset().filter(
unidade_tramitacao_atual__comissao=kwargs['pk'],
)

18
sapl/audiencia/migrations/0019_auto_20240711_1400.py

@ -0,0 +1,18 @@
# Generated by Django 2.2.28 on 2024-07-11 17:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('audiencia', '0018_auto_20230529_1641'),
]
operations = [
migrations.AlterField(
model_name='audienciapublica',
name='ano',
field=models.PositiveSmallIntegerField(choices=[(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'),
),
]

1
sapl/base/apps.py

@ -1,4 +1,3 @@
import django import django
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _

2
sapl/base/email_utils.py

@ -21,7 +21,7 @@ def load_email_templates(templates, context={}):
tpl = loader.get_template(t) tpl = loader.get_template(t)
email = tpl.render(context) email = tpl.render(context)
if t.endswith(".html"): if t.endswith(".html"):
email = email.replace('\n', '').replace('\r', '') email = email.replace('\\n', '').replace('\r', '')
emails.append(email) emails.append(email)
return emails return emails

34
sapl/base/forms.py

@ -40,7 +40,7 @@ from sapl.utils import (autor_label, autor_modal, ChoiceWithoutValidationField,
FilterOverridesMetaMixin, FileFieldCheckMixin, FilterOverridesMetaMixin, FileFieldCheckMixin,
ImageThumbnailFileInput, qs_override_django_filter, ImageThumbnailFileInput, qs_override_django_filter,
RANGE_ANOS, YES_NO_CHOICES, choice_tipos_normas, RANGE_ANOS, YES_NO_CHOICES, choice_tipos_normas,
GoogleRecapthaMixin, parlamentares_ativos, RANGE_MESES) GoogleRecapthaMixin, parlamentares_ativos, RANGE_MESES, is_weak_password, delete_cached_entry)
from .models import AppConfig, CasaLegislativa from .models import AppConfig, CasaLegislativa
@ -288,8 +288,13 @@ class UserAdminForm(ModelForm):
) )
else: else:
if new_password1 and new_password2: if new_password1 and new_password2:
if is_weak_password(new_password1):
raise forms.ValidationError(_(
'A senha deve ter pelo menos 8 caracteres e incluir uma combinação '
'de letras maiúsculas e minúsculas, números e caracteres especiais.'
))
password_validation.validate_password( password_validation.validate_password(
new_password2, self.instance) new_password1, self.instance)
parlamentar = data.get('parlamentar', None) parlamentar = data.get('parlamentar', None)
if parlamentar and parlamentar.votante_set.exists() and \ if parlamentar and parlamentar.votante_set.exists() and \
@ -916,22 +921,26 @@ class CasaLegislativaForm(FileFieldCheckMixin, ModelForm):
# chama __clean de FileFieldCheckMixin # chama __clean de FileFieldCheckMixin
# por estar em clean de campo # por estar em clean de campo
super(CasaLegislativaForm, self)._check() super(CasaLegislativaForm, self)._check()
logotipo = self.cleaned_data.get('logotipo') logotipo = self.cleaned_data.get('logotipo')
if logotipo: if logotipo and logotipo.size > MAX_IMAGE_UPLOAD_SIZE:
if logotipo.size > MAX_IMAGE_UPLOAD_SIZE: raise ValidationError("Imagem muito grande. ( > 2MB )")
raise ValidationError("Imagem muito grande. ( > 2MB )")
return logotipo return logotipo
def save(self, commit=True):
casa = super(CasaLegislativaForm, self).save(commit=commit)
delete_cached_entry("site-title")
return casa
class LoginForm(AuthenticationForm): class LoginForm(AuthenticationForm):
username = forms.CharField( username = forms.CharField(
label="Username", max_length=30, label="Usuário", max_length=30,
widget=forms.TextInput( widget=forms.TextInput(
attrs={ attrs={
'class': 'form-control', 'name': 'username'})) 'class': 'form-control', 'name': 'username'}))
password = forms.CharField( password = forms.CharField(
label="Password", max_length=30, label="Senha", max_length=30,
widget=forms.PasswordInput( widget=forms.PasswordInput(
attrs={ attrs={
'class': 'form-control', 'name': 'password'})) 'class': 'form-control', 'name': 'password'}))
@ -1139,12 +1148,15 @@ class AlterarSenhaForm(Form):
# TODO: caracteres alfanuméricos, maiúsculas (?), # TODO: caracteres alfanuméricos, maiúsculas (?),
# TODO: senha atual igual a senha anterior, etc # TODO: senha atual igual a senha anterior, etc
if len(new_password1) < 6: if is_weak_password(new_password1):
self.logger.warning( self.logger.warning(
'A senha informada não tem o mínimo de 6 caracteres.' 'A senha deve ter pelo menos 8 caracteres e incluir uma combinação '
'de letras maiúsculas e minúsculas, números e caracteres especiais.'
) )
raise ValidationError( raise ValidationError(
"A senha informada deve ter no mínimo 6 caracteres") 'A senha deve ter pelo menos 8 caracteres e incluir uma combinação '
'de letras maiúsculas e minúsculas, números e caracteres especiais.'
)
username = data['username'] username = data['username']
old_password = data['old_password'] old_password = data['old_password']

18
sapl/base/migrations/0060_auto_20240812_1628.py

@ -0,0 +1,18 @@
# Generated by Django 2.2.28 on 2024-08-12 19:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('base', '0059_remove_appconfig_sapl_as_sapn'),
]
operations = [
migrations.AlterField(
model_name='casalegislativa',
name='cep',
field=models.CharField(blank=True, max_length=100, verbose_name='CEP'),
),
]

2
sapl/base/models.py

@ -56,7 +56,7 @@ class CasaLegislativa(models.Model):
nome = models.CharField(max_length=100, verbose_name=_('Nome')) nome = models.CharField(max_length=100, verbose_name=_('Nome'))
sigla = models.CharField(max_length=100, verbose_name=_('Sigla')) sigla = models.CharField(max_length=100, verbose_name=_('Sigla'))
endereco = models.CharField(max_length=100, verbose_name=_('Endereço')) endereco = models.CharField(max_length=100, verbose_name=_('Endereço'))
cep = models.CharField(max_length=100, verbose_name=_('CEP')) cep = models.CharField(max_length=100, blank=True, verbose_name=_('CEP'))
municipio = models.CharField(max_length=50, verbose_name=_('Município')) municipio = models.CharField(max_length=50, verbose_name=_('Município'))
uf = models.CharField(max_length=2, uf = models.CharField(max_length=2,
choices=LISTA_DE_UFS, choices=LISTA_DE_UFS,

18
sapl/base/receivers.py

@ -225,7 +225,7 @@ def signed_files_extraction_function(sender, instance, **kwargs):
issuer = cert.native['tbs_certificate']['issuer'] issuer = cert.native['tbs_certificate']['issuer']
oname = issuer.get('organization_name', '') oname = issuer.get('organization_name', '')
if oname == 'Gov-Br': if oname in ('Gov-Br', '1Doc'):
nome = subject['common_name'].split(':')[0] nome = subject['common_name'].split(':')[0]
continue continue
@ -283,13 +283,18 @@ def signed_files_extraction_function(sender, instance, **kwargs):
return signs return signs
# tenta extrair via /Fields # tenta extrair via /Fields
fields_br = []
try: try:
pdf = PdfFileReader(file) pdf = PdfFileReader(file)
fields = pdf.getFields() fields = pdf.getFields()
fields_br = list(
map(lambda x: x.get('/V', {}).get('/ByteRange', []), fields.values()))
except Exception as e: except Exception as e:
try: try:
pdf = PdfFileReader(file, strict=False) pdf = PdfFileReader(file, strict=False)
fields = pdf.getFields() fields = pdf.getFields()
fields_br = list(
map(lambda x: x.get('/V', {}).get('/ByteRange', []), fields.values()))
except Exception as ee: except Exception as ee:
fields = ee fields = ee
@ -309,13 +314,17 @@ def signed_files_extraction_function(sender, instance, **kwargs):
n += 1 n += 1
br = [int(i, 10) for i in pdfdata[start + 1: stop].split()] br = [int(i, 10) for i in pdfdata[start + 1: stop].split()]
if br in fields_br:
continue
contents = pdfdata[br[0] + br[1] + 1: br[2] - 1] contents = pdfdata[br[0] + br[1] + 1: br[2] - 1]
bcontents = bytes.fromhex(contents.decode("utf8")) bcontents = bytes.fromhex(contents.decode("utf8"))
data1 = pdfdata[br[0]: br[0] + br[1]] data1 = pdfdata[br[0]: br[0] + br[1]]
data2 = pdfdata[br[2]: br[2] + br[3]] data2 = pdfdata[br[2]: br[2] + br[3]]
#signedData = data1 + data2 #signedData = data1 + data2
nome = 'Nome do assinante não localizado.' not_nome = nome = 'Nome do assinante não localizado.'
oname = '' oname = ''
try: try:
info = cms.ContentInfo.load(bcontents) info = cms.ContentInfo.load(bcontents)
@ -327,7 +336,7 @@ def signed_files_extraction_function(sender, instance, **kwargs):
issuer = cert.native['tbs_certificate']['issuer'] issuer = cert.native['tbs_certificate']['issuer']
oname = issuer.get('organization_name', '') oname = issuer.get('organization_name', '')
if oname == 'Gov-Br': if oname in ('Gov-Br', '1Doc'):
nome = subject['common_name'].split(':')[0] nome = subject['common_name'].split(':')[0]
continue continue
@ -348,7 +357,8 @@ def signed_files_extraction_function(sender, instance, **kwargs):
pass pass
fd = None fd = None
signs.append((nome, [fd, oname])) if nome != not_nome:
signs.append((nome, [fd, oname]))
except Exception as e: except Exception as e:
pass pass

2
sapl/base/search_indexes.py

@ -108,7 +108,7 @@ class TextExtractField(CharField):
continue continue
data += getattr(self, func)(value) + ' ' data += getattr(self, func)(value) + ' '
data = data.replace('\n', ' ') data = data.replace('\\n', ' ')
return data return data

7
sapl/base/templatetags/common_tags.py

@ -84,6 +84,9 @@ def desc_operation(value):
@register.filter @register.filter
def format_user(user): def format_user(user):
if not user:
return ""
if user.first_name: if user.first_name:
return user.first_name + " " + user.last_name + " (" + user.username + ")" return user.first_name + " " + user.last_name + " (" + user.username + ")"
else: else:
@ -297,7 +300,7 @@ def youtube_url(value):
# Test if YouTube video # Test if YouTube video
# tested on https://pythex.org/ # tested on https://pythex.org/
value = value.lower() value = value.lower()
youtube_pattern = "^((https?://)?(www\.)?youtube\.com\/watch\?v=)" youtube_pattern = r"^((https?://)?(www\.)?youtube\.com\/watch\?v=)"
r = re.findall(youtube_pattern, value) r = re.findall(youtube_pattern, value)
return True if r else False return True if r else False
@ -305,7 +308,7 @@ def youtube_url(value):
@register.filter @register.filter
def facebook_url(value): def facebook_url(value):
value = value.lower() value = value.lower()
facebook_pattern = "^((https?://)?((www|pt-br)\.)?facebook\.com(\/.+)?\/videos(\/.*)?)" facebook_pattern = r"^((https?://)?((www|pt-br)\.)?facebook\.com(\/.+)?\/videos(\/.*)?)"
r = re.findall(facebook_pattern, value) r = re.findall(facebook_pattern, value)
return True if r else False return True if r else False

26
sapl/base/templatetags/menus.py

@ -183,17 +183,21 @@ def resolve_urls_inplace(menu, pk, rm, context):
as funcionalidades diretas do MasterDetailCrud, como: as funcionalidades diretas do MasterDetailCrud, como:
- visualização de detalhes, adição, edição, remoção. - visualização de detalhes, adição, edição, remoção.
""" """
if 'view' in context: try:
view = context['view'] if 'view' in context:
if hasattr(view, '__class__') and\ view = context['view']
hasattr(view.__class__, 'crud'): if hasattr(view, 'crud'):
urls = view.__class__.crud.get_urls() urls = view.crud.get_urls()
for u in urls: for u in urls:
if (u.name == url_name or if (u.name == url_name or
'urls_extras' in menu and 'urls_extras' in menu and
u.name in menu['urls_extras']): u.name in menu['urls_extras']):
menu['active'] = 'active' menu['active'] = 'active'
break break
except:
url_active = menu.get('url', '')
logger.warning(
f'Não foi possível definir se url {url_active} é a url ativa.')
elif 'check_permission' in menu and not context[ elif 'check_permission' in menu and not context[
'request'].user.has_perm(menu['check_permission']): 'request'].user.has_perm(menu['check_permission']):
menu['active'] = '' menu['active'] = ''

21
sapl/base/views.py

@ -6,7 +6,7 @@ import os
from django.apps.registry import apps from django.apps.registry import apps
from django.contrib import messages from django.contrib import messages
from django.contrib.auth import get_user_model, views from django.contrib.auth import authenticate, login, get_user_model, views
from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from django.contrib.auth.tokens import default_token_generator from django.contrib.auth.tokens import default_token_generator
@ -51,7 +51,7 @@ from sapl.sessao.models import (Bancada, SessaoPlenaria)
from sapl.settings import EMAIL_SEND_USER from sapl.settings import EMAIL_SEND_USER
from sapl.utils import (gerar_hash_arquivo, intervalos_tem_intersecao, mail_service_configured, from sapl.utils import (gerar_hash_arquivo, intervalos_tem_intersecao, mail_service_configured,
SEPARADOR_HASH_PROPOSICAO, show_results_filter_set, google_recaptcha_configured, SEPARADOR_HASH_PROPOSICAO, show_results_filter_set, google_recaptcha_configured,
get_client_ip, sapn_is_enabled) get_client_ip, sapn_is_enabled, is_weak_password)
from .forms import (AlterarSenhaForm, CasaLegislativaForm, ConfiguracoesAppForm, EstatisticasAcessoNormasForm) from .forms import (AlterarSenhaForm, CasaLegislativaForm, ConfiguracoesAppForm, EstatisticasAcessoNormasForm)
from .models import AppConfig, CasaLegislativa from .models import AppConfig, CasaLegislativa
@ -75,6 +75,21 @@ class LoginSapl(views.LoginView):
template_name = 'base/login.html' template_name = 'base/login.html'
authentication_form = LoginForm authentication_form = LoginForm
def form_valid(self, form):
"""Override do comportamento padrão para verificar senha fraca"""
username = form.cleaned_data.get('username')
password = form.cleaned_data.get('password')
user = authenticate(self.request, username=username, password=password)
if user is not None:
login(self.request, user)
if is_weak_password(password):
self.request.session['weak_password'] = True
return redirect(self.get_success_url())
# Fallback se falhar a autenticação (tecnicamente não devia chegar aqui)
return super().form_invalid(form)
class ConfirmarEmailView(TemplateView): class ConfirmarEmailView(TemplateView):
template_name = "email/confirma.html" template_name = "email/confirma.html"
@ -1481,6 +1496,8 @@ class AlterarSenha(FormView):
user.set_password(new_password) user.set_password(new_password)
user.save() user.save()
self.request.session.pop('weak_password', None)
return super().form_valid(form) return super().form_valid(form)

2
sapl/compilacao/forms.py

@ -987,7 +987,7 @@ class DispositivoEdicaoVigenciaForm(ModelForm):
p.pk, _('%s realizada em %s. %s') % ( p.pk, _('%s realizada em %s. %s') % (
p.tipo_publicacao, p.tipo_publicacao,
defaultfilters.date( defaultfilters.date(
p.data, "d \d\e F \d\e Y"), p.data, r"d \d\e F \d\e Y"),
str(p.ta))) for p in pubs] str(p.ta))) for p in pubs]
dvs = Dispositivo.objects.order_by('ordem').filter( dvs = Dispositivo.objects.order_by('ordem').filter(

23
sapl/compilacao/models.py

@ -287,7 +287,7 @@ class TextoArticulado(TimestampedMixin):
return _('%(tipo)s%(numero)s, de %(data)s') % { return _('%(tipo)s%(numero)s, de %(data)s') % {
'tipo': self.tipo_ta, 'tipo': self.tipo_ta,
'numero': numero, 'numero': numero,
'data': defaultfilters.date(self.data, "d \d\e F \d\e Y").lower()} 'data': defaultfilters.date(self.data, r"d \d\e F \d\e Y").lower()}
def hash(self): def hash(self):
from django.core import serializers from django.core import serializers
@ -390,7 +390,6 @@ class TextoArticulado(TimestampedMixin):
@classonlymethod @classonlymethod
def update_or_create(cls, view_integracao, obj): def update_or_create(cls, view_integracao, obj):
map_fields = view_integracao.map_fields map_fields = view_integracao.map_fields
ta_values = getattr(view_integracao, 'ta_values', {}) ta_values = getattr(view_integracao, 'ta_values', {})
@ -944,7 +943,7 @@ class Publicacao(TimestampedMixin):
def __str__(self): def __str__(self):
return _('%s realizada em %s \n <small>%s</small>') % ( return _('%s realizada em %s \n <small>%s</small>') % (
self.tipo_publicacao, self.tipo_publicacao,
defaultfilters.date(self.data, "d \d\e F \d\e Y"), defaultfilters.date(self.data, r"d \d\e F \d\e Y"),
self.ta) self.ta)
@ -1182,6 +1181,24 @@ class Dispositivo(BaseModel, TimestampedMixin):
help_text=_('O recorte de imagem ' help_text=_('O recorte de imagem '
'é possível após a atualização.')) 'é possível após a atualização.'))
# define custom manager
class SelectRelatedManager(models.Manager):
def get_queryset(self):
return super().get_queryset().select_related('tipo_dispositivo',
'publicacao',
'ta',
'ta_publicado',
'dispositivo_subsequente',
'dispositivo_substituido',
'dispositivo_pai',
'dispositivo_pai__tipo_dispositivo',
'dispositivo_raiz',
'dispositivo_vigencia',
'dispositivo_atualizador'
)
# Replace the default manager with custom manager
objects = SelectRelatedManager()
class Meta: class Meta:
verbose_name = _('Dispositivo') verbose_name = _('Dispositivo')
verbose_name_plural = _('Dispositivos') verbose_name_plural = _('Dispositivos')

2
sapl/compilacao/urls.py

@ -23,7 +23,7 @@ urlpatterns_compilacao = [
url(r'^(?P<ta_id>[0-9]+)/text$', url(r'^(?P<ta_id>[0-9]+)/text$',
views.TextView.as_view(), name='ta_text'), views.TextView.as_view(), name='ta_text'),
url(r'^(?P<ta_id>[0-9]+)/text/vigencia/(?P<sign>.+)/$', url(r'^(?P<ta_id>[0-9]+)/text/vigencia/(?P<sign>.*:[A-Za-z0-9_-]+)/$',
views.TextView.as_view(), name='ta_vigencia'), views.TextView.as_view(), name='ta_vigencia'),
url(r'^(?P<ta_id>[0-9]+)/text/edit', url(r'^(?P<ta_id>[0-9]+)/text/edit',

22
sapl/context_processors.py

@ -1,14 +1,13 @@
import logging import logging
from django.conf import settings
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from sapl.utils import google_recaptcha_configured as google_recaptcha_configured_utils, sapn_is_enabled from sapl.utils import google_recaptcha_configured as \
google_recaptcha_configured_utils, sapn_is_enabled, cached_call, get_base_url
from sapl.utils import mail_service_configured as mail_service_configured_utils from sapl.utils import mail_service_configured as mail_service_configured_utils
def parliament_info(request): def parliament_info(request):
from sapl.base.views import get_casalegislativa from sapl.base.views import get_casalegislativa
casa = get_casalegislativa() casa = get_casalegislativa()
if casa: if casa:
@ -18,7 +17,6 @@ def parliament_info(request):
def mail_service_configured(request): def mail_service_configured(request):
if not mail_service_configured_utils(request): if not mail_service_configured_utils(request):
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logger.warning(_('Servidor de email não configurado.')) logger.warning(_('Servidor de email não configurado.'))
@ -27,7 +25,6 @@ def mail_service_configured(request):
def google_recaptcha_configured(request): def google_recaptcha_configured(request):
if not google_recaptcha_configured_utils(): if not google_recaptcha_configured_utils():
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logger.warning(_('Google Recaptcha não configurado.')) logger.warning(_('Google Recaptcha não configurado.'))
@ -35,10 +32,19 @@ def google_recaptcha_configured(request):
return {'google_recaptcha_configured': True} return {'google_recaptcha_configured': True}
@cached_call("site-title", timeout=60 * 2)
def enable_sapn(request): def enable_sapn(request):
verbose_name = _('Sistema de Apoio ao Processo Legislativo') \
if not sapn_is_enabled() \
else _('Sistema de Apoio à Publicação de Leis e Normas')
from sapl.base.models import CasaLegislativa
casa_legislativa = CasaLegislativa.objects.first()
nome_casa = casa_legislativa.nome if casa_legislativa and casa_legislativa.nome else ''
return { return {
'sapl_as_sapn': sapn_is_enabled(), 'sapl_as_sapn': sapn_is_enabled(),
'nome_sistema': _('Sistema de Apoio ao Processo Legislativo') 'nome_sistema': verbose_name,
if not sapn_is_enabled() 'nome_casa': nome_casa,
else _('Sistema de Apoio à Publicação de Leis e Normas') 'base_url': get_base_url(request),
} }

80
sapl/crud/base.py

@ -263,18 +263,18 @@ class CrudBaseMixin(CrispyLayoutFormMixin):
self.model_name_set = getattr( self.model_name_set = getattr(
obj.model, obj.model_set).field.model._meta.model_name obj.model, obj.model_set).field.model._meta.model_name
if hasattr(self, 'permission_required') and self.permission_required: if not hasattr(obj, 'public'):
if hasattr(obj, 'public'): obj.public = []
self.permission_required = list(
set(self.permission_required) - set(obj.public))
else:
obj.public = []
self.permission_required = tuple(( if hasattr(self, 'permission_required') and self.permission_required:
self.permission(pr) for pr in self.permission_required))
else: self.permission_required = tuple(
obj.public = [] (
self.permission(pr) for pr in (
set(self.permission_required) - set(obj.public)
)
)
)
@classmethod @classmethod
def url_name(cls, suffix): def url_name(cls, suffix):
@ -460,7 +460,8 @@ class CrudListView(PermissionRequiredContainerCrudMixin, ListView):
# URL padrão para primeira coluna da listagem # URL padrão para primeira coluna da listagem
url = self.resolve_url( url = self.resolve_url(
ACTION_DETAIL, args=(obj.id,)) if i == 0 else None ACTION_DETAIL, args=(obj.id,)) if i == 0 else None
# gera URL para matéria a partir de fk_urlify_for_list em layouts.yaml # gera URL para matéria a partir de fk_urlify_for_list em
# layouts.yaml
if i > 0 and func is not None: if i > 0 and func is not None:
url = getattr(self, func)(obj, name)[0] url = getattr(self, func)(obj, name)[0]
@ -495,7 +496,7 @@ class CrudListView(PermissionRequiredContainerCrudMixin, ListView):
if m: if m:
ss = get_field_display(m, n[-1])[1] ss = get_field_display(m, n[-1])[1]
ss = ( ss = (
('<br>' if '<ul>' in ss else ' - ') + ss) \ ('<br>' if '<ul>' in ss else ' - ') + ss) \
if ss and j != 0 and s else ss if ss and j != 0 and s else ss
except: except:
pass pass
@ -947,6 +948,8 @@ class CrudDeleteView(PermissionRequiredContainerCrudMixin,
class Crud: class Crud:
__abstract__ = True
BaseMixin = CrudBaseMixin BaseMixin = CrudBaseMixin
ListView = CrudListView ListView = CrudListView
CreateView = CrudCreateView CreateView = CrudCreateView
@ -955,22 +958,38 @@ class Crud:
DeleteView = CrudDeleteView DeleteView = CrudDeleteView
help_topic = '' help_topic = ''
class PublicMixin:
permission_required = []
@classonlymethod @classonlymethod
def get_urls(cls): def get_urls(cls):
def _add_base(view): def _add_base(view):
if view:
class CrudViewWithBase(cls.BaseMixin, view): if not view:
model = cls.model return
help_topic = cls.help_topic
crud = cls
CrudViewWithBase.__name__ = view.__name__ if not cls.__abstract__:
return CrudViewWithBase return view
pr = set(view.permission_required) if hasattr(
view, 'permission_required') else set()
if hasattr(view, 'permission_required') and \
view.permission_required and \
hasattr(cls, 'public') and \
cls.public:
#print(view.permission_required, view)
#print(cls.public, cls)
pr = pr - set(cls.public)
class CrudViewWithBase(cls.BaseMixin, view):
permission_required = tuple(pr)
model = cls.model
help_topic = cls.help_topic
crud = cls
CrudViewWithBase.__name__ = view.__name__
return CrudViewWithBase
CrudListView = _add_base(cls.ListView) CrudListView = _add_base(cls.ListView)
CrudCreateView = _add_base(cls.CreateView) CrudCreateView = _add_base(cls.CreateView)
@ -978,6 +997,21 @@ class Crud:
CrudUpdateView = _add_base(cls.UpdateView) CrudUpdateView = _add_base(cls.UpdateView)
CrudDeleteView = _add_base(cls.DeleteView) CrudDeleteView = _add_base(cls.DeleteView)
cruds = CrudListView, CrudCreateView, CrudDetailView, CrudUpdateView, CrudDeleteView
if cls.__abstract__:
class CRUD(cls):
__abstract__ = False
ListView = CrudListView
CreateView = CrudCreateView
DetailView = CrudDetailView
UpdateView = CrudUpdateView
DeleteView = CrudDeleteView
for c in cruds:
if c:
c.crud = CRUD
cruds_base = [ cruds_base = [
(CrudListView.get_url_regex() (CrudListView.get_url_regex()
if CrudListView else None, CrudListView, ACTION_LIST), if CrudListView else None, CrudListView, ACTION_LIST),
@ -1537,7 +1571,7 @@ class MasterDetailCrud(Crud):
class CrudBaseForListAndDetailExternalAppView(MasterDetailCrud): class CrudBaseForListAndDetailExternalAppView(MasterDetailCrud):
CreateView, UpdateView, DeleteView = None, None, None CreateView, UpdateView, DeleteView = None, None, None
class BaseMixin(Crud.PublicMixin, MasterDetailCrud.BaseMixin): class BaseMixin(MasterDetailCrud.BaseMixin):
def resolve_url(self, suffix, args=None): def resolve_url(self, suffix, args=None):
obj = self.crud if hasattr(self, 'crud') else self obj = self.crud if hasattr(self, 'crud') else self

2
sapl/lexml/forms.py

@ -31,7 +31,7 @@ class LexmlProvedorForm(ModelForm):
return cd return cd
if cd["xml"]: if cd["xml"]:
xml = re.sub("\n|\t", "", cd["xml"].strip()) xml = re.sub(r"\n|\t", "", cd["xml"].strip())
validar_xml(xml) validar_xml(xml)
validar_schema(xml) validar_schema(xml)

2
sapl/lexml/models.py

@ -25,7 +25,7 @@ class LexmlProvedor(models.Model): # LexmlRegistroProvedor
def pretty_xml(self): def pretty_xml(self):
import html import html
safe_xml = html.escape(self.xml) safe_xml = html.escape(self.xml)
return safe_xml.replace('\n', '<br/>').replace(' ', '&nbsp;') return safe_xml.replace('\\n', '<br/>').replace(' ', '&nbsp;')
class Meta: class Meta:
verbose_name = _('Provedor Lexml') verbose_name = _('Provedor Lexml')

11
sapl/materia/forms.py

@ -42,7 +42,7 @@ from sapl.utils import (autor_label, autor_modal, timing,
models_with_gr_for_model, qs_override_django_filter, models_with_gr_for_model, qs_override_django_filter,
SEPARADOR_HASH_PROPOSICAO, SEPARADOR_HASH_PROPOSICAO,
validar_arquivo, YES_NO_CHOICES, validar_arquivo, YES_NO_CHOICES,
GoogleRecapthaMixin) GoogleRecapthaMixin, get_client_ip)
from .models import (AcompanhamentoMateria, Anexada, Autoria, from .models import (AcompanhamentoMateria, Anexada, Autoria,
DespachoInicial, DocumentoAcessorio, Numeracao, DespachoInicial, DocumentoAcessorio, Numeracao,
@ -1039,6 +1039,7 @@ class MateriaLegislativaFilterSet(django_filters.FilterSet):
'ano_origem_externa', 'ano_origem_externa',
'data_origem_externa', 'data_origem_externa',
'local_origem_externa', 'local_origem_externa',
'regime_tramitacao',
] ]
def filter_ementa(self, queryset, name, value): def filter_ementa(self, queryset, name, value):
@ -1099,7 +1100,7 @@ class MateriaLegislativaFilterSet(django_filters.FilterSet):
('tramitacao__status', 6), ('tramitacao__status', 6),
]) ])
row9 = to_row( row9 = to_row(
[('materiaassunto__assunto', 6), ('indexacao', 6)]) [('materiaassunto__assunto', 4), ('indexacao', 4), ('regime_tramitacao', 4)])
row8 = to_row( row8 = to_row(
[ [
@ -1715,7 +1716,7 @@ class TramitacaoEmLoteForm(ModelForm):
('texto', 12) ('texto', 12)
]) ])
documentos_checkbox_HTML = ''' documentos_checkbox_HTML = r'''
<br\><br\><br\> <br\><br\><br\>
<fieldset> <fieldset>
<legend style="font-size: 24px;">Selecione as matérias para tramitação:</legend> <legend style="font-size: 24px;">Selecione as matérias para tramitação:</legend>
@ -2653,7 +2654,9 @@ class ConfirmarProposicaoForm(ProposicaoForm):
protocolo.save() protocolo.save()
HistoricoProposicao.objects.create(proposicao=proposicao, HistoricoProposicao.objects.create(proposicao=proposicao,
status='E') status='E',
user=self.initial['user'],
ip=self.initial['ip'])
self.instance.results['messages']['success'].append(_( self.instance.results['messages']['success'].append(_(
'Protocolo realizado com sucesso')) 'Protocolo realizado com sucesso'))

33
sapl/materia/migrations/0086_auto_20240711_1400.py

@ -0,0 +1,33 @@
# Generated by Django 2.2.28 on 2024-07-11 17:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('materia', '0085_auto_20231028_1838'),
]
operations = [
migrations.AlterField(
model_name='materialegislativa',
name='ano',
field=models.PositiveSmallIntegerField(choices=[(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='materialegislativa',
name='ano_origem_externa',
field=models.PositiveSmallIntegerField(blank=True, choices=[(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)], null=True, verbose_name='Ano'),
),
migrations.AlterField(
model_name='numeracao',
name='ano_materia',
field=models.PositiveSmallIntegerField(choices=[(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='proposicao',
name='ano',
field=models.PositiveSmallIntegerField(blank=True, choices=[(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)], default=None, null=True, verbose_name='Ano'),
),
]

26
sapl/materia/migrations/0087_update_viewdb_materiaemtramitacao.py

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-08-27 20:13
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('materia', '0086_auto_20240711_1400'),
]
operations = [
migrations.RunSQL("""
create or replace view materia_materiaemtramitacao as
select m.id as id,
m.id as materia_id,
t.id as tramitacao_id,
t.unidade_tramitacao_destino_id as unidade_tramitacao_atual_id
from materia_materialegislativa m
inner join materia_tramitacao t on (m.id = t.materia_id)
where t.id = (select max(id) from materia_tramitacao where materia_id = m.id)
order by m.id DESC
"""),
]

51
sapl/materia/models.py

@ -1,3 +1,4 @@
from datetime import datetime
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.fields import GenericRelation
@ -14,13 +15,12 @@ from sapl.comissoes.models import Comissao, Reuniao
from sapl.compilacao.models import (PerfilEstruturalTextoArticulado, from sapl.compilacao.models import (PerfilEstruturalTextoArticulado,
TextoArticulado) TextoArticulado)
from sapl.parlamentares.models import Parlamentar from sapl.parlamentares.models import Parlamentar
#from sapl.protocoloadm.models import Protocolo
from sapl.utils import (RANGE_ANOS, YES_NO_CHOICES, SaplGenericForeignKey, from sapl.utils import (RANGE_ANOS, YES_NO_CHOICES, SaplGenericForeignKey,
SaplGenericRelation, restringe_tipos_de_arquivo_txt, SaplGenericRelation, restringe_tipos_de_arquivo_txt,
texto_upload_path, get_settings_auth_user_model, texto_upload_path, get_settings_auth_user_model,
OverwriteStorage) OverwriteStorage)
# from sapl.protocoloadm.models import Protocolo
EM_TRAMITACAO = [(1, 'Sim'), EM_TRAMITACAO = [(1, 'Sim'),
(0, 'Não')] (0, 'Não')]
@ -183,7 +183,6 @@ def anexo_upload_path(instance, filename):
class MateriaLegislativa(models.Model): class MateriaLegislativa(models.Model):
tipo = models.ForeignKey( tipo = models.ForeignKey(
TipoMateriaLegislativa, TipoMateriaLegislativa,
on_delete=models.PROTECT, on_delete=models.PROTECT,
@ -280,7 +279,7 @@ class MateriaLegislativa(models.Model):
Autor, Autor,
through='Autoria', through='Autoria',
through_fields=('materia', 'autor'), through_fields=('materia', 'autor'),
symmetrical=False,) symmetrical=False, )
data_ultima_atualizacao = models.DateTimeField( data_ultima_atualizacao = models.DateTimeField(
blank=True, null=True, blank=True, null=True,
@ -323,7 +322,7 @@ class MateriaLegislativa(models.Model):
'numero': self.numero, 'numero': self.numero,
'data': defaultfilters.date( 'data': defaultfilters.date(
self.data_apresentacao, self.data_apresentacao,
"d \d\e F \d\e Y" r"d \d\e F \d\e Y"
)} )}
def data_entrada_protocolo(self): def data_entrada_protocolo(self):
@ -340,7 +339,13 @@ class MateriaLegislativa(models.Model):
if protocolo.timestamp: if protocolo.timestamp:
return protocolo.timestamp return protocolo.timestamp
elif protocolo.timestamp_data_hora_manual: elif protocolo.timestamp_data_hora_manual:
return protocolo.timestamp_data_hora_manual tz = timezone.localtime().tzinfo
return tz.localize(
datetime.combine(
protocolo.data,
protocolo.hora
)
)
elif protocolo.data: elif protocolo.data:
return protocolo.data return protocolo.data
@ -392,7 +397,7 @@ class Autoria(models.Model):
class Meta: class Meta:
verbose_name = _('Autoria') verbose_name = _('Autoria')
verbose_name_plural = _('Autorias') verbose_name_plural = _('Autorias')
unique_together = (('autor', 'materia'), ) unique_together = (('autor', 'materia'),)
ordering = ('-primeiro_autor', 'autor__nome') ordering = ('-primeiro_autor', 'autor__nome')
def __str__(self): def __str__(self):
@ -448,9 +453,9 @@ class PautaReuniao(models.Model):
def __str__(self): def __str__(self):
return _('Reunião: %(reuniao)s' return _('Reunião: %(reuniao)s'
' - Matéria: %(materia)s') % { ' - Matéria: %(materia)s') % {
'reuniao': self.reuniao, 'reuniao': self.reuniao,
'materia': self.materia 'materia': self.materia
} }
class Anexada(models.Model): class Anexada(models.Model):
@ -474,8 +479,8 @@ class Anexada(models.Model):
def __str__(self): def __str__(self):
return _('Principal: %(materia_principal)s' return _('Principal: %(materia_principal)s'
' - Anexada: %(materia_anexada)s') % { ' - Anexada: %(materia_anexada)s') % {
'materia_principal': self.materia_principal, 'materia_principal': self.materia_principal,
'materia_anexada': self.materia_anexada} 'materia_anexada': self.materia_anexada}
class AssuntoMateria(models.Model): class AssuntoMateria(models.Model):
@ -747,7 +752,6 @@ class Parecer(models.Model):
class Proposicao(models.Model): class Proposicao(models.Model):
autor = models.ForeignKey( autor = models.ForeignKey(
Autor, Autor,
null=True, null=True,
@ -926,7 +930,7 @@ class Proposicao(models.Model):
""" """
Ao ser recebida, irá gerar uma nova matéria ou um documento acessorio de uma existente Ao ser recebida, irá gerar uma nova matéria ou um documento acessorio de uma existente
materia_gerada = models.ForeignKey( materia_gerada = models.ForeignKey(
MateriaLegislativa, MateriaLegislativa,
blank=True, blank=True,
@ -970,13 +974,13 @@ class Proposicao(models.Model):
return '%s nº _____ %s' % ( return '%s nº _____ %s' % (
self.tipo, formats.date_format( self.tipo, formats.date_format(
self.data_envio if self.data_envio else timezone.now(), self.data_envio if self.data_envio else timezone.now(),
"\d\e d \d\e F \d\e Y")) r"\d\e d \d\e F \d\e Y"))
class Meta: class Meta:
ordering = ['-data_recebimento'] ordering = ['-data_recebimento']
verbose_name = _('Proposição') verbose_name = _('Proposição')
verbose_name_plural = _('Proposições') verbose_name_plural = _('Proposições')
unique_together = (('content_type', 'object_id'), ) unique_together = (('content_type', 'object_id'),)
permissions = ( permissions = (
('detail_proposicao_enviada', ('detail_proposicao_enviada',
_('Pode acessar detalhes de uma proposição enviada.')), _('Pode acessar detalhes de uma proposição enviada.')),
@ -1008,7 +1012,7 @@ class Proposicao(models.Model):
'numero': self.numero_proposicao, 'numero': self.numero_proposicao,
'data': defaultfilters.date( 'data': defaultfilters.date(
self.data_envio if self.data_envio else timezone.now(), self.data_envio if self.data_envio else timezone.now(),
"d \d\e F \d\e Y" r"d \d\e F \d\e Y"
)} )}
def delete(self, using=None, keep_parents=False): def delete(self, using=None, keep_parents=False):
@ -1022,7 +1026,8 @@ class Proposicao(models.Model):
def save(self, force_insert=False, force_update=False, using=None, def save(self, force_insert=False, force_update=False, using=None,
update_fields=None): update_fields=None):
# atualiza o usuario baseado no status da proposição (que esta sendo calculado pela data) # atualiza o usuario baseado no status da proposição (que esta sendo
# calculado pela data)
if self.data_envio is not None and not self.usuario_envio: if self.data_envio is not None and not self.usuario_envio:
self.usuario_envio = self.user self.usuario_envio = self.user
elif self.data_recebimento is not None and not self.usuario_recebimento: elif self.data_recebimento is not None and not self.usuario_recebimento:
@ -1254,6 +1259,16 @@ class MateriaEmTramitacao(models.Model):
MateriaLegislativa, on_delete=models.DO_NOTHING) MateriaLegislativa, on_delete=models.DO_NOTHING)
tramitacao = models.ForeignKey(Tramitacao, on_delete=models.DO_NOTHING) tramitacao = models.ForeignKey(Tramitacao, on_delete=models.DO_NOTHING)
unidade_tramitacao_atual = models.ForeignKey(
UnidadeTramitacao,
related_name='materiaemtramitacao_set',
on_delete=models.DO_NOTHING,
verbose_name=_('Unidade de Tramitação Atual'),
db_column='unidade_tramitacao_atual_id',
null=True,
blank=True
)
class Meta: class Meta:
managed = False managed = False
db_table = "materia_materiaemtramitacao" db_table = "materia_materiaemtramitacao"

2
sapl/materia/urls.py

@ -143,7 +143,7 @@ urlpatterns_proposicao = [
url(r'^proposicao/devolvida/', ProposicaoDevolvida.as_view(), url(r'^proposicao/devolvida/', ProposicaoDevolvida.as_view(),
name='proposicao-devolvida'), name='proposicao-devolvida'),
url(r'^proposicao/confirmar/P(?P<hash>[0-9A-Fa-f]+)/' url(r'^proposicao/confirmar/P(?P<hash>[0-9A-Fa-f]+)/'
'(?P<pk>\d+)', ConfirmarProposicao.as_view(), r'(?P<pk>\d+)', ConfirmarProposicao.as_view(),
name='proposicao-confirmar'), name='proposicao-confirmar'),
url(r'^sistema/proposicao/tipo/', url(r'^sistema/proposicao/tipo/',
include(TipoProposicaoCrud.get_urls())), include(TipoProposicaoCrud.get_urls())),

54
sapl/materia/views.py

@ -24,6 +24,7 @@ from django.shortcuts import render
from django.template import loader from django.template import loader
from django.urls import reverse from django.urls import reverse
from django.utils import formats, timezone from django.utils import formats, timezone
from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.views.generic import CreateView, ListView, TemplateView, UpdateView from django.views.generic import CreateView, ListView, TemplateView, UpdateView
from django.views.generic.base import RedirectView from django.views.generic.base import RedirectView
@ -53,7 +54,7 @@ from sapl.utils import (autor_label, autor_modal, gerar_hash_arquivo, get_base_u
get_client_ip, get_mime_type_from_file_extension, lista_anexados, get_client_ip, get_mime_type_from_file_extension, lista_anexados,
mail_service_configured, montar_row_autor, SEPARADOR_HASH_PROPOSICAO, mail_service_configured, montar_row_autor, SEPARADOR_HASH_PROPOSICAO,
show_results_filter_set, get_tempfile_dir, show_results_filter_set, get_tempfile_dir,
google_recaptcha_configured) google_recaptcha_configured, MultiFormatOutputMixin)
from .forms import (AcessorioEmLoteFilterSet, AcompanhamentoMateriaForm, from .forms import (AcessorioEmLoteFilterSet, AcompanhamentoMateriaForm,
AnexadaEmLoteFilterSet, AdicionarVariasAutoriasFilterSet, AnexadaEmLoteFilterSet, AdicionarVariasAutoriasFilterSet,
@ -104,9 +105,9 @@ def proposicao_texto(request, pk):
if proposicao.texto_original: if proposicao.texto_original:
if (not proposicao.data_recebimento and if (not proposicao.data_recebimento and
not proposicao.autor.operadores.filter( not proposicao.autor.operadores.filter(
id=request.user.id id=request.user.id
).exists() ).exists()
): ):
logger.error("user=" + username + ". Usuário ({}) não tem permissão para acessar o texto original." logger.error("user=" + username + ". Usuário ({}) não tem permissão para acessar o texto original."
.format(request.user.id)) .format(request.user.id))
@ -1261,11 +1262,11 @@ class HistoricoProposicaoView(PermissionRequiredMixin, ListView):
paginate_by = 10 paginate_by = 10
model = HistoricoProposicao model = HistoricoProposicao
permission_required = ( permission_required = (
'materia.list_historicoproposicao', 'materia.list_historicoproposicao',
'materia.add_historicoproposicao', 'materia.add_historicoproposicao',
'materia.change_historicoproposicao', 'materia.change_historicoproposicao',
'materia.delete_historicoproposicao', 'materia.delete_historicoproposicao',
'materia.detail_historicoproposicao', 'materia.detail_historicoproposicao',
) )
def get_queryset(self): def get_queryset(self):
@ -2039,11 +2040,34 @@ class AcompanhamentoExcluirView(TemplateView):
return HttpResponseRedirect(self.get_success_url()) return HttpResponseRedirect(self.get_success_url())
class MateriaLegislativaPesquisaView(FilterView): class MateriaLegislativaPesquisaView(MultiFormatOutputMixin, FilterView):
model = MateriaLegislativa model = MateriaLegislativa
filterset_class = MateriaLegislativaFilterSet filterset_class = MateriaLegislativaFilterSet
paginate_by = 50 paginate_by = 50
export_fields = [
'id', 'ano', 'numero', 'tipo__sigla', 'tipo__descricao', 'autoria', 'texto_original', 'ementa'
]
def hook_texto_original(self, obj):
url = self.request.build_absolute_uri('/')[:-1]
texto_original = obj.texto_original if not isinstance(
obj, dict) else obj["texto_original"]
return f'{url}/media/{texto_original}'
def hook_autoria(self, obj):
"""
Hook específico para pegar nomes dos autores (reverse query)
"""
try:
autores = [
str(autoria.autor.nome)
for autoria in obj.autoria_set.select_related('autor').all()
]
return ', '.join(autores)
except AttributeError:
return ''
def get_filterset_kwargs(self, filterset_class): def get_filterset_kwargs(self, filterset_class):
super().get_filterset_kwargs(filterset_class) super().get_filterset_kwargs(filterset_class)
@ -2078,6 +2102,9 @@ class MateriaLegislativaPesquisaView(FilterView):
"anexadas", "anexadas",
"tipo", "tipo",
"texto_articulado", "texto_articulado",
"relatoria_set",
"relatoria_set__comissao",
"relatoria_set__parlamentar",
"tramitacao_set", "tramitacao_set",
"tramitacao_set__status", "tramitacao_set__status",
"tramitacao_set__unidade_tramitacao_local", "tramitacao_set__unidade_tramitacao_local",
@ -2099,8 +2126,9 @@ class MateriaLegislativaPesquisaView(FilterView):
qs = qs.filter(materiaassunto__isnull=True) qs = qs.filter(materiaassunto__isnull=True)
if 'o' in self.request.GET and not self.request.GET['o']: if 'o' in self.request.GET and not self.request.GET['o']:
args = ['-ano', 'tipo__sequencia_regimental', '-numero'] if BaseAppConfig.attr('ordenacao_pesquisa_materia') == 'R' else ['-ano', 'tipo__sigla', '-numero'] args = ['-ano', 'tipo__sequencia_regimental', '-numero'] if BaseAppConfig.attr(
'ordenacao_pesquisa_materia') == 'R' else ['-ano', 'tipo__sigla', '-numero']
qs = qs.order_by(*args) qs = qs.order_by(*args)
kwargs.update({ kwargs.update({
@ -2964,7 +2992,7 @@ def create_pdf_docacessorios(materia):
materia.pk, materia.pk,
time.mktime(datetime.now().timetuple())) time.mktime(datetime.now().timetuple()))
merger = PdfFileMerger() merger = PdfFileMerger(strict=False)
for f in docs_path: for f in docs_path:
merger.append(fileobj=f) merger.append(fileobj=f)

21
sapl/middleware.py

@ -0,0 +1,21 @@
import logging
from django.shortcuts import redirect
from django.urls import reverse
class CheckWeakPasswordMiddleware:
logger = logging.getLogger(__name__)
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if request.user.is_authenticated and \
request.session.get('weak_password', False) and \
request.path != reverse('sapl.base:alterar_senha') and \
request.path != reverse('sapl.base:logout'):
logging.warning(f"Usuário {request.user.username} possui senha fraca.")
return redirect('sapl.base:alterar_senha')
return self.get_response(request)

24
sapl/norma/forms.py

@ -1,10 +1,11 @@
import logging import logging
import re
from crispy_forms.layout import (Button, Fieldset, HTML, Layout) from crispy_forms.layout import (Button, Fieldset, HTML, Layout)
from django import forms from django import forms
from django.contrib.postgres.search import SearchVector from django.contrib.postgres.search import SearchVector
from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.db.models import Q from django.db.models import Q, F, Func, Value
from django.forms import ModelChoiceField, ModelForm, widgets from django.forms import ModelChoiceField, ModelForm, widgets
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -69,6 +70,10 @@ class NormaFilterSet(django_filters.FilterSet):
label='Ano', label='Ano',
choices=choice_anos_com_normas) choices=choice_anos_com_normas)
numero = django_filters.CharFilter(
method='filter_numero',
label=_('Número'))
ementa = django_filters.CharFilter( ementa = django_filters.CharFilter(
method='filter_ementa', method='filter_ementa',
label=_('Pesquisar expressões na ementa da norma')) label=_('Pesquisar expressões na ementa da norma'))
@ -129,6 +134,19 @@ class NormaFilterSet(django_filters.FilterSet):
form_actions(label='Pesquisar')) form_actions(label='Pesquisar'))
) )
def filter_numero(self, queryset, name, value):
p = r'(\W|_)'
value = re.sub(p, '', value, flags=re.IGNORECASE)
return queryset.annotate(
numero_clean=Func(
F('numero'),
Value(p),
Value(''),
Value('g'),
function='REGEXP_REPLACE'
)
).filter(numero_clean=value)
def filter_ementa(self, queryset, name, value): def filter_ementa(self, queryset, name, value):
return queryset.annotate(search=SearchVector('ementa', return queryset.annotate(search=SearchVector('ementa',
config='portuguese')).filter(search=value) config='portuguese')).filter(search=value)
@ -206,7 +224,7 @@ class NormaJuridicaForm(FileFieldCheckMixin, ModelForm):
return cleaned_data return cleaned_data
import re import re
has_digits = re.sub('[^0-9]', '', cleaned_data['numero']) has_digits = re.sub(r'[^0-9]', '', cleaned_data['numero'])
if not has_digits: if not has_digits:
self.logger.error("Número de norma ({}) não pode conter somente letras.".format( self.logger.error("Número de norma ({}) não pode conter somente letras.".format(
cleaned_data['numero'])) cleaned_data['numero']))
@ -264,7 +282,7 @@ class NormaJuridicaForm(FileFieldCheckMixin, ModelForm):
texto_integral = self.cleaned_data.get('texto_integral', False) texto_integral = self.cleaned_data.get('texto_integral', False)
if texto_integral: if texto_integral:
validar_arquivo(texto_integral, "Texto Integral") validar_arquivo(texto_integral, "Texto Original")
return texto_integral return texto_integral

46
sapl/norma/migrations/0045_auto_20240711_1405.py

@ -0,0 +1,46 @@
# Generated by Django 2.2.28 on 2024-07-11 17:05
from django.db import migrations, models
import sapl.norma.models
import sapl.utils
class Migration(migrations.Migration):
dependencies = [
('norma', '0044_auto_20230529_1641'),
]
operations = [
migrations.AlterField(
model_name='anexonormajuridica',
name='ano',
field=models.PositiveSmallIntegerField(choices=[(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='normaestatisticas',
name='ano',
field=models.PositiveSmallIntegerField(choices=[(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)], default=sapl.norma.models.get_ano_atual, verbose_name='Ano'),
),
migrations.AlterField(
model_name='normajuridica',
name='ano',
field=models.PositiveSmallIntegerField(choices=[(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='normajuridica',
name='texto_integral',
field=models.FileField(blank=True,
max_length=300,
null=True,
storage=sapl.utils.OverwriteStorage(),
upload_to=sapl.norma.models.norma_upload_path,
validators=[
sapl.utils.restringe_tipos_de_arquivo_txt
],
verbose_name='Texto Original'),
),
]

37
sapl/norma/models.py

@ -138,7 +138,7 @@ class NormaJuridica(models.Model):
blank=True, blank=True,
null=True, null=True,
upload_to=norma_upload_path, upload_to=norma_upload_path,
verbose_name=_('Texto Integral'), verbose_name=_('Texto Original'),
storage=OverwriteStorage(), storage=OverwriteStorage(),
validators=[restringe_tipos_de_arquivo_txt]) validators=[restringe_tipos_de_arquivo_txt])
tipo = models.ForeignKey( tipo = models.ForeignKey(
@ -231,12 +231,22 @@ class NormaJuridica(models.Model):
ordering = ['-data', '-numero'] ordering = ['-data', '-numero']
def get_normas_relacionadas(self): def get_normas_relacionadas(self):
principais = NormaRelacionada.objects.filter( principais = NormaRelacionada.objects.\
norma_principal=self.id).order_by('norma_principal__data', select_related('tipo_vinculo',
'norma_relacionada__data') 'norma_principal',
relacionadas = NormaRelacionada.objects.filter( 'norma_relacionada',
norma_relacionada=self.id).order_by('norma_principal__data', 'norma_principal__tipo',
'norma_relacionada__data') 'norma_relacionada__tipo').\
filter(norma_principal=self.id).order_by('norma_principal__data',
'norma_relacionada__data')
relacionadas = NormaRelacionada.objects.\
select_related('tipo_vinculo',
'norma_principal',
'norma_relacionada',
'norma_principal__tipo',
'norma_relacionada__tipo').\
filter(norma_relacionada=self.id).order_by('norma_principal__data',
'norma_relacionada__data')
return (principais, relacionadas) return (principais, relacionadas)
def get_anexos_norma_juridica(self): def get_anexos_norma_juridica(self):
@ -253,12 +263,23 @@ class NormaJuridica(models.Model):
'tipo': self.tipo, 'tipo': self.tipo,
'orgao_sigla': f'-{self.orgao.sigla}' if self.orgao else '', 'orgao_sigla': f'-{self.orgao.sigla}' if self.orgao else '',
'numero': numero_norma, 'numero': numero_norma,
'data': defaultfilters.date(self.data, "d \d\e F \d\e Y").lower()} 'data': defaultfilters.date(self.data, r"d \d\e F \d\e Y").lower()}
@property @property
def epigrafe(self): def epigrafe(self):
return self.__str__() return self.__str__()
@property
def epigrafe_simplificada(self):
numero_norma = self.numero
if numero_norma.isnumeric():
numero_norma = '{0:,}'.format(int(self.numero)).replace(',', '.')
return _('%(tipo)s%(numero)s, de %(data)s') % {
'tipo': self.tipo,
'numero': numero_norma,
'data': defaultfilters.date(self.data, r"d \d\e F \d\e Y").lower()}
def delete(self, using=None, keep_parents=False): def delete(self, using=None, keep_parents=False):
texto_integral = self.texto_integral texto_integral = self.texto_integral
result = super().delete(using=using, keep_parents=keep_parents) result = super().delete(using=using, keep_parents=keep_parents)

19
sapl/norma/views.py

@ -28,7 +28,7 @@ from sapl.crud.base import (RP_DETAIL, RP_LIST, Crud, CrudAux,
MasterDetailCrud, make_pagination) MasterDetailCrud, make_pagination)
from sapl.materia.models import Orgao from sapl.materia.models import Orgao
from sapl.utils import show_results_filter_set, get_client_ip,\ from sapl.utils import show_results_filter_set, get_client_ip,\
sapn_is_enabled sapn_is_enabled, MultiFormatOutputMixin
from .forms import (AnexoNormaJuridicaForm, NormaFilterSet, NormaJuridicaForm, from .forms import (AnexoNormaJuridicaForm, NormaFilterSet, NormaJuridicaForm,
NormaPesquisaSimplesForm, NormaRelacionadaForm, NormaPesquisaSimplesForm, NormaRelacionadaForm,
@ -104,7 +104,7 @@ class PesquisarAssuntoNormaView(FilterView):
if 'assunto' in self.request.META['QUERY_STRING'] or\ if 'assunto' in self.request.META['QUERY_STRING'] or\
'page' in self.request.META['QUERY_STRING']: 'page' in self.request.META['QUERY_STRING']:
resultados = self.object_list resultados = self.object_list
else: else:
resultados = [] resultados = []
@ -147,11 +147,22 @@ class NormaRelacionadaCrud(MasterDetailCrud):
layout_key = 'NormaRelacionadaDetail' layout_key = 'NormaRelacionadaDetail'
class NormaPesquisaView(FilterView): class NormaPesquisaView(MultiFormatOutputMixin, FilterView):
model = NormaJuridica model = NormaJuridica
filterset_class = NormaFilterSet filterset_class = NormaFilterSet
paginate_by = 50 paginate_by = 50
export_fields = [
'id', 'ano', 'numero', 'tipo__sigla', 'tipo__descricao', 'texto_integral', 'ementa'
]
def hook_texto_integral(self, obj):
url = self.request.build_absolute_uri('/')[:-1]
texto_integral = obj.texto_integral if not isinstance(
obj, dict) else obj["texto_integral"]
return f'{url}/media/{texto_integral}'
def get_queryset(self): def get_queryset(self):
qs = super().get_queryset() qs = super().get_queryset()
@ -478,7 +489,7 @@ def recuperar_numero_norma(request):
norma = NormaJuridica.objects.filter(**param).order_by( norma = NormaJuridica.objects.filter(**param).order_by(
'tipo', 'ano', 'numero').values_list('numero', flat=True) 'tipo', 'ano', 'numero').values_list('numero', flat=True)
if norma: if norma:
numeros = sorted([int(re.sub("[^0-9].*", '', n)) for n in norma]) numeros = sorted([int(re.sub(r"[^0-9].*", '', n)) for n in norma])
next_num = numeros.pop() + 1 next_num = numeros.pop() + 1
response = JsonResponse({'numero': next_num, response = JsonResponse({'numero': next_num,
'ano': param['ano']}) 'ano': param['ano']})

5
sapl/parlamentares/forms.py

@ -13,7 +13,6 @@ from django.forms import ModelForm
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
import django_filters import django_filters
from floppyforms.widgets import ClearableFileInput
from image_cropping.widgets import CropWidget, ImageCropWidget from image_cropping.widgets import CropWidget, ImageCropWidget
from sapl.base.models import Autor, TipoAutor from sapl.base.models import Autor, TipoAutor
@ -26,10 +25,6 @@ from .models import (Coligacao, ComposicaoColigacao, Filiacao, Frente, Legislatu
Mandato, Parlamentar, Partido, Votante, Bloco, FrenteParlamentar, BlocoMembro) Mandato, Parlamentar, Partido, Votante, Bloco, FrenteParlamentar, BlocoMembro)
class ImageThumbnailFileInput(ClearableFileInput):
template_name = 'floppyforms/image_thumbnail.html'
class CustomImageCropWidget(ImageCropWidget): class CustomImageCropWidget(ImageCropWidget):
""" """
Custom ImageCropWidget that doesn't show the initial value of the field. Custom ImageCropWidget that doesn't show the initial value of the field.

1
sapl/parlamentares/models.py

@ -4,7 +4,6 @@ from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from image_cropping.fields import ImageCropField, ImageRatioField from image_cropping.fields import ImageCropField, ImageRatioField
from model_utils import Choices from model_utils import Choices
from prompt_toolkit.key_binding.bindings.named_commands import self_insert
from sapl.base.models import Autor from sapl.base.models import Autor
from sapl.decorators import vigencia_atual from sapl.decorators import vigencia_atual

30
sapl/parlamentares/urls.py

@ -37,6 +37,8 @@ urlpatterns = [
ProposicaoParlamentarCrud.get_urls() + ProposicaoParlamentarCrud.get_urls() +
RelatoriaParlamentarCrud.get_urls() + RelatoriaParlamentarCrud.get_urls() +
VotanteView.get_urls() VotanteView.get_urls()
)), )),
url(r'^parlamentar/pesquisar-parlamentar/', url(r'^parlamentar/pesquisar-parlamentar/',
@ -48,16 +50,21 @@ urlpatterns = [
url(r'^parlamentar/(?P<pk>\d+)/normas$', url(r'^parlamentar/(?P<pk>\d+)/normas$',
ParlamentarNormasView.as_view(), name='parlamentar_normas'), ParlamentarNormasView.as_view(), name='parlamentar_normas'),
url(r'^parlamentar/(?P<pk>\d+)/frentes/$', get_parlamentar_frentes, name='parlamentar_frentes'), url(r'^parlamentar/(?P<pk>\d+)/frentes/$',
get_parlamentar_frentes, name='parlamentar_frentes'),
url(r'^parlamentar/vincular-parlamentar/$', url(r'^parlamentar/vincular-parlamentar/$',
VincularParlamentarView.as_view(), name='vincular_parlamentar'), VincularParlamentarView.as_view(), name='vincular_parlamentar'),
url(r'^parlamentar/coligacao-legislatura/', coligacao_legislatura, name="coligacao_legislatura"), url(r'^parlamentar/coligacao-legislatura/',
url(r'^sistema/coligacao/', include(ColigacaoCrud.get_urls() + ComposicaoColigacaoCrud.get_urls())), coligacao_legislatura, name="coligacao_legislatura"),
url(r'^sistema/pesquisar-coligacao/', PesquisarColigacaoView.as_view(), name='pesquisar_coligacao'), url(r'^sistema/coligacao/', include(ColigacaoCrud.get_urls() +
ComposicaoColigacaoCrud.get_urls())),
url(r'^sistema/pesquisar-coligacao/',
PesquisarColigacaoView.as_view(), name='pesquisar_coligacao'),
url(r'^sistema/coligacao/', include(ColigacaoCrud.get_urls() + ComposicaoColigacaoCrud.get_urls())), url(r'^sistema/coligacao/', include(ColigacaoCrud.get_urls() +
ComposicaoColigacaoCrud.get_urls())),
url(r'^sistema/bloco/', include(BlocoCrud.get_urls())), url(r'^sistema/bloco/', include(BlocoCrud.get_urls())),
url(r'^sistema/bloco-cargo/', include(BlocoCargoCrud.get_urls())), url(r'^sistema/bloco-cargo/', include(BlocoCargoCrud.get_urls())),
@ -65,7 +72,8 @@ urlpatterns = [
url(r'^sistema/frente/', include(FrenteCrud.get_urls())), url(r'^sistema/frente/', include(FrenteCrud.get_urls())),
url(r'^sistema/frente-cargo/', include(FrenteCargoCrud.get_urls())), url(r'^sistema/frente-cargo/', include(FrenteCargoCrud.get_urls())),
url(r'^sistema/frente-parlamentares/', include(FrenteParlamentarCrud.get_urls())), url(r'^sistema/frente-parlamentares/',
include(FrenteParlamentarCrud.get_urls())),
url(r'^sistema/frente/atualiza-lista-parlamentares', url(r'^sistema/frente/atualiza-lista-parlamentares',
frente_atualiza_lista_parlamentares, frente_atualiza_lista_parlamentares,
@ -86,8 +94,10 @@ urlpatterns = [
include(TipoMilitarCrud.get_urls())), include(TipoMilitarCrud.get_urls())),
url(r'^sistema/parlamentar/partido/', include(PartidoCrud.get_urls())), url(r'^sistema/parlamentar/partido/', include(PartidoCrud.get_urls())),
url(r'^sistema/parlamentar/pesquisar-partido/', PesquisarPartidoView.as_view(), name='pesquisar_partido'), url(r'^sistema/parlamentar/pesquisar-partido/',
url(r'^sistema/parlamentar/partido/(?P<pk>\d+)/filiados$', parlamentares_filiados, name='parlamentares_filiados'), PesquisarPartidoView.as_view(), name='pesquisar_partido'),
url(r'^sistema/parlamentar/partido/(?P<pk>\d+)/filiados$',
parlamentares_filiados, name='parlamentares_filiados'),
url(r'^sistema/mesa-diretora/sessao-legislativa/', url(r'^sistema/mesa-diretora/sessao-legislativa/',
include(SessaoLegislativaCrud.get_urls())), include(SessaoLegislativaCrud.get_urls())),
@ -109,7 +119,7 @@ urlpatterns = [
url(r'^mesa-diretora/remove-parlamentar-composicao/$', url(r'^mesa-diretora/remove-parlamentar-composicao/$',
remove_parlamentar_composicao, name='remove_parlamentar_composicao'), remove_parlamentar_composicao, name='remove_parlamentar_composicao'),
url(r'^parlamentar/get-sessoes-legislatura/$', url(r'^parlamentar/get-sessoes-legislatura/$',
get_sessoes_legislatura, name='get_sessoes_legislatura'), get_sessoes_legislatura, name='get_sessoes_legislatura'),
] ]

12
sapl/parlamentares/views.py

@ -113,12 +113,13 @@ class VotanteView(MasterDetailCrud):
class FrenteList(MasterDetailCrud): class FrenteList(MasterDetailCrud):
public = [RP_DETAIL, RP_LIST]
model = Frente model = Frente
is_m2m = True is_m2m = True
parent_field = 'parlamentares' parent_field = 'parlamentares'
CreateView, UpdateView, DeleteView = None, None, None CreateView, UpdateView, DeleteView = None, None, None
class BaseMixin(Crud.PublicMixin, MasterDetailCrud.BaseMixin): class BaseMixin(MasterDetailCrud.BaseMixin):
list_field_names = ['nome', 'data_criacao', 'data_extincao'] list_field_names = ['nome', 'data_criacao', 'data_extincao']
@classmethod @classmethod
@ -127,6 +128,7 @@ class FrenteList(MasterDetailCrud):
class RelatoriaParlamentarCrud(CrudBaseForListAndDetailExternalAppView): class RelatoriaParlamentarCrud(CrudBaseForListAndDetailExternalAppView):
public = [RP_DETAIL, RP_LIST]
model = Relatoria model = Relatoria
parent_field = 'parlamentar' parent_field = 'parlamentar'
help_topic = 'tramitacao_relatoria' help_topic = 'tramitacao_relatoria'
@ -355,6 +357,7 @@ class PesquisarPartidoView(FilterView):
class ParticipacaoParlamentarCrud(CrudBaseForListAndDetailExternalAppView): class ParticipacaoParlamentarCrud(CrudBaseForListAndDetailExternalAppView):
public = [RP_DETAIL, RP_LIST]
model = Participacao model = Participacao
parent_field = 'parlamentar' parent_field = 'parlamentar'
namespace = AppConfig.name namespace = AppConfig.name
@ -589,6 +592,7 @@ def get_parlamentar_frentes(request, pk):
context = { context = {
'subnav_template_name': 'parlamentares/subnav.yaml', 'subnav_template_name': 'parlamentares/subnav.yaml',
'root_pk': pk, 'root_pk': pk,
'sexo_parlamentar': Parlamentar.objects.get(id=pk).sexo,
'nome_parlamentar': Parlamentar.objects.get(id=pk).nome_parlamentar, 'nome_parlamentar': Parlamentar.objects.get(id=pk).nome_parlamentar,
'frentes': frentes, 'frentes': frentes,
'num_frentes': len(frentes) 'num_frentes': len(frentes)
@ -1388,7 +1392,8 @@ def altera_field_mesa_public_view(request):
partido_parlamentar_sessao_legislativa(sessao, parlamentar)) partido_parlamentar_sessao_legislativa(sessao, parlamentar))
if parlamentar.fotografia: if parlamentar.fotografia:
try: try:
logger.warning(f"Iniciando cropping da imagem {parlamentar.fotografia}") logger.warning(
f"Iniciando cropping da imagem {parlamentar.fotografia}")
thumbnail_url = get_backend().get_thumbnail_url( thumbnail_url = get_backend().get_thumbnail_url(
parlamentar.fotografia, parlamentar.fotografia,
{ {
@ -1398,7 +1403,8 @@ def altera_field_mesa_public_view(request):
'detail': True, 'detail': True,
} }
) )
logger.warning(f"Cropping da imagem {parlamentar.fotografia} realizado com sucesso") logger.warning(
f"Cropping da imagem {parlamentar.fotografia} realizado com sucesso")
lista_fotos.append(thumbnail_url) lista_fotos.append(thumbnail_url)
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)

4
sapl/protocoloadm/forms.py

@ -1110,7 +1110,7 @@ class DocumentoAdministrativoForm(FileFieldCheckMixin, ModelForm):
numero_protocolo = self.data['numero_protocolo'] numero_protocolo = self.data['numero_protocolo']
ano_protocolo = self.data['ano_protocolo'] ano_protocolo = self.data['ano_protocolo']
complemento = re.sub('\s+', '', self.data['complemento']).upper() complemento = re.sub(r'\s+', '', self.data['complemento']).upper()
numero_documento = int(self.cleaned_data['numero']) numero_documento = int(self.cleaned_data['numero'])
tipo_documento = int(self.data['tipo']) tipo_documento = int(self.data['tipo'])
ano_documento = int(self.data['ano']) ano_documento = int(self.data['ano'])
@ -1558,7 +1558,7 @@ class TramitacaoEmLoteAdmForm(ModelForm):
('texto', 12) ('texto', 12)
]) ])
documentos_checkbox_HTML = ''' documentos_checkbox_HTML = r'''
<br\><br\><br\> <br\><br\><br\>
<fieldset> <fieldset>
<legend style="font-size: 24px;">Selecione os documentos para tramitação:</legend> <legend style="font-size: 24px;">Selecione os documentos para tramitação:</legend>

23
sapl/protocoloadm/migrations/0045_auto_20240711_1405.py

@ -0,0 +1,23 @@
# Generated by Django 2.2.28 on 2024-07-11 17:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('protocoloadm', '0044_auto_20230529_1641'),
]
operations = [
migrations.AlterField(
model_name='documentoadministrativo',
name='ano',
field=models.PositiveSmallIntegerField(choices=[(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='protocolo',
name='ano',
field=models.PositiveSmallIntegerField(choices=[(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 do Protocolo'),
),
]

14
sapl/protocoloadm/views.py

@ -47,7 +47,7 @@ from sapl.relatorios.views import relatorio_doc_administrativos
from sapl.utils import (create_barcode, get_base_url, get_client_ip, from sapl.utils import (create_barcode, get_base_url, get_client_ip,
get_mime_type_from_file_extension, lista_anexados, get_mime_type_from_file_extension, lista_anexados,
show_results_filter_set, mail_service_configured, from_date_to_datetime_utc, show_results_filter_set, mail_service_configured, from_date_to_datetime_utc,
google_recaptcha_configured, get_tempfile_dir) google_recaptcha_configured, get_tempfile_dir, MultiFormatOutputMixin)
from .forms import (AcompanhamentoDocumentoForm, AnexadoEmLoteFilterSet, AnexadoForm, from .forms import (AcompanhamentoDocumentoForm, AnexadoEmLoteFilterSet, AnexadoForm,
AnularProtocoloAdmForm, compara_tramitacoes_doc, AnularProtocoloAdmForm, compara_tramitacoes_doc,
@ -174,7 +174,7 @@ def create_pdf_docacessorios(docadministrativo):
logger.info("Gerando compilado PDF de documentos acessorios com {} documentos" logger.info("Gerando compilado PDF de documentos acessorios com {} documentos"
.format(docs_path)) .format(docs_path))
merger = PdfFileMerger() merger = PdfFileMerger(strict=False)
for f in docs_path: for f in docs_path:
merger.append(fileobj=f) merger.append(fileobj=f)
@ -448,7 +448,7 @@ class DocumentoAdministrativoCrud(Crud):
def form_valid(self, form): def form_valid(self, form):
form.instance.complemento = re.sub( form.instance.complemento = re.sub(
'\s+', '', form.instance.complemento).upper() r'\s+', '', form.instance.complemento).upper()
return super().form_valid(form) return super().form_valid(form)
class UpdateView(Crud.UpdateView): class UpdateView(Crud.UpdateView):
@ -481,7 +481,7 @@ class DocumentoAdministrativoCrud(Crud):
break break
form.instance.complemento = re.sub( form.instance.complemento = re.sub(
'\s+', '', form.instance.complemento).upper() r'\s+', '', form.instance.complemento).upper()
return super().form_valid(form) return super().form_valid(form)
@ -992,7 +992,6 @@ class ProtocoloMateriaView(PermissionRequiredMixin, CreateView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(CreateView, self).get_context_data(**kwargs) context = super(CreateView, self).get_context_data(**kwargs)
autores_ativos = self.autores_ativos() autores_ativos = self.autores_ativos()
autores = [] autores = []
autores.append(['0', '------']) autores.append(['0', '------'])
for a in autores_ativos: for a in autores_ativos:
@ -1034,6 +1033,7 @@ class ProtocoloMateriaTemplateView(PermissionRequiredMixin, TemplateView):
class PesquisarDocumentoAdministrativoView(DocumentoAdministrativoMixin, class PesquisarDocumentoAdministrativoView(DocumentoAdministrativoMixin,
MultiFormatOutputMixin,
PermissionRequiredMixin, PermissionRequiredMixin,
FilterView): FilterView):
model = DocumentoAdministrativo model = DocumentoAdministrativo
@ -1041,6 +1041,10 @@ class PesquisarDocumentoAdministrativoView(DocumentoAdministrativoMixin,
paginate_by = 10 paginate_by = 10
permission_required = ('protocoloadm.list_documentoadministrativo', ) permission_required = ('protocoloadm.list_documentoadministrativo', )
export_fields = [
'id', 'ano', 'numero', 'tipo__sigla', 'tipo__descricao', 'assunto'
]
def get_filterset_kwargs(self, filterset_class): def get_filterset_kwargs(self, filterset_class):
super(PesquisarDocumentoAdministrativoView, super(PesquisarDocumentoAdministrativoView,
self).get_filterset_kwargs(filterset_class) self).get_filterset_kwargs(filterset_class)

73
sapl/relatorios/forms.py

@ -3,17 +3,19 @@ from crispy_forms.bootstrap import (FormActions)
from crispy_forms.layout import (HTML, Button, Fieldset, from crispy_forms.layout import (HTML, Button, Fieldset,
Layout, Submit) Layout, Submit)
from django import forms from django import forms
from django.forms import ModelChoiceField
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.db.models import Q
from sapl.audiencia.models import AudienciaPublica from sapl.audiencia.models import AudienciaPublica
from sapl.base.models import Autor from sapl.base.models import Autor
from sapl.comissoes.models import Reuniao from sapl.comissoes.models import Reuniao
from sapl.crispy_layout_mixin import SaplFormHelper, to_row, form_actions from sapl.crispy_layout_mixin import SaplFormHelper, to_row, form_actions
from sapl.materia.models import DocumentoAcessorio, MateriaLegislativa, MateriaEmTramitacao, UnidadeTramitacao, \ from sapl.materia.models import DocumentoAcessorio, MateriaLegislativa, MateriaEmTramitacao, UnidadeTramitacao, \
StatusTramitacao StatusTramitacao, TipoMateriaLegislativa
from sapl.norma.models import NormaJuridica from sapl.norma.models import NormaJuridica
from sapl.protocoloadm.models import DocumentoAdministrativo from sapl.protocoloadm.models import DocumentoAdministrativo
from sapl.sessao.models import SessaoPlenaria from sapl.sessao.models import SessaoPlenaria, VotoParlamentar, RegistroVotacao
from sapl.utils import FilterOverridesMetaMixin, choice_anos_com_normas, qs_override_django_filter, \ from sapl.utils import FilterOverridesMetaMixin, choice_anos_com_normas, qs_override_django_filter, \
choice_anos_com_materias, choice_tipos_normas, autor_label, autor_modal choice_anos_com_materias, choice_tipos_normas, autor_label, autor_modal
@ -68,6 +70,70 @@ 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):
tipo_id = django_filters.ModelChoiceFilter(
queryset=TipoMateriaLegislativa.objects.all(),
method='ordem_or_expediente',
label='Tipo de Matéria',
empty_label="---------"
)
numero = django_filters.NumberFilter(
widget=forms.NumberInput(attrs={'class': 'form-control', 'step': 'any'}),
method='ordem_or_expediente',
label='Número'
)
ano = django_filters.ChoiceFilter(
choices=list(choice_anos_com_materias()),
widget=forms.Select(attrs={'class': 'form-control'}),
method='ordem_or_expediente',
label='Ano da Matéria'
)
class Meta(FilterOverridesMetaMixin):
model = RegistroVotacao
fields = ['data_hora']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.filters['data_hora'].label = 'Período (Data Inicial - Data Final)'
row0 = to_row([('tipo_id', 6), ('numero', 3), ('ano', 3)])
row1 = to_row([('data_hora', 12)])
buttons = FormActions(
*[
HTML("""
<div class="form-check">
<input name="relatorio" type="checkbox" class="form-check-input" id="relatorio">
<label class="form-check-label" for="relatorio">Gerar relatório PDF</label>
</div>
""")
],
Submit('pesquisar', _('Pesquisar'), css_class='float-right',
onclick='return true;'),
css_class='form-group row justify-content-between',
)
self.form.helper = SaplFormHelper()
self.form.helper.form_method = 'GET'
self.form.helper.layout = Layout(
Fieldset(_('Pesquisa'),
row0, row1,
buttons)
)
class RelatorioAtasFilterSet(django_filters.FilterSet): class RelatorioAtasFilterSet(django_filters.FilterSet):
class Meta(FilterOverridesMetaMixin): class Meta(FilterOverridesMetaMixin):
model = SessaoPlenaria model = SessaoPlenaria
@ -313,7 +379,8 @@ class RelatorioDataFimPrazoTramitacaoFilterSet(django_filters.FilterSet):
@property @property
def qs(self): def qs(self):
parent = super(RelatorioDataFimPrazoTramitacaoFilterSet, self).qs parent = super(RelatorioDataFimPrazoTramitacaoFilterSet, self).qs
return parent.distinct().prefetch_related('materia__tipo').order_by('tramitacao__data_fim_prazo', 'materia__tipo', 'materia__numero') return parent.distinct().prefetch_related('materia__tipo').order_by('tramitacao__data_fim_prazo',
'materia__tipo', 'materia__numero')
class Meta(FilterOverridesMetaMixin): class Meta(FilterOverridesMetaMixin):
model = MateriaEmTramitacao model = MateriaEmTramitacao

8
sapl/relatorios/urls.py

@ -11,7 +11,7 @@ from .views import (relatorio_capa_processo,
RelatorioMateriasTramitacaoView, RelatorioMateriaAnoAssuntoView, RelatorioHistoricoTramitacaoView, RelatorioMateriasTramitacaoView, RelatorioMateriaAnoAssuntoView, RelatorioHistoricoTramitacaoView,
RelatorioDataFimPrazoTramitacaoView, RelatorioPresencaSessaoView, RelatorioAtasView, RelatorioDataFimPrazoTramitacaoView, RelatorioPresencaSessaoView, RelatorioAtasView,
RelatorioReuniaoView, RelatorioAudienciaView, RelatorioHistoricoTramitacaoAdmView, RelatorioReuniaoView, RelatorioAudienciaView, RelatorioHistoricoTramitacaoAdmView,
RelatorioDocumentosAcessoriosView, RelatorioNormasPorAutorView) RelatorioDocumentosAcessoriosView, RelatorioNormasPorAutorView, RelatorioVotacoesNominaisView)
from ..base.views import EstatisticasAcessoNormas from ..base.views import EstatisticasAcessoNormas
app_name = AppConfig.name app_name = AppConfig.name
@ -95,6 +95,10 @@ urlpatterns = [
url(r'^sistema/relatorios/documentos_acessorios$', url(r'^sistema/relatorios/documentos_acessorios$',
RelatorioDocumentosAcessoriosView.as_view(), RelatorioDocumentosAcessoriosView.as_view(),
name='relatorio_documentos_acessorios'), name='relatorio_documentos_acessorios'),
url(r'^sistema/relatorios/votacoes_nominais$',
RelatorioVotacoesNominaisView.as_view(),
name='relatorio_votacoes_nominais'),
url(r'^sistema/relatorios/normas-por-autor$', url(r'^sistema/relatorios/normas-por-autor$',
RelatorioNormasPorAutorView.as_view(), name='normas_por_autor'), RelatorioNormasPorAutorView.as_view(), name='normas_por_autor'),
] ]

102
sapl/relatorios/views.py

@ -31,7 +31,8 @@ from sapl.relatorios.forms import RelatorioNormasPorAutorFilterSet, RelatorioHis
RelatorioNormasVigenciaFilterSet, RelatorioNormasMesFilterSet, RelatorioMateriasPorAutorFilterSet, \ RelatorioNormasVigenciaFilterSet, RelatorioNormasMesFilterSet, RelatorioMateriasPorAutorFilterSet, \
RelatorioMateriasPorAnoAutorTipoFilterSet, RelatorioMateriasTramitacaoFilterSet, RelatorioAudienciaFilterSet, \ RelatorioMateriasPorAnoAutorTipoFilterSet, RelatorioMateriasTramitacaoFilterSet, RelatorioAudienciaFilterSet, \
RelatorioReuniaoFilterSet, RelatorioDataFimPrazoTramitacaoFilterSet, RelatorioHistoricoTramitacaoFilterSet, \ RelatorioReuniaoFilterSet, RelatorioDataFimPrazoTramitacaoFilterSet, RelatorioHistoricoTramitacaoFilterSet, \
RelatorioPresencaSessaoFilterSet, RelatorioAtasFilterSet, RelatorioDocumentosAcessoriosFilterSet RelatorioPresencaSessaoFilterSet, RelatorioAtasFilterSet, RelatorioDocumentosAcessoriosFilterSet, \
RelatorioVotacoesNominaisFilterSet
from sapl.sessao.models import (ExpedienteMateria, ExpedienteSessao, from sapl.sessao.models import (ExpedienteMateria, ExpedienteSessao,
IntegranteMesa, JustificativaAusencia, IntegranteMesa, JustificativaAusencia,
Orador, OradorExpediente, Orador, OradorExpediente,
@ -50,7 +51,7 @@ from sapl.sessao.views import (get_identificacao_basica, get_mesa_diretora,
from sapl.settings import MEDIA_URL from sapl.settings import MEDIA_URL
from sapl.settings import STATIC_ROOT from sapl.settings import STATIC_ROOT
from sapl.utils import LISTA_DE_UFS, TrocaTag, filiacao_data, create_barcode, show_results_filter_set, \ from sapl.utils import LISTA_DE_UFS, TrocaTag, filiacao_data, create_barcode, show_results_filter_set, \
num_materias_por_tipo, parlamentares_ativos num_materias_por_tipo, parlamentares_ativos, MultiFormatOutputMixin
from .templates import (pdf_capa_processo_gerar, from .templates import (pdf_capa_processo_gerar,
pdf_documento_administrativo_gerar, pdf_espelho_gerar, pdf_documento_administrativo_gerar, pdf_espelho_gerar,
pdf_etiqueta_protocolo_gerar, pdf_materia_gerar, pdf_etiqueta_protocolo_gerar, pdf_materia_gerar,
@ -612,13 +613,13 @@ def get_sessao_plenaria(sessao, casa, user):
if not is_empty(conteudo): if not is_empty(conteudo):
# unescape HTML codes # unescape HTML codes
# https://github.com/interlegis/sapl/issues/1046 # https://github.com/interlegis/sapl/issues/1046
conteudo = re.sub('style=".*?"', '', conteudo) conteudo = re.sub(r'style=".*?"', '', conteudo)
conteudo = re.sub('class=".*?"', '', conteudo) conteudo = re.sub(r'class=".*?"', '', conteudo)
# OSTicket Ticket #796450 # OSTicket Ticket #796450
conteudo = re.sub('align=".*?"', '', conteudo) conteudo = re.sub(r'align=".*?"', '', conteudo)
conteudo = re.sub('<p\s+>', '<p>', conteudo) conteudo = re.sub(r'<p\s+>', '<p>', conteudo)
# OSTicket Ticket #796450 # OSTicket Ticket #796450
conteudo = re.sub('<br\s+/>', '<br/>', conteudo) conteudo = re.sub(r'<br\s+/>', '<br/>', conteudo)
conteudo = html.unescape(conteudo) conteudo = html.unescape(conteudo)
# escape special character '&' # escape special character '&'
@ -874,7 +875,7 @@ def get_sessao_plenaria(sessao, casa, user):
# unescape HTML codes # unescape HTML codes
# https://github.com/interlegis/sapl/issues/1046 # https://github.com/interlegis/sapl/issues/1046
conteudo = re.sub('style=".*?"', '', conteudo) conteudo = re.sub(r'style=".*?"', '', conteudo)
conteudo = html.unescape(conteudo) conteudo = html.unescape(conteudo)
# escape special character '&' # escape special character '&'
@ -894,7 +895,7 @@ def get_sessao_plenaria(sessao, casa, user):
# unescape HTML codes # unescape HTML codes
# https://github.com/interlegis/sapl/issues/1046 # https://github.com/interlegis/sapl/issues/1046
conteudo = re.sub('style=".*?"', '', conteudo) conteudo = re.sub(r'style=".*?"', '', conteudo)
conteudo = html.unescape(conteudo) conteudo = html.unescape(conteudo)
# escape special character '&' # escape special character '&'
@ -1321,13 +1322,13 @@ def get_pauta_sessao(sessao, casa):
if not is_empty(conteudo): if not is_empty(conteudo):
# unescape HTML codes # unescape HTML codes
# https://github.com/interlegis/sapl/issues/1046 # https://github.com/interlegis/sapl/issues/1046
conteudo = re.sub('style=".*?"', '', conteudo) conteudo = re.sub(r'style=".*?"', '', conteudo)
conteudo = re.sub('class=".*?"', '', conteudo) conteudo = re.sub(r'class=".*?"', '', conteudo)
# OSTicket Ticket #796450 # OSTicket Ticket #796450
conteudo = re.sub('align=".*?"', '', conteudo) conteudo = re.sub(r'align=".*?"', '', conteudo)
conteudo = re.sub('<p\s+>', '<p>', conteudo) conteudo = re.sub(r'<p\s+>', '<p>', conteudo)
# OSTicket Ticket #796450 # OSTicket Ticket #796450
conteudo = re.sub('<br\s+/>', '<br/>', conteudo) conteudo = re.sub(r'<br\s+/>', '<br/>', conteudo)
conteudo = html.unescape(conteudo) conteudo = html.unescape(conteudo)
# escape special character '&' # escape special character '&'
@ -1560,6 +1561,10 @@ def relatorio_documento_acessorio(obj, request, context):
return cria_relatorio(request, context, 'relatorios/relatorio_documento_acessorio.html') return cria_relatorio(request, context, 'relatorios/relatorio_documento_acessorio.html')
def relatorio_votacao_nominal(obj, request, context):
return cria_relatorio(request, context, 'relatorios/relatorio_votacao_nominal.html')
def relatorio_normas_por_autor(obj, request, context): def relatorio_normas_por_autor(obj, request, context):
return cria_relatorio(request, context, 'relatorios/relatorio_normas_por_autor.html') return cria_relatorio(request, context, 'relatorios/relatorio_normas_por_autor.html')
@ -1880,6 +1885,75 @@ class RelatorioDocumentosAcessoriosView(RelatorioMixin, FilterView):
return context return context
class RelatorioVotacoesNominaisView(RelatorioMixin, MultiFormatOutputMixin, FilterView):
model = VotoParlamentar
filterset_class = RelatorioVotacoesNominaisFilterSet
template_name = 'relatorios/RelatorioVotacoesNominais_filter.html'
relatorio = relatorio_votacao_nominal
paginate_by = 20
export_fields = [
'votacao_id', 'votacao', 'parlamentar__nome_parlamentar', 'voto'
]
def get_queryset(self):
query_params = Q(ordem__tipo_votacao=2)|Q(expediente__tipo_votacao=2)
if 'format' in self.request.GET:
order_fields = ['-votacao_id', 'parlamentar']
qs = VotoParlamentar.objects.filter(query_params).order_by(*order_fields)
else:
order_fields = ['-id']
qs = RegistroVotacao.objects.filter(query_params).order_by(*order_fields)
return qs
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = _('Votações Nominais')
if not self.filterset.form.is_valid():
return context
query_dict = self.request.GET.copy()
if 'page' in query_dict:
del query_dict['page']
context['filter_url'] = f"&{query_dict.urlencode()}" if query_dict else ''
context['show_results'] = show_results_filter_set(query_dict)
data_inicial = self.request.GET.get('data_hora_0', '')
data_final = self.request.GET.get('data_hora_1', '')
if not data_inicial:
data_inicial = "Data Inicial não definida"
if not data_final:
data_final = "Data Final não definida"
context['periodo'] = f"{data_inicial} - {data_final}"
tipo_id = self.request.GET.get('tipo_id')
numero = self.request.GET.get('numero')
ano = self.request.GET.get('ano')
if tipo_id:
context['tipo_materia'] = TipoMateriaLegislativa.objects.get(id=tipo_id)
if numero:
context['numero'] = int(numero)
if ano:
context['ano'] = ano
if 'relatorio' not in self.request.GET:
paginator = context['paginator']
page_obj = context['page_obj']
context['page_range'] = make_pagination(
page_obj.number, paginator.num_pages)
context['qtde_votacoes'] = paginator.count
else:
self.paginate_by = None
context['qtde_votacoes'] = len(context['object_list'])
return context
class RelatorioAtasView(RelatorioMixin, FilterView): class RelatorioAtasView(RelatorioMixin, FilterView):
model = SessaoPlenaria model = SessaoPlenaria
filterset_class = RelatorioAtasFilterSet filterset_class = RelatorioAtasFilterSet

2
sapl/rules/group_geral.py

@ -26,7 +26,7 @@ rules_group_geral = {
[RP_ADD], __perms_publicas__), [RP_ADD], __perms_publicas__),
(base.TipoAutor, __base__, __perms_publicas__), (base.TipoAutor, __base__, __perms_publicas__),
(base.Autor, __base__, __perms_publicas__), (base.Autor, __base__, __perms_publicas__),
(base.OperadorAutor, __base__, __perms_publicas__), (base.OperadorAutor, __base__, set()),
(base.AuditLog, __base__, set()), (base.AuditLog, __base__, set()),
(base.Metadata, __base__, set()), (base.Metadata, __base__, set()),

13
sapl/sessao/models.py

@ -641,11 +641,14 @@ class RegistroVotacao(models.Model):
ordering = ('id',) ordering = ('id',)
def __str__(self): def __str__(self):
return _('Ordem: %(ordem)s - Votação: %(votacao)s - ' if self.ordem:
'Matéria: %(materia)s') % { return _('Ordem: %(ordem)s - Votação: %(votacao)s') % {
'ordem': self.ordem, 'ordem': self.ordem,
'votacao': self.tipo_resultado_votacao, 'votacao': self.tipo_resultado_votacao}
'materia': self.materia} else:
return _('Expediente: %(expediente)s - Votação: %(votacao)s') % {
'expediente': self.expediente,
'votacao': self.tipo_resultado_votacao}
def clean(self): def clean(self):
"""Exatamente um dos campos ordem ou expediente deve estar preenchido. """Exatamente um dos campos ordem ou expediente deve estar preenchido.

142
sapl/sessao/views.py

@ -45,7 +45,8 @@ from sapl.sessao.forms import ExpedienteMateriaForm, OrdemDiaForm, OrdemExpedien
CorrespondenciaForm, CorrespondenciaEmLoteFilterSet CorrespondenciaForm, CorrespondenciaEmLoteFilterSet
from sapl.sessao.models import Correspondencia from sapl.sessao.models import Correspondencia
from sapl.settings import TIME_ZONE from sapl.settings import TIME_ZONE
from sapl.utils import show_results_filter_set, remover_acentos, get_client_ip from sapl.utils import show_results_filter_set, remover_acentos, get_client_ip,\
MultiFormatOutputMixin, PautaMultiFormatOutputMixin
from .forms import (AdicionarVariasMateriasFilterSet, BancadaForm, from .forms import (AdicionarVariasMateriasFilterSet, BancadaForm,
ExpedienteForm, JustificativaAusenciaForm, OcorrenciaSessaoForm, ListMateriaForm, ExpedienteForm, JustificativaAusenciaForm, OcorrenciaSessaoForm, ListMateriaForm,
@ -147,7 +148,8 @@ def verifica_votacoes_abertas(request):
for sessao in votacoes_abertas: for sessao in votacoes_abertas:
ordens = sessao.ordemdia_set.filter(votacao_aberta=True) ordens = sessao.ordemdia_set.filter(votacao_aberta=True)
expediente = sessao.expedientemateria_set.filter(votacao_aberta=True) expediente = sessao.expedientemateria_set.filter(
votacao_aberta=True)
for o in ordens: for o in ordens:
o.votacao_aberta = False o.votacao_aberta = False
o.save() o.save()
@ -167,7 +169,7 @@ def verifica_sessao_iniciada(request, spk, is_leitura=False):
aux_text = 'leitura' if is_leitura else 'votação' aux_text = 'leitura' if is_leitura else 'votação'
logger.info('user=' + username + '. Não é possível abrir matérias para {}. ' logger.info('user=' + username + '. Não é possível abrir matérias para {}. '
'Esta SessaoPlenaria (id={}) não foi iniciada ou está finalizada.'.format( 'Esta SessaoPlenaria (id={}) não foi iniciada ou está finalizada.'.format(
aux_text, spk)) aux_text, spk))
msg = _('Não é possível abrir matérias para {}. ' msg = _('Não é possível abrir matérias para {}. '
'Esta Sessão Plenária não foi iniciada ou está finalizada.' 'Esta Sessão Plenária não foi iniciada ou está finalizada.'
' Vá em "Abertura"->"Dados Básicos" e altere os valores dos campos necessários.'.format(aux_text)) ' Vá em "Abertura"->"Dados Básicos" e altere os valores dos campos necessários.'.format(aux_text))
@ -481,11 +483,11 @@ def customize_link_materia(context, pk, has_permission, is_expediente):
'mid': obj.materia_id}) 'mid': obj.materia_id})
resultado = ( resultado = (
'<a href="%s?page=%s">%s<br/><br/>%s</a>' % ( '<a href="%s?page=%s">%s<br/><br/>%s</a>' % (
url, url,
context.get('page', 1), context.get('page', 1),
resultado_descricao, resultado_descricao,
resultado_observacao)) resultado_observacao))
else: else:
if obj.tipo_votacao == NOMINAL: if obj.tipo_votacao == NOMINAL:
@ -496,7 +498,7 @@ def customize_link_materia(context, pk, has_permission, is_expediente):
'pk': obj.sessao_plenaria_id, 'pk': obj.sessao_plenaria_id,
'oid': obj.pk, 'oid': obj.pk,
'mid': obj.materia_id}) + \ 'mid': obj.materia_id}) + \
'?&materia=expediente' '?&materia=expediente'
else: else:
url = reverse( url = reverse(
'sapl.sessao:votacao_nominal_transparencia', 'sapl.sessao:votacao_nominal_transparencia',
@ -504,7 +506,7 @@ def customize_link_materia(context, pk, has_permission, is_expediente):
'pk': obj.sessao_plenaria_id, 'pk': obj.sessao_plenaria_id,
'oid': obj.pk, 'oid': obj.pk,
'mid': obj.materia_id}) + \ 'mid': obj.materia_id}) + \
'?&materia=ordem' '?&materia=ordem'
resultado = ('<a href="%s">%s<br/>%s</a>' % resultado = ('<a href="%s">%s<br/>%s</a>' %
(url, (url,
@ -519,7 +521,7 @@ def customize_link_materia(context, pk, has_permission, is_expediente):
'pk': obj.sessao_plenaria_id, 'pk': obj.sessao_plenaria_id,
'oid': obj.pk, 'oid': obj.pk,
'mid': obj.materia_id}) + \ 'mid': obj.materia_id}) + \
'?&materia=expediente' '?&materia=expediente'
else: else:
url = reverse( url = reverse(
'sapl.sessao:votacao_simbolica_transparencia', 'sapl.sessao:votacao_simbolica_transparencia',
@ -527,7 +529,7 @@ def customize_link_materia(context, pk, has_permission, is_expediente):
'pk': obj.sessao_plenaria_id, 'pk': obj.sessao_plenaria_id,
'oid': obj.pk, 'oid': obj.pk,
'mid': obj.materia_id}) + \ 'mid': obj.materia_id}) + \
'?&materia=ordem' '?&materia=ordem'
resultado = ('<a href="%s">%s<br/>%s</a>' % resultado = ('<a href="%s">%s<br/>%s</a>' %
(url, (url,
@ -791,7 +793,7 @@ class MateriaOrdemDiaCrud(MasterDetailCrud):
sessao_plenaria=self.kwargs['pk']).aggregate( sessao_plenaria=self.kwargs['pk']).aggregate(
Max('numero_ordem'))['numero_ordem__max'] Max('numero_ordem'))['numero_ordem__max']
self.initial['numero_ordem'] = ( self.initial['numero_ordem'] = (
max_numero_ordem if max_numero_ordem else 0) + 1 max_numero_ordem if max_numero_ordem else 0) + 1
return self.initial return self.initial
def get_success_url(self): def get_success_url(self):
@ -934,7 +936,7 @@ class ExpedienteMateriaCrud(MasterDetailCrud):
sessao_plenaria=self.kwargs['pk']).aggregate( sessao_plenaria=self.kwargs['pk']).aggregate(
Max('numero_ordem'))['numero_ordem__max'] Max('numero_ordem'))['numero_ordem__max']
initial['numero_ordem'] = ( initial['numero_ordem'] = (
max_numero_ordem if max_numero_ordem else 0) + 1 max_numero_ordem if max_numero_ordem else 0) + 1
return initial return initial
def get_success_url(self): def get_success_url(self):
@ -1414,7 +1416,7 @@ class PresencaView(FormMixin, PresencaMixin, DetailView):
# Id dos parlamentares presentes # Id dos parlamentares presentes
marcados = request.POST.getlist('presenca_ativos') \ marcados = request.POST.getlist('presenca_ativos') \
+ request.POST.getlist('presenca_inativos') + request.POST.getlist('presenca_inativos')
# Deletar os que foram desmarcados # Deletar os que foram desmarcados
deletar = set(presentes_banco) - set(marcados) deletar = set(presentes_banco) - set(marcados)
@ -1529,7 +1531,7 @@ class PresencaOrdemDiaView(FormMixin, PresencaMixin, DetailView):
# Id dos parlamentares presentes # Id dos parlamentares presentes
marcados = request.POST.getlist('presenca_ativos') \ marcados = request.POST.getlist('presenca_ativos') \
+ request.POST.getlist('presenca_inativos') + request.POST.getlist('presenca_inativos')
# Deletar os que foram desmarcados # Deletar os que foram desmarcados
deletar = set(presentes_banco) - set(marcados) deletar = set(presentes_banco) - set(marcados)
@ -1791,7 +1793,7 @@ def insere_parlamentar_composicao(request):
username = request.user.username username = request.user.username
if request.user.has_perm( if request.user.has_perm(
'%s.add_%s' % ( '%s.add_%s' % (
AppConfig.label, IntegranteMesa._meta.model_name)): AppConfig.label, IntegranteMesa._meta.model_name)):
composicao = IntegranteMesa() composicao = IntegranteMesa()
@ -1825,7 +1827,8 @@ def insere_parlamentar_composicao(request):
if parlamentar_ja_inserido: if parlamentar_ja_inserido:
logger.debug( logger.debug(
"user=" + username + ". Parlamentar (id={}) já inserido na sessao_plenaria(id={}) e cargo(ìd={})." "user=" + username +
". Parlamentar (id={}) já inserido na sessao_plenaria(id={}) e cargo(ìd={})."
.format(request.POST['parlamentar'], composicao.sessao_plenaria.id, composicao.cargo.id)) .format(request.POST['parlamentar'], composicao.sessao_plenaria.id, composicao.cargo.id))
return JsonResponse({'msg': ('Parlamentar já inserido!', 0)}) return JsonResponse({'msg': ('Parlamentar já inserido!', 0)})
@ -1854,7 +1857,7 @@ def remove_parlamentar_composicao(request):
username = request.user.username username = request.user.username
if request.POST and request.user.has_perm( if request.POST and request.user.has_perm(
'%s.delete_%s' % ( '%s.delete_%s' % (
AppConfig.label, IntegranteMesa._meta.model_name)): AppConfig.label, IntegranteMesa._meta.model_name)):
if 'composicao_mesa' in request.POST: if 'composicao_mesa' in request.POST:
try: try:
@ -2534,7 +2537,8 @@ class ExpedienteView(FormMixin, DetailView):
msg = _('Registro salvo com sucesso') msg = _('Registro salvo com sucesso')
messages.add_message(self.request, messages.SUCCESS, msg) messages.add_message(self.request, messages.SUCCESS, msg)
self.logger.info( self.logger.info(
'user=' + username + '. ExpedienteSessao(sessao_plenaria_id={} e tipo_id={}) salvo com sucesso.' 'user=' + username +
'. ExpedienteSessao(sessao_plenaria_id={} e tipo_id={}) salvo com sucesso.'
.format(self.object.id, tipo)) .format(self.object.id, tipo))
return self.form_valid(form) return self.form_valid(form)
@ -2907,7 +2911,7 @@ class VotacaoView(SessaoPermissionMixin):
username = request.user.username username = request.user.username
self.logger.error('user=' + username + '. Problemas ao salvar RegistroVotacao da materia de id={} ' self.logger.error('user=' + username + '. Problemas ao salvar RegistroVotacao da materia de id={} '
'e da ordem de id={}. '.format(materia_id, ordem_id) + str( 'e da ordem de id={}. '.format(materia_id, ordem_id) + str(
e)) e))
return self.form_invalid(form) return self.form_invalid(form)
else: else:
ordem = OrdemDia.objects.get(id=ordem_id) ordem = OrdemDia.objects.get(id=ordem_id)
@ -3805,10 +3809,28 @@ class PautaSessaoView(TemplateView):
reverse('sapl.sessao:pauta_sessao_detail', kwargs={'pk': sessao.pk})) reverse('sapl.sessao:pauta_sessao_detail', kwargs={'pk': sessao.pk}))
class PautaSessaoDetailView(DetailView): class PautaSessaoDetailView(PautaMultiFormatOutputMixin, DetailView):
template_name = "sessao/pauta_sessao_detail.html" template_name = "sessao/pauta_sessao_detail.html"
model = SessaoPlenaria model = SessaoPlenaria
export_fields = (
('id', 'ID'),
('periodo', 'Período'),
('titulo', 'Matéria'),
('autor', 'Autor'),
('ementa', 'Ementa'),
('situacao', 'Situação')
)
def hook_autor(self, obj):
return ','.join(obj['autor'])
def hook_titulo(self, obj):
return str(obj['titulo'])
def hook_situacao(self, obj):
return str(obj['situacao'])
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
from sapl.relatorios.views import relatorio_pauta_sessao_weasy # Evitar import ciclico from sapl.relatorios.views import relatorio_pauta_sessao_weasy # Evitar import ciclico
@ -3867,7 +3889,8 @@ class PautaSessaoDetailView(DetailView):
'situacao': ultima_tramitacao.status if ultima_tramitacao else _("Não informada"), 'situacao': ultima_tramitacao.status if ultima_tramitacao else _("Não informada"),
'processo': f'{str(numeracao.numero_materia)}/{str(numeracao.ano_materia)}' if numeracao else '-', 'processo': f'{str(numeracao.numero_materia)}/{str(numeracao.ano_materia)}' if numeracao else '-',
'autor': [str(x.autor) for x in m.materia.autoria_set.select_related('autor').all()], 'autor': [str(x.autor) for x in m.materia.autoria_set.select_related('autor').all()],
'turno': get_turno(ultima_tramitacao.turno) if ultima_tramitacao else '' 'turno': get_turno(ultima_tramitacao.turno) if ultima_tramitacao else '',
'periodo': 'expediente',
}) })
context.update({'materia_expediente': materias_expediente}) context.update({'materia_expediente': materias_expediente})
@ -3951,7 +3974,8 @@ class PautaSessaoDetailView(DetailView):
'situacao': ultima_tramitacao.status if ultima_tramitacao else _("Não informada"), 'situacao': ultima_tramitacao.status if ultima_tramitacao else _("Não informada"),
'processo': f'{str(numeracao.numero_materia)}/{str(numeracao.ano_materia)}' if numeracao else '-', 'processo': f'{str(numeracao.numero_materia)}/{str(numeracao.ano_materia)}' if numeracao else '-',
'autor': [str(x.autor) for x in Autoria.objects.select_related("autor").filter(materia_id=o.materia_id)], 'autor': [str(x.autor) for x in Autoria.objects.select_related("autor").filter(materia_id=o.materia_id)],
'turno': get_turno(ultima_tramitacao.turno) if ultima_tramitacao else '' 'turno': get_turno(ultima_tramitacao.turno) if ultima_tramitacao else '',
'periodo': 'ordem dia',
}) })
context.update({ context.update({
@ -3966,13 +3990,21 @@ class PautaSessaoDetailView(DetailView):
return self.render_to_response(context) return self.render_to_response(context)
class PesquisarSessaoPlenariaView(FilterView): class PesquisarSessaoPlenariaView(MultiFormatOutputMixin, FilterView):
model = SessaoPlenaria model = SessaoPlenaria
filterset_class = SessaoPlenariaFilterSet filterset_class = SessaoPlenariaFilterSet
paginate_by = 10 paginate_by = 10
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
viewname = 'sapl.sessao:pesquisar_sessao'
queryset_values_for_formats = False
export_fields = [
'id', 'data_inicio', 'hora_inicio', 'data_fim', 'hora_fim', '',
]
def get_filterset_kwargs(self, filterset_class): def get_filterset_kwargs(self, filterset_class):
super().get_filterset_kwargs(filterset_class) super().get_filterset_kwargs(filterset_class)
@ -3989,47 +4021,60 @@ class PesquisarSessaoPlenariaView(FilterView):
}) })
return kwargs return kwargs
def hook_header_(self):
return force_text(_('Título'))
def hook_(self, obj):
return str(obj)
def hook_data_inicio(self, obj):
return str(obj.data_inicio or '')
def hook_data_fim(self, obj):
return str(obj.data_fim or '')
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['title'] = _('Pesquisar Sessão Plenária') context['title'] = _('Pesquisar Sessão Plenária')
paginator = context['paginator'] paginator = context['paginator']
page_obj = context['page_obj'] page_obj = context['page_obj']
context['page_range'] = make_pagination( context['page_range'] = make_pagination(
page_obj.number, paginator.num_pages) page_obj.number, paginator.num_pages)
return context context['show_results'] = show_results_filter_set(
self.request.GET.copy())
def get(self, request, *args, **kwargs):
super().get(request)
# Se a pesquisa estiver quebrando com a paginação
# Olhe esta função abaixo
# Provavelmente você criou um novo campo no Form/FilterSet
# Então a ordem da URL está diferente
data = self.filterset.data data = self.filterset.data
if data and data.get('data_inicio__year') is not None: if data and data.get('data_inicio__year') is not None:
url = "&" + str(self.request.META['QUERY_STRING']) url = "&" + str(self.request.META['QUERY_STRING'])
if url.startswith("&page"): if url.startswith("&page"):
ponto_comeco = url.find('data_inicio__year=') - 1 ponto_comeco = url.find('data_inicio__year=') - 1
url = url[ponto_comeco:] url = url[ponto_comeco:]
else: context['filter_url'] = url
url = ''
context = self.get_context_data(filter=self.filterset, context['numero_res'] = len(self.object_list)
object_list=self.object_list,
filter_url=url,
numero_res=len(self.object_list)
)
context['show_results'] = show_results_filter_set( return context
self.request.GET.copy())
def get(self, request, *args, **kwargs):
r = super().get(request)
data = self.filterset.data
if not data:
return HttpResponseRedirect(
reverse(
self.viewname
) + f'?data_inicio__year={timezone.now().year}'
)
username = request.user.username username = request.user.username
self.logger.debug('user=' + username + '. Pesquisa de SessaoPlenaria.') self.logger.debug('user=' + username + '. Pesquisa de SessaoPlenaria.')
return self.render_to_response(context) return r
class PesquisarPautaSessaoView(PesquisarSessaoPlenariaView): class PesquisarPautaSessaoView(PesquisarSessaoPlenariaView):
@ -4038,6 +4083,8 @@ class PesquisarPautaSessaoView(PesquisarSessaoPlenariaView):
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
viewname = 'sapl.sessao:pesquisar_pauta'
def get_filterset_kwargs(self, filterset_class): def get_filterset_kwargs(self, filterset_class):
kwargs = super().get_filterset_kwargs(filterset_class) kwargs = super().get_filterset_kwargs(filterset_class)
qs = kwargs.get('queryset') qs = kwargs.get('queryset')
@ -4318,7 +4365,7 @@ def mudar_ordem_materia_sessao(request):
class JustificativaAusenciaCrud(MasterDetailCrud): class JustificativaAusenciaCrud(MasterDetailCrud):
model = JustificativaAusencia model = JustificativaAusencia
public = [RP_LIST, RP_DETAIL, ] public = [RP_LIST]
parent_field = 'sessao_plenaria' parent_field = 'sessao_plenaria'
class BaseMixin(MasterDetailCrud.BaseMixin): class BaseMixin(MasterDetailCrud.BaseMixin):
@ -4456,8 +4503,11 @@ class LeituraEmBloco(PermissionRequiredForAppCrudMixin, ListView):
leituras.append(obj) leituras.append(obj)
RegistroLeitura.objects.bulk_create(leituras) RegistroLeitura.objects.bulk_create(leituras)
models.update(resultado='Matéria Lida')
else: else:
messages.add_message(self.request, messages.ERROR, _('Nenhuma matéria selecionada para leitura em Bloco')) messages.add_message(self.request, messages.ERROR, _(
'Nenhuma matéria selecionada para leitura em Bloco'))
return self.get(request, self.kwargs) return self.get(request, self.kwargs)
return HttpResponseRedirect(self.get_success_url()) return HttpResponseRedirect(self.get_success_url())
@ -5263,7 +5313,7 @@ class CorrespondenciaCrud(MasterDetailCrud):
sessao_plenaria=self.kwargs['pk']).aggregate( sessao_plenaria=self.kwargs['pk']).aggregate(
Max('numero_ordem'))['numero_ordem__max'] Max('numero_ordem'))['numero_ordem__max']
initial['numero_ordem'] = ( initial['numero_ordem'] = (
max_numero_ordem if max_numero_ordem else 0) + 1 max_numero_ordem if max_numero_ordem else 0) + 1
return initial return initial

137
sapl/settings.py

@ -24,13 +24,15 @@ from unipath import Path
logging.captureWarnings(True) logging.captureWarnings(True)
logger = logging.getLogger(__name__)
host = socket.gethostbyname_ex(socket.gethostname())[0] host = socket.gethostbyname_ex(socket.gethostname())[0]
BASE_DIR = Path(__file__).ancestor(1) BASE_DIR = Path(__file__).ancestor(1)
PROJECT_DIR = Path(__file__).ancestor(2) PROJECT_DIR = Path(__file__).ancestor(2)
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = config('SECRET_KEY', default='') SECRET_KEY = config('SECRET_KEY', default='32jk1h412l3kjh421lkj4hlkj234')
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = config('DEBUG', default=False, cast=bool) DEBUG = config('DEBUG', default=False, cast=bool)
@ -41,7 +43,7 @@ ALLOWED_HOSTS = ['*']
LOGIN_REDIRECT_URL = '/' LOGIN_REDIRECT_URL = '/'
LOGIN_URL = '/login/?next=' LOGIN_URL = '/login/?next='
SAPL_VERSION = '3.1.163-RC20' SAPL_VERSION = '3.1.164-RC1'
if DEBUG: if DEBUG:
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
@ -68,36 +70,37 @@ SAPL_APPS = (
) )
INSTALLED_APPS = ( INSTALLED_APPS = (
'django_admin_bootstrapped', # must come before django.contrib.admin 'django_admin_bootstrapped', # must come before django.contrib.admin
'django.contrib.admin', 'django.contrib.admin',
'django.contrib.auth', 'django.contrib.auth',
'django.contrib.contenttypes', 'django.contrib.contenttypes',
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'django_extensions', 'django.forms',
'django_extensions',
'crispy_forms', 'crispy_forms',
'floppyforms',
'waffle', 'waffle',
'drf_spectacular', 'drf_spectacular',
'rest_framework', 'rest_framework',
'rest_framework.authtoken', 'rest_framework.authtoken',
'django_filters', 'django_filters',
'easy_thumbnails', 'easy_thumbnails',
'image_cropping', 'image_cropping',
'haystack', 'haystack',
'django.contrib.postgres', 'django.contrib.postgres',
'webpack_loader', 'webpack_loader',
'django_prometheus', 'django_prometheus',
) + SAPL_APPS ) + SAPL_APPS
# FTS = Full Text Search # FTS = Full Text Search
# Desabilita a indexação textual até encontramos uma solução para a issue # Desabilita a indexação textual até encontramos uma solução para a issue
@ -111,6 +114,10 @@ USE_SOLR = config('USE_SOLR', cast=bool, default=False)
SOLR_URL = config('SOLR_URL', cast=str, default='http://localhost:8983') SOLR_URL = config('SOLR_URL', cast=str, default='http://localhost:8983')
SOLR_COLLECTION = config('SOLR_COLLECTION', cast=str, default='sapl') SOLR_COLLECTION = config('SOLR_COLLECTION', cast=str, default='sapl')
# FOR HAYSTACK 3.3.1 (Django >= 3)
# SOLR_USER = config('SOLR_USER', cast=str)
# SOLR_PASSWORD = config('SOLR_PASSWORD', cast=str)
if USE_SOLR: if USE_SOLR:
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor' # enable auto-index HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor' # enable auto-index
SEARCH_BACKEND = 'haystack.backends.solr_backend.SolrEngine' SEARCH_BACKEND = 'haystack.backends.solr_backend.SolrEngine'
@ -123,6 +130,10 @@ HAYSTACK_CONNECTIONS = {
SEARCH_URL[0]: SEARCH_URL[1], SEARCH_URL[0]: SEARCH_URL[1],
'BATCH_SIZE': 1000, 'BATCH_SIZE': 1000,
'TIMEOUT': 20, 'TIMEOUT': 20,
# 'KWARGS': {
# 'timeout': 60,
# 'auth': (SOLR_USER, SOLR_PASSWORD), # Basic Auth
# },
}, },
} }
@ -140,6 +151,7 @@ MIDDLEWARE = [
'whitenoise.middleware.WhiteNoiseMiddleware', 'whitenoise.middleware.WhiteNoiseMiddleware',
'django_prometheus.middleware.PrometheusAfterMiddleware', 'django_prometheus.middleware.PrometheusAfterMiddleware',
'waffle.middleware.WaffleMiddleware', 'waffle.middleware.WaffleMiddleware',
'sapl.middleware.CheckWeakPasswordMiddleware',
] ]
if DEBUG: if DEBUG:
INSTALLED_APPS += ('debug_toolbar',) INSTALLED_APPS += ('debug_toolbar',)
@ -192,11 +204,14 @@ CACHES = {
'default': { 'default': {
'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache', 'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
'LOCATION': '/var/tmp/django_cache', 'LOCATION': '/var/tmp/django_cache',
'OPTIONS': {"MAX_ENTRIES": 10000},
} }
} }
ROOT_URLCONF = 'sapl.urls' ROOT_URLCONF = 'sapl.urls'
FORM_RENDERER = 'django.forms.renderers.TemplatesSetting'
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', 'BACKEND': 'django.template.backends.django.DjangoTemplates',
@ -225,17 +240,47 @@ WSGI_APPLICATION = 'sapl.wsgi.application'
# Database # Database
# https://docs.djangoproject.com/en/1.8/ref/settings/#databases # https://docs.djangoproject.com/en/1.8/ref/settings/#databases
# Parse DATABASE_URL
# dj-database-url==0.5.0 is the latest compatible with Django 2.2, later versions required Django >= 4
# but it doesn't support OPTIONS tag, so we need setup_db_tz
# This should be removed once we are able to upgrade to Django >= 4
DATABASES = { DATABASES = {
'default': config( "default": config("DATABASE_URL", cast=db_url)
'DATABASE_URL', default='sqlite://:memory:',
cast=db_url,
)
} }
def setup_db_tz():
db = DATABASES["default"]
# Normalize legacy engine alias returned by old dj-database-url
if db.get("ENGINE") == "django.db.backends.postgresql_psycopg2":
db["ENGINE"] = "django.db.backends.postgresql"
# Force UTC per connection for Postgres (fixes Django’s utc_tzinfo_factory assertion)
if db.get("ENGINE") == "django.db.backends.postgresql":
opts = db.setdefault("OPTIONS", {})
existing = (opts.get("options") or "").strip()
force_utc = "-c timezone=UTC"
opts["options"] = f"{existing} {force_utc}".strip() if existing else force_utc
# ensure default TCP port if you use HOST; leave sockets alone if HOST is empty
if db.get("HOST") and not db.get("PORT"):
db["PORT"] = "5432"
# Add connection lifetime
# in recent dj-database-url versions, replace by config("DATABASE_URL", conn_max_age=300)
db["CONN_MAX_AGE"] = 300 # keep connections for 5 minutes
# Log if DEBUG mode
if config("DEBUG", default=False, cast=bool):
logger.debug("DB config: %r", db)
setup_db_tz()
IMAGE_CROPPING_JQUERY_URL = None IMAGE_CROPPING_JQUERY_URL = None
THUMBNAIL_PROCESSORS = ( THUMBNAIL_PROCESSORS = (
'image_cropping.thumbnail_processors.crop_corners', 'image_cropping.thumbnail_processors.crop_corners',
) + thumbnail_settings.THUMBNAIL_PROCESSORS ) + thumbnail_settings.THUMBNAIL_PROCESSORS
THUMBNAIL_SOURCE_GENERATORS = ( THUMBNAIL_SOURCE_GENERATORS = (
'sapl.utils.pil_image', 'sapl.utils.pil_image',
@ -266,7 +311,6 @@ WAFFLE_CREATE_MISSING_SWITCHES = True
WAFFLE_LOG_MISSING_SWITCHES = True WAFFLE_LOG_MISSING_SWITCHES = True
WAFFLE_ENABLE_ADMIN_PAGES = True WAFFLE_ENABLE_ADMIN_PAGES = True
MAX_DOC_UPLOAD_SIZE = 150 * 1024 * 1024 # 150MB MAX_DOC_UPLOAD_SIZE = 150 * 1024 * 1024 # 150MB
MAX_IMAGE_UPLOAD_SIZE = 2 * 1024 * 1024 # 2MB MAX_IMAGE_UPLOAD_SIZE = 2 * 1024 * 1024 # 2MB
DATA_UPLOAD_MAX_MEMORY_SIZE = 10 * 1024 * 1024 # 10MB DATA_UPLOAD_MAX_MEMORY_SIZE = 10 * 1024 * 1024 # 10MB
@ -286,6 +330,30 @@ if not TIME_ZONE:
USE_I18N = True USE_I18N = True
USE_L10N = True USE_L10N = True
USE_TZ = True USE_TZ = True
##
## Monkey patch of the Django 2.2 because latest version of psycopg2 returns DB time zone as UTC,
## but Django 2.2 requires an int! This should be removed once we are able to upgrade to Django >= 4
##
import importlib
from django.utils.timezone import utc
pg_utils = importlib.import_module("django.db.backends.postgresql.utils")
def _compat_utc_tzinfo_factory(offset):
try:
minutes = int(offset.total_seconds() // 60) if hasattr(offset, "total_seconds") else int(offset)
except Exception:
raise AssertionError("database connection isn't set to UTC")
if minutes != 0:
raise AssertionError("database connection isn't set to UTC")
return utc
pg_utils.utc_tzinfo_factory = _compat_utc_tzinfo_factory
# DATE_FORMAT = 'N j, Y' # DATE_FORMAT = 'N j, Y'
DATE_FORMAT = 'd/m/Y' DATE_FORMAT = 'd/m/Y'
SHORT_DATE_FORMAT = 'd/m/Y' SHORT_DATE_FORMAT = 'd/m/Y'
@ -333,9 +401,6 @@ DAB_FIELD_RENDERER = \
CRISPY_TEMPLATE_PACK = 'bootstrap4' CRISPY_TEMPLATE_PACK = 'bootstrap4'
CRISPY_ALLOWED_TEMPLATE_PACKS = 'bootstrap4' CRISPY_ALLOWED_TEMPLATE_PACKS = 'bootstrap4'
CRISPY_FAIL_SILENTLY = not DEBUG CRISPY_FAIL_SILENTLY = not DEBUG
FLOPPY_FORMS_USE_GIS = False
FORM_RENDERER = 'django.forms.renderers.DjangoTemplates'
# suprime texto de ajuda default do django-filter # suprime texto de ajuda default do django-filter
FILTERS_HELP_TEXT_FILTER = False FILTERS_HELP_TEXT_FILTER = False
@ -387,12 +452,12 @@ LOGGING = {
}, },
'loggers': { 'loggers': {
'sapl': { 'sapl': {
'handlers': ['applogfile'] + ['console_verbose'] if LOGGING_CONSOLE_VERBOSE else [], 'handlers': ['applogfile'] + (['console_verbose'] if LOGGING_CONSOLE_VERBOSE else []),
'level': 'DEBUG' if LOGGING_CONSOLE_VERBOSE else 'INFO', 'level': 'DEBUG' if LOGGING_CONSOLE_VERBOSE else 'INFO',
'propagate': True, 'propagate': True,
}, },
'django': { 'django': {
'handlers': ['applogfile'] + ['console_verbose'] if LOGGING_CONSOLE_VERBOSE else [], 'handlers': ['applogfile'] + (['console_verbose'] if LOGGING_CONSOLE_VERBOSE else []),
'level': 'ERROR', 'level': 'ERROR',
'propagate': True, 'propagate': True,
}, },

13
sapl/static/sapl/frontend/css/chunk-vendors.045ec640.css

File diff suppressed because one or more lines are too long

BIN
sapl/static/sapl/frontend/css/chunk-vendors.045ec640.css.gz

Binary file not shown.

13
sapl/static/sapl/frontend/css/chunk-vendors.299c587b.css

File diff suppressed because one or more lines are too long

BIN
sapl/static/sapl/frontend/css/chunk-vendors.299c587b.css.gz

Binary file not shown.

BIN
sapl/static/sapl/frontend/css/compilacao.0baf3580.css.gz

Binary file not shown.

2
sapl/static/sapl/frontend/css/global.45591136.css → sapl/static/sapl/frontend/css/global.042f6737.css

File diff suppressed because one or more lines are too long

BIN
sapl/static/sapl/frontend/css/global.042f6737.css.gz

Binary file not shown.

BIN
sapl/static/sapl/frontend/css/global.45591136.css.gz

Binary file not shown.

BIN
sapl/static/sapl/frontend/css/painel.e2b9504e.css.gz

Binary file not shown.

BIN
sapl/static/sapl/frontend/fonts/fa-brands-400.5d18d427.ttf

Binary file not shown.

BIN
sapl/static/sapl/frontend/fonts/fa-brands-400.5d18d427.ttf.gz

Binary file not shown.

BIN
sapl/static/sapl/frontend/fonts/fa-brands-400.87587a68.woff2

Binary file not shown.

BIN
sapl/static/sapl/frontend/fonts/fa-brands-400.9a905705.ttf

Binary file not shown.

BIN
sapl/static/sapl/frontend/fonts/fa-brands-400.9a905705.ttf.gz

Binary file not shown.

BIN
sapl/static/sapl/frontend/fonts/fa-brands-400.b6033b54.woff2

Binary file not shown.

BIN
sapl/static/sapl/frontend/fonts/fa-regular-400.3580b4a9.woff2

Binary file not shown.

BIN
sapl/static/sapl/frontend/fonts/fa-regular-400.3ccdbd3d.woff2

Binary file not shown.

BIN
sapl/static/sapl/frontend/fonts/fa-regular-400.67a0fb74.ttf

Binary file not shown.

BIN
sapl/static/sapl/frontend/fonts/fa-regular-400.67a0fb74.ttf.gz

Binary file not shown.

BIN
sapl/static/sapl/frontend/fonts/fa-regular-400.81482cd4.ttf

Binary file not shown.

BIN
sapl/static/sapl/frontend/fonts/fa-regular-400.81482cd4.ttf.gz

Binary file not shown.

BIN
sapl/static/sapl/frontend/fonts/fa-solid-900.0b0cc8a6.woff2

Binary file not shown.

BIN
sapl/static/sapl/frontend/fonts/fa-solid-900.69d3141a.ttf

Binary file not shown.

BIN
sapl/static/sapl/frontend/fonts/fa-solid-900.69d3141a.ttf.gz

Binary file not shown.

BIN
sapl/static/sapl/frontend/fonts/fa-solid-900.6a8db53d.ttf

Binary file not shown.

BIN
sapl/static/sapl/frontend/fonts/fa-solid-900.6a8db53d.ttf.gz

Binary file not shown.

BIN
sapl/static/sapl/frontend/fonts/fa-solid-900.fd0b155c.woff2

Binary file not shown.

BIN
sapl/static/sapl/frontend/fonts/fa-v4compatibility.2c070fd2.ttf

Binary file not shown.

BIN
sapl/static/sapl/frontend/fonts/fa-v4compatibility.2c070fd2.ttf.gz

Binary file not shown.

BIN
sapl/static/sapl/frontend/fonts/fa-v4compatibility.e4efb16c.ttf

Binary file not shown.

BIN
sapl/static/sapl/frontend/fonts/fa-v4compatibility.e4efb16c.ttf.gz

Binary file not shown.

BIN
sapl/static/sapl/frontend/img/down_arrow_select.jpg.gz

Binary file not shown.

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save