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. 24
      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. 17
      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. 32
      sapl/base/forms.py
  30. 18
      sapl/base/migrations/0060_auto_20240812_1628.py
  31. 2
      sapl/base/models.py
  32. 16
      sapl/base/receivers.py
  33. 2
      sapl/base/search_indexes.py
  34. 7
      sapl/base/templatetags/common_tags.py
  35. 10
      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. 64
      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. 39
      sapl/materia/models.py
  48. 2
      sapl/materia/urls.py
  49. 36
      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. 33
      sapl/norma/models.py
  54. 17
      sapl/norma/views.py
  55. 5
      sapl/parlamentares/forms.py
  56. 1
      sapl/parlamentares/models.py
  57. 26
      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. 6
      sapl/relatorios/urls.py
  64. 102
      sapl/relatorios/views.py
  65. 2
      sapl/rules/group_geral.py
  66. 11
      sapl/sessao/models.py
  67. 106
      sapl/sessao/views.py
  68. 95
      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
=========================

176
docker/Dockerfile

@ -1,73 +1,123 @@
FROM python:3.9-slim-buster
# Setup env
ENV LANG C.UTF-8
ENV LC_ALL C.UTF-8
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED=1
ENV DEBIAN_FRONTEND noninteractive
ENV BUILD_PACKAGES apt-utils apt-file libpq-dev graphviz-dev build-essential git pkg-config \
python3-dev libxml2-dev libjpeg-dev libssl-dev libffi-dev libxslt1-dev \
libcairo2-dev software-properties-common python3-setuptools python3-pip
## NAO EH PRA TIRAR O vim DA LISTA DE COMANDOS INSTALADOS!!!
ENV RUN_PACKAGES graphviz python3-lxml python3-magic postgresql-client python3-psycopg2 \
poppler-utils curl jq bash vim python3-venv tzdata nodejs \
fontconfig ttf-dejavu python nginx
RUN mkdir -p /var/interlegis/sapl
# ---------- 1) BUILDER ----------
FROM python:3.12-slim-bookworm AS builder
ENV LANG=C.UTF-8 LC_ALL=C.UTF-8 PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 \
DEBIAN_FRONTEND=noninteractive \
VENV_DIR=/opt/venv \
PIP_NO_CACHE_DIR=on
# Dev headers e toolchain só no builder
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential git pkg-config \
libpq-dev libxml2-dev libjpeg-dev libssl-dev libffi-dev libxslt1-dev \
libcairo2-dev libpango1.0-dev libgdk-pixbuf-2.0-dev libharfbuzz-dev \
libfreetype6-dev zlib1g-dev \
&& rm -rf /var/lib/apt/lists/*
# Venv independente do sistema
RUN python -m venv "${VENV_DIR}" \
&& "${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/
ADD . /var/interlegis/sapl/
RUN apt-get update && \
apt-get upgrade -y && \
apt-get install -y --no-install-recommends $BUILD_PACKAGES $RUN_PACKAGES && \
fc-cache -fv && \
pip3 install --no-cache-dir --upgrade pip setuptools && \
rm -f /etc/nginx/conf.d/* && \
pip install --no-cache-dir -r /var/interlegis/sapl/requirements/dev-requirements.txt --upgrade setuptools && \
SUDO_FORCE_REMOVE=yes apt-get purge -y --auto-remove $BUILD_PACKAGES && \
apt-get autoremove && apt-get clean && rm -rf /var/lib/apt/lists/*
WORKDIR /var/interlegis/sapl/
ADD . /var/interlegis/sapl/
COPY docker/start.sh $HOME
COPY docker/solr_cli.py $HOME
COPY docker/wait-for-pg.sh $HOME
COPY docker/wait-for-solr.sh $HOME
COPY docker/create_admin.py $HOME
COPY docker/genkey.py $HOME
COPY docker/gunicorn_start.sh $HOME
COPY docker/config/nginx/sapl.conf /etc/nginx/conf.d
COPY docker/config/nginx/nginx.conf /etc/nginx/nginx.conf
# Traga o venv pré-instalado
COPY --from=builder ${VENV_DIR} ${VENV_DIR}
# Código da aplicação (depois do venv para aproveitar cache)
COPY . /var/interlegis/sapl/
# Nginx (somente se instalado)
RUN if [ "$WITH_NGINX" = "1" ]; then \
rm -f /etc/nginx/conf.d/*; \
cp docker/config/nginx/sapl.conf /etc/nginx/conf.d/sapl.conf; \
cp docker/config/nginx/nginx.conf /etc/nginx/nginx.conf; \
fi
# Scripts + gunicorn.conf no diretório da app
RUN install -m 755 docker/startup_scripts/start.sh /var/interlegis/sapl/start.sh \
&& install -m 755 docker/startup_scripts/wait-for-pg.sh /var/interlegis/sapl/wait-for-pg.sh \
&& install -m 755 docker/startup_scripts/wait-for-solr.sh /var/interlegis/sapl/wait-for-solr.sh \
&& install -m 644 docker/startup_scripts/solr_cli.py /var/interlegis/sapl/solr_cli.py \
&& install -m 644 docker/startup_scripts/create_admin.py /var/interlegis/sapl/create_admin.py \
&& install -m 644 docker/startup_scripts/genkey.py /var/interlegis/sapl/genkey.py \
&& install -m 644 docker/startup_scripts/gunicorn.conf.py /var/interlegis/sapl/gunicorn.conf.py
# (Se possível, evite copiar .env no build. Use secrets/variáveis em runtime.)
COPY docker/config/env_dockerfile /var/interlegis/sapl/sapl/.env
RUN python3 manage.py collectstatic --noinput --clear
# Remove .env(fake) e sapl.db da imagem
RUN rm -rf /var/interlegis/sapl/sapl/.env && \
rm -rf /var/interlegis/sapl/sapl.db
RUN chmod +x /var/interlegis/sapl/start.sh && \
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
# Logs (só se nginx estiver presente)
RUN if [ "$WITH_NGINX" = "1" ]; then \
ln -sf /dev/stdout /var/log/nginx/access.log; \
ln -sf /dev/stderr /var/log/nginx/error.log; \
fi \
&& mkdir -p /var/log/sapl/ \
&& ln -sf /var/interlegis/sapl/sapl.log /var/log/sapl/sapl.log
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"]

2
docker/config/nginx/nginx.conf

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

17
docker/docker-compose.yaml

@ -1,4 +1,3 @@
version: "3.7"
services:
sapldb:
image: postgres:10.5-alpine
@ -10,7 +9,9 @@ services:
POSTGRES_PASSWORD: sapl
POSTGRES_USER: sapl
POSTGRES_DB: sapl
PGDATA : /var/lib/postgresql/data/
PGDATA: /var/lib/postgresql/data/
TZ: UTC
PG_TZ: UTC
volumes:
- sapldb_data:/var/lib/postgresql/data/
ports:
@ -32,10 +33,10 @@ services:
networks:
- sapl-net
sapl:
image: interlegis/sapl:3.1.163-RC20
# build:
# context: ../
# dockerfile: ./docker/Dockerfile
# image: interlegis/sapl:3.1.164-RC1
build:
context: ../
dockerfile: ./docker/Dockerfile
container_name: sapl
labels:
NAME: "sapl"
@ -52,7 +53,9 @@ services:
EMAIL_HOST_PASSWORD: senhasmtp
USE_SOLR: 'True'
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'
ENABLE_SAPN: 'False'
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.)
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.captureWarnings(True)
logger = logging.getLogger(__name__)
SECURITY_FILE_TEMPLATE = """
{
@ -49,6 +51,7 @@ def solr_hash_password(password: str, salt: str = None):
salt (optional): base64 salt string
returns: sha256 hash of password and salt (both base64 strings)
"""
logger.debug("Generating Solr password")
m = sha256()
if salt is None:
salt = secrets.token_bytes(32)
@ -67,32 +70,32 @@ def solr_hash_password(password: str, salt: str = None):
def create_security_file(username, password):
print("Creating security.json file...")
logger.info("Creating security.json file...")
with open("security.json", "w") as f:
cypher, salt = solr_hash_password(password)
f.write(SECURITY_FILE_TEMPLATE % (username, cypher, salt, username))
print("file created!")
logger.info("file created!")
def upload_security_file(zk_host):
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:
with open('security.json', 'r') as f:
data = f.read()
zk = KazooClient(hosts=f"{zk_host}:{zk_port}")
zk.start()
print("Uploading security.json file...")
logger.info("Uploading security.json file...")
if zk.exists('/security.json'):
zk.set("/security.json", str.encode(data))
else:
zk.create("/security.json", str.encode(data))
data, stat = zk.get('/security.json')
print("file uploaded!")
print(data.decode('utf-8'))
logger.info("file uploaded!")
logger.info(data.decode('utf-8'))
zk.stop()
except Exception as e:
print(e)
logger.error(e)
sys.exit(-1)
@ -250,6 +253,7 @@ def setup_embedded_zk(solr_url):
_, solr_user, solr_pwd, solr_host, solr_port = match.groups()
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)
upload_security_file(solr_host)
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

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

@ -1,4 +1,3 @@
const _$ = window.$
window.DispositivoEdit = function () {
@ -500,7 +499,11 @@ window.DispositivoEdit = function () {
}
instance.reloadFunctionsDraggables = function () {
_$('.dpt-alts').sortable({
const dptAlts = _$('.dpt-alts')
if (dptAlts.length > 0) {
dptAlts.sortable({
connectWith: '.dpt-alts',
items: '.sorting-initialize',
revert: true,
distance: 15,
start: function (event, ui) {
@ -511,11 +514,14 @@ window.DispositivoEdit = function () {
const url = pk + '/refresh?action=json_drag_move_dpt_alterado&index=' + ui.item.index() + '&bloco_pk=' + bloco_pk
_$.get(url).done(function (data) {
// console.log(pk + ' - ' + bloco_pk)
// reloadFunctionsForObjectsOfCompilacao();
// handle data if needed
})
}
})
dptAlts.find('.dpt').one('mouseenter', function () {
$(this).addClass('sorting-initialize')
dptAlts.sortable('refresh')
})
_$('.dpt-alts .dpt').draggable({
connectToSortable: '.dpt-alts',
@ -523,7 +529,6 @@ window.DispositivoEdit = function () {
zIndex: 1,
distance: 15,
drag: function (event, ui) {
// _$('.dpt-comp-selected').removeClass('dpt-comp-selected');
_$('.dpt-alts').addClass('drag')
},
stop: function (event, ui) {
@ -531,7 +536,10 @@ window.DispositivoEdit = function () {
}
})
_$('.dpt-alts').disableSelection()
dptAlts.disableSelection()
} else {
console.warn("No '.dpt-alts' elements found to make sortable/draggable.")
}
}
instance.scrollTo = function (dpt) {
try {
@ -570,7 +578,9 @@ window.DispositivoEdit = function () {
instance.triggerBtnDptEdit(href[1])
}
_$('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()
}

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

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

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

@ -187,4 +187,7 @@ small {
a[href]:after {
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": {
"@fortawesome/fontawesome-free": "^6.1.2",
"axios": "^0.27.2",
"axios": "^1.7.2",
"bootstrap": "^4.6.2",
"bootstrap-vue": "^2.22.0",
"diff": "^5.1.0",
@ -21,7 +21,7 @@
"moment": "^2.29.4",
"moment-locales-webpack-plugin": "^1.2.0",
"popper.js": "^1.16.1",
"tinymce": "^6.1.2",
"tinymce": "^7.2.0",
"vue": "^2.7.9"
},
"devDependencies": {

17
release.sh

@ -5,7 +5,6 @@
##
## 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)
@ -44,14 +43,19 @@ function change_files {
echo "Updating from "$OLD_VERSION" to "$FINAL_VERSION""
if [[ "$OSTYPE" == "darwin"* ]]; then
# MacOS (BSD sed)
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/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 {
@ -72,9 +76,6 @@ function set_rc_version {
fi
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

16
requirements/dev-requirements.txt

@ -1,9 +1,11 @@
-r test-requirements.txt
autopep8==1.2.4
beautifulsoup4==4.9.1
django-debug-toolbar==1.11.1
ipdb==0.13.3
pdbpp==0.9.2
pip-review==0.4
pipdeptree==0.10.1
autopep8==2.3.2
beautifulsoup4==4.13.5
django-debug-toolbar==3.2.4
ipdb==0.13.13
fancycompleter==0.11.1
pdbpp==0.11.7
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-filter==2.4.0
djangorestframework==3.12.4
dj-database-url==0.5.0
django-braces==1.14.0
django-crispy-forms==1.7.2
django-contrib-postgres==0.0.1
django-floppyforms==1.8.0
django-extra-views==0.12.0
django-model-utils==3.1.2
django-extensions==2.1.4
@ -17,14 +15,18 @@ drf-spectacular==0.18.2
django-ratelimit==3.0.1
easy-thumbnails==2.8.5
python-decouple==3.1
psycopg2-binary==2.8.6
dj-database-url==0.5.0
psycopg2-binary==2.9.9
pyyaml==6.0.1
pytz==2019.3
python-magic==0.4.15
unipath==1.1
WeasyPrint==51
Pillow==10.0.1
gunicorn==19.9.0
Pillow==10.3.0
rlPyCairo==0.3.0
reportlab==4.2.0
WeasyPrint==66
trml2pdf==0.6
gunicorn==23.0.0
more-itertools==8.2.0
pysolr==3.6.0
PyPDF4==1.27.0
@ -36,6 +38,6 @@ kazoo==2.8.0
django-prometheus==2.2.0
asn1crypto==1.5.1
XlsxWriter==3.2.0
git+https://github.com/interlegis/trml2pdf
git+https://github.com/interlegis/django-admin-bootstrapped

19
requirements/test-requirements.txt

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

17
sapl/api/views_comissoes.py

@ -3,6 +3,10 @@ from django.apps.registry import apps
from drfautoapi.drfautoapi import ApiViewSetConstrutor, \
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(
@ -10,3 +14,16 @@ ApiViewSetConstrutor.build_class(
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
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)
email = tpl.render(context)
if t.endswith(".html"):
email = email.replace('\n', '').replace('\r', '')
email = email.replace('\\n', '').replace('\r', '')
emails.append(email)
return emails

32
sapl/base/forms.py

@ -40,7 +40,7 @@ from sapl.utils import (autor_label, autor_modal, ChoiceWithoutValidationField,
FilterOverridesMetaMixin, FileFieldCheckMixin,
ImageThumbnailFileInput, qs_override_django_filter,
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
@ -288,8 +288,13 @@ class UserAdminForm(ModelForm):
)
else:
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(
new_password2, self.instance)
new_password1, self.instance)
parlamentar = data.get('parlamentar', None)
if parlamentar and parlamentar.votante_set.exists() and \
@ -916,22 +921,26 @@ class CasaLegislativaForm(FileFieldCheckMixin, ModelForm):
# chama __clean de FileFieldCheckMixin
# por estar em clean de campo
super(CasaLegislativaForm, self)._check()
logotipo = self.cleaned_data.get('logotipo')
if logotipo:
if logotipo.size > MAX_IMAGE_UPLOAD_SIZE:
if logotipo and logotipo.size > MAX_IMAGE_UPLOAD_SIZE:
raise ValidationError("Imagem muito grande. ( > 2MB )")
return logotipo
def save(self, commit=True):
casa = super(CasaLegislativaForm, self).save(commit=commit)
delete_cached_entry("site-title")
return casa
class LoginForm(AuthenticationForm):
username = forms.CharField(
label="Username", max_length=30,
label="Usuário", max_length=30,
widget=forms.TextInput(
attrs={
'class': 'form-control', 'name': 'username'}))
password = forms.CharField(
label="Password", max_length=30,
label="Senha", max_length=30,
widget=forms.PasswordInput(
attrs={
'class': 'form-control', 'name': 'password'}))
@ -1139,12 +1148,15 @@ class AlterarSenhaForm(Form):
# TODO: caracteres alfanuméricos, maiúsculas (?),
# TODO: senha atual igual a senha anterior, etc
if len(new_password1) < 6:
if is_weak_password(new_password1):
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(
"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']
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'))
sigla = models.CharField(max_length=100, verbose_name=_('Sigla'))
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'))
uf = models.CharField(max_length=2,
choices=LISTA_DE_UFS,

16
sapl/base/receivers.py

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

2
sapl/base/search_indexes.py

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

7
sapl/base/templatetags/common_tags.py

@ -84,6 +84,9 @@ def desc_operation(value):
@register.filter
def format_user(user):
if not user:
return ""
if user.first_name:
return user.first_name + " " + user.last_name + " (" + user.username + ")"
else:
@ -297,7 +300,7 @@ def youtube_url(value):
# Test if YouTube video
# tested on https://pythex.org/
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)
return True if r else False
@ -305,7 +308,7 @@ def youtube_url(value):
@register.filter
def facebook_url(value):
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)
return True if r else False

10
sapl/base/templatetags/menus.py

@ -183,17 +183,21 @@ def resolve_urls_inplace(menu, pk, rm, context):
as funcionalidades diretas do MasterDetailCrud, como:
- visualização de detalhes, adição, edição, remoção.
"""
try:
if 'view' in context:
view = context['view']
if hasattr(view, '__class__') and\
hasattr(view.__class__, 'crud'):
urls = view.__class__.crud.get_urls()
if hasattr(view, 'crud'):
urls = view.crud.get_urls()
for u in urls:
if (u.name == url_name or
'urls_extras' in menu and
u.name in menu['urls_extras']):
menu['active'] = 'active'
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[
'request'].user.has_perm(menu['check_permission']):
menu['active'] = ''

21
sapl/base/views.py

@ -6,7 +6,7 @@ import os
from django.apps.registry import apps
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.models import Group
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.utils import (gerar_hash_arquivo, intervalos_tem_intersecao, mail_service_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 .models import AppConfig, CasaLegislativa
@ -75,6 +75,21 @@ class LoginSapl(views.LoginView):
template_name = 'base/login.html'
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):
template_name = "email/confirma.html"
@ -1481,6 +1496,8 @@ class AlterarSenha(FormView):
user.set_password(new_password)
user.save()
self.request.session.pop('weak_password', None)
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.tipo_publicacao,
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]
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') % {
'tipo': self.tipo_ta,
'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):
from django.core import serializers
@ -390,7 +390,6 @@ class TextoArticulado(TimestampedMixin):
@classonlymethod
def update_or_create(cls, view_integracao, obj):
map_fields = view_integracao.map_fields
ta_values = getattr(view_integracao, 'ta_values', {})
@ -944,7 +943,7 @@ class Publicacao(TimestampedMixin):
def __str__(self):
return _('%s realizada em %s \n <small>%s</small>') % (
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)
@ -1182,6 +1181,24 @@ class Dispositivo(BaseModel, TimestampedMixin):
help_text=_('O recorte de imagem '
'é 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:
verbose_name = _('Dispositivo')
verbose_name_plural = _('Dispositivos')

2
sapl/compilacao/urls.py

@ -23,7 +23,7 @@ urlpatterns_compilacao = [
url(r'^(?P<ta_id>[0-9]+)/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'),
url(r'^(?P<ta_id>[0-9]+)/text/edit',

22
sapl/context_processors.py

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

64
sapl/crud/base.py

@ -263,18 +263,18 @@ class CrudBaseMixin(CrispyLayoutFormMixin):
self.model_name_set = getattr(
obj.model, obj.model_set).field.model._meta.model_name
if hasattr(self, 'permission_required') and self.permission_required:
if hasattr(obj, 'public'):
self.permission_required = list(
set(self.permission_required) - set(obj.public))
else:
if not hasattr(obj, 'public'):
obj.public = []
self.permission_required = tuple((
self.permission(pr) for pr in self.permission_required))
if hasattr(self, 'permission_required') and self.permission_required:
else:
obj.public = []
self.permission_required = tuple(
(
self.permission(pr) for pr in (
set(self.permission_required) - set(obj.public)
)
)
)
@classmethod
def url_name(cls, suffix):
@ -460,7 +460,8 @@ class CrudListView(PermissionRequiredContainerCrudMixin, ListView):
# URL padrão para primeira coluna da listagem
url = self.resolve_url(
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:
url = getattr(self, func)(obj, name)[0]
@ -947,6 +948,8 @@ class CrudDeleteView(PermissionRequiredContainerCrudMixin,
class Crud:
__abstract__ = True
BaseMixin = CrudBaseMixin
ListView = CrudListView
CreateView = CrudCreateView
@ -955,16 +958,32 @@ class Crud:
DeleteView = CrudDeleteView
help_topic = ''
class PublicMixin:
permission_required = []
@classonlymethod
def get_urls(cls):
def _add_base(view):
if view:
if not view:
return
if not cls.__abstract__:
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
@ -978,6 +997,21 @@ class Crud:
CrudUpdateView = _add_base(cls.UpdateView)
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 = [
(CrudListView.get_url_regex()
if CrudListView else None, CrudListView, ACTION_LIST),
@ -1537,7 +1571,7 @@ class MasterDetailCrud(Crud):
class CrudBaseForListAndDetailExternalAppView(MasterDetailCrud):
CreateView, UpdateView, DeleteView = None, None, None
class BaseMixin(Crud.PublicMixin, MasterDetailCrud.BaseMixin):
class BaseMixin(MasterDetailCrud.BaseMixin):
def resolve_url(self, suffix, args=None):
obj = self.crud if hasattr(self, 'crud') else self

2
sapl/lexml/forms.py

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

2
sapl/lexml/models.py

@ -25,7 +25,7 @@ class LexmlProvedor(models.Model): # LexmlRegistroProvedor
def pretty_xml(self):
import html
safe_xml = html.escape(self.xml)
return safe_xml.replace('\n', '<br/>').replace(' ', '&nbsp;')
return safe_xml.replace('\\n', '<br/>').replace(' ', '&nbsp;')
class Meta:
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,
SEPARADOR_HASH_PROPOSICAO,
validar_arquivo, YES_NO_CHOICES,
GoogleRecapthaMixin)
GoogleRecapthaMixin, get_client_ip)
from .models import (AcompanhamentoMateria, Anexada, Autoria,
DespachoInicial, DocumentoAcessorio, Numeracao,
@ -1039,6 +1039,7 @@ class MateriaLegislativaFilterSet(django_filters.FilterSet):
'ano_origem_externa',
'data_origem_externa',
'local_origem_externa',
'regime_tramitacao',
]
def filter_ementa(self, queryset, name, value):
@ -1099,7 +1100,7 @@ class MateriaLegislativaFilterSet(django_filters.FilterSet):
('tramitacao__status', 6),
])
row9 = to_row(
[('materiaassunto__assunto', 6), ('indexacao', 6)])
[('materiaassunto__assunto', 4), ('indexacao', 4), ('regime_tramitacao', 4)])
row8 = to_row(
[
@ -1715,7 +1716,7 @@ class TramitacaoEmLoteForm(ModelForm):
('texto', 12)
])
documentos_checkbox_HTML = '''
documentos_checkbox_HTML = r'''
<br\><br\><br\>
<fieldset>
<legend style="font-size: 24px;">Selecione as matérias para tramitação:</legend>
@ -2653,7 +2654,9 @@ class ConfirmarProposicaoForm(ProposicaoForm):
protocolo.save()
HistoricoProposicao.objects.create(proposicao=proposicao,
status='E')
status='E',
user=self.initial['user'],
ip=self.initial['ip'])
self.instance.results['messages']['success'].append(_(
'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
"""),
]

39
sapl/materia/models.py

@ -1,3 +1,4 @@
from datetime import datetime
from django.contrib.auth.models import Group
from django.contrib.contenttypes.fields import GenericRelation
@ -14,13 +15,12 @@ from sapl.comissoes.models import Comissao, Reuniao
from sapl.compilacao.models import (PerfilEstruturalTextoArticulado,
TextoArticulado)
from sapl.parlamentares.models import Parlamentar
#from sapl.protocoloadm.models import Protocolo
from sapl.utils import (RANGE_ANOS, YES_NO_CHOICES, SaplGenericForeignKey,
SaplGenericRelation, restringe_tipos_de_arquivo_txt,
texto_upload_path, get_settings_auth_user_model,
OverwriteStorage)
# from sapl.protocoloadm.models import Protocolo
EM_TRAMITACAO = [(1, 'Sim'),
(0, 'Não')]
@ -183,7 +183,6 @@ def anexo_upload_path(instance, filename):
class MateriaLegislativa(models.Model):
tipo = models.ForeignKey(
TipoMateriaLegislativa,
on_delete=models.PROTECT,
@ -280,7 +279,7 @@ class MateriaLegislativa(models.Model):
Autor,
through='Autoria',
through_fields=('materia', 'autor'),
symmetrical=False,)
symmetrical=False, )
data_ultima_atualizacao = models.DateTimeField(
blank=True, null=True,
@ -323,7 +322,7 @@ class MateriaLegislativa(models.Model):
'numero': self.numero,
'data': defaultfilters.date(
self.data_apresentacao,
"d \d\e F \d\e Y"
r"d \d\e F \d\e Y"
)}
def data_entrada_protocolo(self):
@ -340,7 +339,13 @@ class MateriaLegislativa(models.Model):
if protocolo.timestamp:
return protocolo.timestamp
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:
return protocolo.data
@ -392,7 +397,7 @@ class Autoria(models.Model):
class Meta:
verbose_name = _('Autoria')
verbose_name_plural = _('Autorias')
unique_together = (('autor', 'materia'), )
unique_together = (('autor', 'materia'),)
ordering = ('-primeiro_autor', 'autor__nome')
def __str__(self):
@ -747,7 +752,6 @@ class Parecer(models.Model):
class Proposicao(models.Model):
autor = models.ForeignKey(
Autor,
null=True,
@ -970,13 +974,13 @@ class Proposicao(models.Model):
return '%s nº _____ %s' % (
self.tipo, formats.date_format(
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:
ordering = ['-data_recebimento']
verbose_name = _('Proposição')
verbose_name_plural = _('Proposições')
unique_together = (('content_type', 'object_id'), )
unique_together = (('content_type', 'object_id'),)
permissions = (
('detail_proposicao_enviada',
_('Pode acessar detalhes de uma proposição enviada.')),
@ -1008,7 +1012,7 @@ class Proposicao(models.Model):
'numero': self.numero_proposicao,
'data': defaultfilters.date(
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):
@ -1022,7 +1026,8 @@ class Proposicao(models.Model):
def save(self, force_insert=False, force_update=False, using=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:
self.usuario_envio = self.user
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)
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:
managed = False
db_table = "materia_materiaemtramitacao"

2
sapl/materia/urls.py

@ -143,7 +143,7 @@ urlpatterns_proposicao = [
url(r'^proposicao/devolvida/', ProposicaoDevolvida.as_view(),
name='proposicao-devolvida'),
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'),
url(r'^sistema/proposicao/tipo/',
include(TipoProposicaoCrud.get_urls())),

36
sapl/materia/views.py

@ -24,6 +24,7 @@ from django.shortcuts import render
from django.template import loader
from django.urls import reverse
from django.utils import formats, timezone
from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _
from django.views.generic import CreateView, ListView, TemplateView, UpdateView
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,
mail_service_configured, montar_row_autor, SEPARADOR_HASH_PROPOSICAO,
show_results_filter_set, get_tempfile_dir,
google_recaptcha_configured)
google_recaptcha_configured, MultiFormatOutputMixin)
from .forms import (AcessorioEmLoteFilterSet, AcompanhamentoMateriaForm,
AnexadaEmLoteFilterSet, AdicionarVariasAutoriasFilterSet,
@ -2039,11 +2040,34 @@ class AcompanhamentoExcluirView(TemplateView):
return HttpResponseRedirect(self.get_success_url())
class MateriaLegislativaPesquisaView(FilterView):
class MateriaLegislativaPesquisaView(MultiFormatOutputMixin, FilterView):
model = MateriaLegislativa
filterset_class = MateriaLegislativaFilterSet
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):
super().get_filterset_kwargs(filterset_class)
@ -2078,6 +2102,9 @@ class MateriaLegislativaPesquisaView(FilterView):
"anexadas",
"tipo",
"texto_articulado",
"relatoria_set",
"relatoria_set__comissao",
"relatoria_set__parlamentar",
"tramitacao_set",
"tramitacao_set__status",
"tramitacao_set__unidade_tramitacao_local",
@ -2099,7 +2126,8 @@ class MateriaLegislativaPesquisaView(FilterView):
qs = qs.filter(materiaassunto__isnull=True)
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)
@ -2964,7 +2992,7 @@ def create_pdf_docacessorios(materia):
materia.pk,
time.mktime(datetime.now().timetuple()))
merger = PdfFileMerger()
merger = PdfFileMerger(strict=False)
for f in docs_path:
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 re
from crispy_forms.layout import (Button, Fieldset, HTML, Layout)
from django import forms
from django.contrib.postgres.search import SearchVector
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.utils import timezone
from django.utils.translation import ugettext_lazy as _
@ -69,6 +70,10 @@ class NormaFilterSet(django_filters.FilterSet):
label='Ano',
choices=choice_anos_com_normas)
numero = django_filters.CharFilter(
method='filter_numero',
label=_('Número'))
ementa = django_filters.CharFilter(
method='filter_ementa',
label=_('Pesquisar expressões na ementa da norma'))
@ -129,6 +134,19 @@ class NormaFilterSet(django_filters.FilterSet):
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):
return queryset.annotate(search=SearchVector('ementa',
config='portuguese')).filter(search=value)
@ -206,7 +224,7 @@ class NormaJuridicaForm(FileFieldCheckMixin, ModelForm):
return cleaned_data
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:
self.logger.error("Número de norma ({}) não pode conter somente letras.".format(
cleaned_data['numero']))
@ -264,7 +282,7 @@ class NormaJuridicaForm(FileFieldCheckMixin, ModelForm):
texto_integral = self.cleaned_data.get('texto_integral', False)
if texto_integral:
validar_arquivo(texto_integral, "Texto Integral")
validar_arquivo(texto_integral, "Texto Original")
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'),
),
]

33
sapl/norma/models.py

@ -138,7 +138,7 @@ class NormaJuridica(models.Model):
blank=True,
null=True,
upload_to=norma_upload_path,
verbose_name=_('Texto Integral'),
verbose_name=_('Texto Original'),
storage=OverwriteStorage(),
validators=[restringe_tipos_de_arquivo_txt])
tipo = models.ForeignKey(
@ -231,11 +231,21 @@ class NormaJuridica(models.Model):
ordering = ['-data', '-numero']
def get_normas_relacionadas(self):
principais = NormaRelacionada.objects.filter(
norma_principal=self.id).order_by('norma_principal__data',
principais = NormaRelacionada.objects.\
select_related('tipo_vinculo',
'norma_principal',
'norma_relacionada',
'norma_principal__tipo',
'norma_relacionada__tipo').\
filter(norma_principal=self.id).order_by('norma_principal__data',
'norma_relacionada__data')
relacionadas = NormaRelacionada.objects.filter(
norma_relacionada=self.id).order_by('norma_principal__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)
@ -253,12 +263,23 @@ class NormaJuridica(models.Model):
'tipo': self.tipo,
'orgao_sigla': f'-{self.orgao.sigla}' if self.orgao else '',
'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
def epigrafe(self):
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):
texto_integral = self.texto_integral
result = super().delete(using=using, keep_parents=keep_parents)

17
sapl/norma/views.py

@ -28,7 +28,7 @@ from sapl.crud.base import (RP_DETAIL, RP_LIST, Crud, CrudAux,
MasterDetailCrud, make_pagination)
from sapl.materia.models import Orgao
from sapl.utils import show_results_filter_set, get_client_ip,\
sapn_is_enabled
sapn_is_enabled, MultiFormatOutputMixin
from .forms import (AnexoNormaJuridicaForm, NormaFilterSet, NormaJuridicaForm,
NormaPesquisaSimplesForm, NormaRelacionadaForm,
@ -147,11 +147,22 @@ class NormaRelacionadaCrud(MasterDetailCrud):
layout_key = 'NormaRelacionadaDetail'
class NormaPesquisaView(FilterView):
class NormaPesquisaView(MultiFormatOutputMixin, FilterView):
model = NormaJuridica
filterset_class = NormaFilterSet
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):
qs = super().get_queryset()
@ -478,7 +489,7 @@ def recuperar_numero_norma(request):
norma = NormaJuridica.objects.filter(**param).order_by(
'tipo', 'ano', 'numero').values_list('numero', flat=True)
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
response = JsonResponse({'numero': next_num,
'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.translation import ugettext_lazy as _
import django_filters
from floppyforms.widgets import ClearableFileInput
from image_cropping.widgets import CropWidget, ImageCropWidget
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)
class ImageThumbnailFileInput(ClearableFileInput):
template_name = 'floppyforms/image_thumbnail.html'
class CustomImageCropWidget(ImageCropWidget):
"""
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 image_cropping.fields import ImageCropField, ImageRatioField
from model_utils import Choices
from prompt_toolkit.key_binding.bindings.named_commands import self_insert
from sapl.base.models import Autor
from sapl.decorators import vigencia_atual

26
sapl/parlamentares/urls.py

@ -37,6 +37,8 @@ urlpatterns = [
ProposicaoParlamentarCrud.get_urls() +
RelatoriaParlamentarCrud.get_urls() +
VotanteView.get_urls()
)),
url(r'^parlamentar/pesquisar-parlamentar/',
@ -48,16 +50,21 @@ urlpatterns = [
url(r'^parlamentar/(?P<pk>\d+)/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/$',
VincularParlamentarView.as_view(), name='vincular_parlamentar'),
url(r'^parlamentar/coligacao-legislatura/', coligacao_legislatura, name="coligacao_legislatura"),
url(r'^sistema/coligacao/', include(ColigacaoCrud.get_urls() + ComposicaoColigacaoCrud.get_urls())),
url(r'^sistema/pesquisar-coligacao/', PesquisarColigacaoView.as_view(), name='pesquisar_coligacao'),
url(r'^parlamentar/coligacao-legislatura/',
coligacao_legislatura, name="coligacao_legislatura"),
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-cargo/', include(BlocoCargoCrud.get_urls())),
@ -65,7 +72,8 @@ urlpatterns = [
url(r'^sistema/frente/', include(FrenteCrud.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',
frente_atualiza_lista_parlamentares,
@ -86,8 +94,10 @@ urlpatterns = [
include(TipoMilitarCrud.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/partido/(?P<pk>\d+)/filiados$', parlamentares_filiados, name='parlamentares_filiados'),
url(r'^sistema/parlamentar/pesquisar-partido/',
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/',
include(SessaoLegislativaCrud.get_urls())),

12
sapl/parlamentares/views.py

@ -113,12 +113,13 @@ class VotanteView(MasterDetailCrud):
class FrenteList(MasterDetailCrud):
public = [RP_DETAIL, RP_LIST]
model = Frente
is_m2m = True
parent_field = 'parlamentares'
CreateView, UpdateView, DeleteView = None, None, None
class BaseMixin(Crud.PublicMixin, MasterDetailCrud.BaseMixin):
class BaseMixin(MasterDetailCrud.BaseMixin):
list_field_names = ['nome', 'data_criacao', 'data_extincao']
@classmethod
@ -127,6 +128,7 @@ class FrenteList(MasterDetailCrud):
class RelatoriaParlamentarCrud(CrudBaseForListAndDetailExternalAppView):
public = [RP_DETAIL, RP_LIST]
model = Relatoria
parent_field = 'parlamentar'
help_topic = 'tramitacao_relatoria'
@ -355,6 +357,7 @@ class PesquisarPartidoView(FilterView):
class ParticipacaoParlamentarCrud(CrudBaseForListAndDetailExternalAppView):
public = [RP_DETAIL, RP_LIST]
model = Participacao
parent_field = 'parlamentar'
namespace = AppConfig.name
@ -589,6 +592,7 @@ def get_parlamentar_frentes(request, pk):
context = {
'subnav_template_name': 'parlamentares/subnav.yaml',
'root_pk': pk,
'sexo_parlamentar': Parlamentar.objects.get(id=pk).sexo,
'nome_parlamentar': Parlamentar.objects.get(id=pk).nome_parlamentar,
'frentes': frentes,
'num_frentes': len(frentes)
@ -1388,7 +1392,8 @@ def altera_field_mesa_public_view(request):
partido_parlamentar_sessao_legislativa(sessao, parlamentar))
if parlamentar.fotografia:
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(
parlamentar.fotografia,
{
@ -1398,7 +1403,8 @@ def altera_field_mesa_public_view(request):
'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)
except Exception as e:
logger.error(e)

4
sapl/protocoloadm/forms.py

@ -1110,7 +1110,7 @@ class DocumentoAdministrativoForm(FileFieldCheckMixin, ModelForm):
numero_protocolo = self.data['numero_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'])
tipo_documento = int(self.data['tipo'])
ano_documento = int(self.data['ano'])
@ -1558,7 +1558,7 @@ class TramitacaoEmLoteAdmForm(ModelForm):
('texto', 12)
])
documentos_checkbox_HTML = '''
documentos_checkbox_HTML = r'''
<br\><br\><br\>
<fieldset>
<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,
get_mime_type_from_file_extension, lista_anexados,
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,
AnularProtocoloAdmForm, compara_tramitacoes_doc,
@ -174,7 +174,7 @@ def create_pdf_docacessorios(docadministrativo):
logger.info("Gerando compilado PDF de documentos acessorios com {} documentos"
.format(docs_path))
merger = PdfFileMerger()
merger = PdfFileMerger(strict=False)
for f in docs_path:
merger.append(fileobj=f)
@ -448,7 +448,7 @@ class DocumentoAdministrativoCrud(Crud):
def form_valid(self, form):
form.instance.complemento = re.sub(
'\s+', '', form.instance.complemento).upper()
r'\s+', '', form.instance.complemento).upper()
return super().form_valid(form)
class UpdateView(Crud.UpdateView):
@ -481,7 +481,7 @@ class DocumentoAdministrativoCrud(Crud):
break
form.instance.complemento = re.sub(
'\s+', '', form.instance.complemento).upper()
r'\s+', '', form.instance.complemento).upper()
return super().form_valid(form)
@ -992,7 +992,6 @@ class ProtocoloMateriaView(PermissionRequiredMixin, CreateView):
def get_context_data(self, **kwargs):
context = super(CreateView, self).get_context_data(**kwargs)
autores_ativos = self.autores_ativos()
autores = []
autores.append(['0', '------'])
for a in autores_ativos:
@ -1034,6 +1033,7 @@ class ProtocoloMateriaTemplateView(PermissionRequiredMixin, TemplateView):
class PesquisarDocumentoAdministrativoView(DocumentoAdministrativoMixin,
MultiFormatOutputMixin,
PermissionRequiredMixin,
FilterView):
model = DocumentoAdministrativo
@ -1041,6 +1041,10 @@ class PesquisarDocumentoAdministrativoView(DocumentoAdministrativoMixin,
paginate_by = 10
permission_required = ('protocoloadm.list_documentoadministrativo', )
export_fields = [
'id', 'ano', 'numero', 'tipo__sigla', 'tipo__descricao', 'assunto'
]
def get_filterset_kwargs(self, filterset_class):
super(PesquisarDocumentoAdministrativoView,
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,
Layout, Submit)
from django import forms
from django.forms import ModelChoiceField
from django.utils.translation import ugettext_lazy as _
from django.db.models import Q
from sapl.audiencia.models import AudienciaPublica
from sapl.base.models import Autor
from sapl.comissoes.models import Reuniao
from sapl.crispy_layout_mixin import SaplFormHelper, to_row, form_actions
from sapl.materia.models import DocumentoAcessorio, MateriaLegislativa, MateriaEmTramitacao, UnidadeTramitacao, \
StatusTramitacao
StatusTramitacao, TipoMateriaLegislativa
from sapl.norma.models import NormaJuridica
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, \
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 Meta(FilterOverridesMetaMixin):
model = SessaoPlenaria
@ -313,7 +379,8 @@ class RelatorioDataFimPrazoTramitacaoFilterSet(django_filters.FilterSet):
@property
def qs(self):
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):
model = MateriaEmTramitacao

6
sapl/relatorios/urls.py

@ -11,7 +11,7 @@ from .views import (relatorio_capa_processo,
RelatorioMateriasTramitacaoView, RelatorioMateriaAnoAssuntoView, RelatorioHistoricoTramitacaoView,
RelatorioDataFimPrazoTramitacaoView, RelatorioPresencaSessaoView, RelatorioAtasView,
RelatorioReuniaoView, RelatorioAudienciaView, RelatorioHistoricoTramitacaoAdmView,
RelatorioDocumentosAcessoriosView, RelatorioNormasPorAutorView)
RelatorioDocumentosAcessoriosView, RelatorioNormasPorAutorView, RelatorioVotacoesNominaisView)
from ..base.views import EstatisticasAcessoNormas
app_name = AppConfig.name
@ -95,6 +95,10 @@ urlpatterns = [
url(r'^sistema/relatorios/documentos_acessorios$',
RelatorioDocumentosAcessoriosView.as_view(),
name='relatorio_documentos_acessorios'),
url(r'^sistema/relatorios/votacoes_nominais$',
RelatorioVotacoesNominaisView.as_view(),
name='relatorio_votacoes_nominais'),
url(r'^sistema/relatorios/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, \
RelatorioMateriasPorAnoAutorTipoFilterSet, RelatorioMateriasTramitacaoFilterSet, RelatorioAudienciaFilterSet, \
RelatorioReuniaoFilterSet, RelatorioDataFimPrazoTramitacaoFilterSet, RelatorioHistoricoTramitacaoFilterSet, \
RelatorioPresencaSessaoFilterSet, RelatorioAtasFilterSet, RelatorioDocumentosAcessoriosFilterSet
RelatorioPresencaSessaoFilterSet, RelatorioAtasFilterSet, RelatorioDocumentosAcessoriosFilterSet, \
RelatorioVotacoesNominaisFilterSet
from sapl.sessao.models import (ExpedienteMateria, ExpedienteSessao,
IntegranteMesa, JustificativaAusencia,
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 STATIC_ROOT
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,
pdf_documento_administrativo_gerar, pdf_espelho_gerar,
pdf_etiqueta_protocolo_gerar, pdf_materia_gerar,
@ -612,13 +613,13 @@ def get_sessao_plenaria(sessao, casa, user):
if not is_empty(conteudo):
# unescape HTML codes
# https://github.com/interlegis/sapl/issues/1046
conteudo = re.sub('style=".*?"', '', conteudo)
conteudo = re.sub('class=".*?"', '', conteudo)
conteudo = re.sub(r'style=".*?"', '', conteudo)
conteudo = re.sub(r'class=".*?"', '', conteudo)
# OSTicket Ticket #796450
conteudo = re.sub('align=".*?"', '', conteudo)
conteudo = re.sub('<p\s+>', '<p>', conteudo)
conteudo = re.sub(r'align=".*?"', '', conteudo)
conteudo = re.sub(r'<p\s+>', '<p>', conteudo)
# OSTicket Ticket #796450
conteudo = re.sub('<br\s+/>', '<br/>', conteudo)
conteudo = re.sub(r'<br\s+/>', '<br/>', conteudo)
conteudo = html.unescape(conteudo)
# escape special character '&'
@ -874,7 +875,7 @@ def get_sessao_plenaria(sessao, casa, user):
# unescape HTML codes
# https://github.com/interlegis/sapl/issues/1046
conteudo = re.sub('style=".*?"', '', conteudo)
conteudo = re.sub(r'style=".*?"', '', conteudo)
conteudo = html.unescape(conteudo)
# escape special character '&'
@ -894,7 +895,7 @@ def get_sessao_plenaria(sessao, casa, user):
# unescape HTML codes
# https://github.com/interlegis/sapl/issues/1046
conteudo = re.sub('style=".*?"', '', conteudo)
conteudo = re.sub(r'style=".*?"', '', conteudo)
conteudo = html.unescape(conteudo)
# escape special character '&'
@ -1321,13 +1322,13 @@ def get_pauta_sessao(sessao, casa):
if not is_empty(conteudo):
# unescape HTML codes
# https://github.com/interlegis/sapl/issues/1046
conteudo = re.sub('style=".*?"', '', conteudo)
conteudo = re.sub('class=".*?"', '', conteudo)
conteudo = re.sub(r'style=".*?"', '', conteudo)
conteudo = re.sub(r'class=".*?"', '', conteudo)
# OSTicket Ticket #796450
conteudo = re.sub('align=".*?"', '', conteudo)
conteudo = re.sub('<p\s+>', '<p>', conteudo)
conteudo = re.sub(r'align=".*?"', '', conteudo)
conteudo = re.sub(r'<p\s+>', '<p>', conteudo)
# OSTicket Ticket #796450
conteudo = re.sub('<br\s+/>', '<br/>', conteudo)
conteudo = re.sub(r'<br\s+/>', '<br/>', conteudo)
conteudo = html.unescape(conteudo)
# escape special character '&'
@ -1560,6 +1561,10 @@ def relatorio_documento_acessorio(obj, request, context):
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):
return cria_relatorio(request, context, 'relatorios/relatorio_normas_por_autor.html')
@ -1880,6 +1885,75 @@ class RelatorioDocumentosAcessoriosView(RelatorioMixin, FilterView):
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):
model = SessaoPlenaria
filterset_class = RelatorioAtasFilterSet

2
sapl/rules/group_geral.py

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

11
sapl/sessao/models.py

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

106
sapl/sessao/views.py

@ -45,7 +45,8 @@ from sapl.sessao.forms import ExpedienteMateriaForm, OrdemDiaForm, OrdemExpedien
CorrespondenciaForm, CorrespondenciaEmLoteFilterSet
from sapl.sessao.models import Correspondencia
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,
ExpedienteForm, JustificativaAusenciaForm, OcorrenciaSessaoForm, ListMateriaForm,
@ -147,7 +148,8 @@ def verifica_votacoes_abertas(request):
for sessao in votacoes_abertas:
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:
o.votacao_aberta = False
o.save()
@ -1825,7 +1827,8 @@ def insere_parlamentar_composicao(request):
if parlamentar_ja_inserido:
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))
return JsonResponse({'msg': ('Parlamentar já inserido!', 0)})
@ -2534,7 +2537,8 @@ class ExpedienteView(FormMixin, DetailView):
msg = _('Registro salvo com sucesso')
messages.add_message(self.request, messages.SUCCESS, msg)
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))
return self.form_valid(form)
@ -3805,10 +3809,28 @@ class PautaSessaoView(TemplateView):
reverse('sapl.sessao:pauta_sessao_detail', kwargs={'pk': sessao.pk}))
class PautaSessaoDetailView(DetailView):
class PautaSessaoDetailView(PautaMultiFormatOutputMixin, DetailView):
template_name = "sessao/pauta_sessao_detail.html"
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):
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"),
'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()],
'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})
@ -3951,7 +3974,8 @@ class PautaSessaoDetailView(DetailView):
'situacao': ultima_tramitacao.status if ultima_tramitacao else _("Não informada"),
'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)],
'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({
@ -3966,13 +3990,21 @@ class PautaSessaoDetailView(DetailView):
return self.render_to_response(context)
class PesquisarSessaoPlenariaView(FilterView):
class PesquisarSessaoPlenariaView(MultiFormatOutputMixin, FilterView):
model = SessaoPlenaria
filterset_class = SessaoPlenariaFilterSet
paginate_by = 10
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):
super().get_filterset_kwargs(filterset_class)
@ -3989,47 +4021,60 @@ class PesquisarSessaoPlenariaView(FilterView):
})
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):
context = super().get_context_data(**kwargs)
context['title'] = _('Pesquisar Sessão Plenária')
paginator = context['paginator']
page_obj = context['page_obj']
context['page_range'] = make_pagination(
page_obj.number, paginator.num_pages)
return context
def get(self, request, *args, **kwargs):
super().get(request)
context['show_results'] = show_results_filter_set(
self.request.GET.copy())
# 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
if data and data.get('data_inicio__year') is not None:
url = "&" + str(self.request.META['QUERY_STRING'])
if url.startswith("&page"):
ponto_comeco = url.find('data_inicio__year=') - 1
url = url[ponto_comeco:]
else:
url = ''
context['filter_url'] = url
context = self.get_context_data(filter=self.filterset,
object_list=self.object_list,
filter_url=url,
numero_res=len(self.object_list)
)
context['numero_res'] = len(self.object_list)
context['show_results'] = show_results_filter_set(
self.request.GET.copy())
return context
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
self.logger.debug('user=' + username + '. Pesquisa de SessaoPlenaria.')
return self.render_to_response(context)
return r
class PesquisarPautaSessaoView(PesquisarSessaoPlenariaView):
@ -4038,6 +4083,8 @@ class PesquisarPautaSessaoView(PesquisarSessaoPlenariaView):
logger = logging.getLogger(__name__)
viewname = 'sapl.sessao:pesquisar_pauta'
def get_filterset_kwargs(self, filterset_class):
kwargs = super().get_filterset_kwargs(filterset_class)
qs = kwargs.get('queryset')
@ -4318,7 +4365,7 @@ def mudar_ordem_materia_sessao(request):
class JustificativaAusenciaCrud(MasterDetailCrud):
model = JustificativaAusencia
public = [RP_LIST, RP_DETAIL, ]
public = [RP_LIST]
parent_field = 'sessao_plenaria'
class BaseMixin(MasterDetailCrud.BaseMixin):
@ -4456,8 +4503,11 @@ class LeituraEmBloco(PermissionRequiredForAppCrudMixin, ListView):
leituras.append(obj)
RegistroLeitura.objects.bulk_create(leituras)
models.update(resultado='Matéria Lida')
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 HttpResponseRedirect(self.get_success_url())

95
sapl/settings.py

@ -24,13 +24,15 @@ from unipath import Path
logging.captureWarnings(True)
logger = logging.getLogger(__name__)
host = socket.gethostbyname_ex(socket.gethostname())[0]
BASE_DIR = Path(__file__).ancestor(1)
PROJECT_DIR = Path(__file__).ancestor(2)
# 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!
DEBUG = config('DEBUG', default=False, cast=bool)
@ -41,7 +43,7 @@ ALLOWED_HOSTS = ['*']
LOGIN_REDIRECT_URL = '/'
LOGIN_URL = '/login/?next='
SAPL_VERSION = '3.1.163-RC20'
SAPL_VERSION = '3.1.164-RC1'
if DEBUG:
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
@ -75,10 +77,11 @@ INSTALLED_APPS = (
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.forms',
'django_extensions',
'crispy_forms',
'floppyforms',
'waffle',
@ -97,7 +100,7 @@ INSTALLED_APPS = (
'django_prometheus',
) + SAPL_APPS
) + SAPL_APPS
# FTS = Full Text Search
# 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_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:
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor' # enable auto-index
SEARCH_BACKEND = 'haystack.backends.solr_backend.SolrEngine'
@ -123,6 +130,10 @@ HAYSTACK_CONNECTIONS = {
SEARCH_URL[0]: SEARCH_URL[1],
'BATCH_SIZE': 1000,
'TIMEOUT': 20,
# 'KWARGS': {
# 'timeout': 60,
# 'auth': (SOLR_USER, SOLR_PASSWORD), # Basic Auth
# },
},
}
@ -140,6 +151,7 @@ MIDDLEWARE = [
'whitenoise.middleware.WhiteNoiseMiddleware',
'django_prometheus.middleware.PrometheusAfterMiddleware',
'waffle.middleware.WaffleMiddleware',
'sapl.middleware.CheckWeakPasswordMiddleware',
]
if DEBUG:
INSTALLED_APPS += ('debug_toolbar',)
@ -192,11 +204,14 @@ CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
'LOCATION': '/var/tmp/django_cache',
'OPTIONS': {"MAX_ENTRIES": 10000},
}
}
ROOT_URLCONF = 'sapl.urls'
FORM_RENDERER = 'django.forms.renderers.TemplatesSetting'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
@ -225,17 +240,47 @@ WSGI_APPLICATION = 'sapl.wsgi.application'
# Database
# 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 = {
'default': config(
'DATABASE_URL', default='sqlite://:memory:',
cast=db_url,
)
"default": config("DATABASE_URL", 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
THUMBNAIL_PROCESSORS = (
'image_cropping.thumbnail_processors.crop_corners',
) + thumbnail_settings.THUMBNAIL_PROCESSORS
) + thumbnail_settings.THUMBNAIL_PROCESSORS
THUMBNAIL_SOURCE_GENERATORS = (
'sapl.utils.pil_image',
@ -266,7 +311,6 @@ WAFFLE_CREATE_MISSING_SWITCHES = True
WAFFLE_LOG_MISSING_SWITCHES = True
WAFFLE_ENABLE_ADMIN_PAGES = True
MAX_DOC_UPLOAD_SIZE = 150 * 1024 * 1024 # 150MB
MAX_IMAGE_UPLOAD_SIZE = 2 * 1024 * 1024 # 2MB
DATA_UPLOAD_MAX_MEMORY_SIZE = 10 * 1024 * 1024 # 10MB
@ -286,6 +330,30 @@ if not TIME_ZONE:
USE_I18N = True
USE_L10N = 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 = 'd/m/Y'
SHORT_DATE_FORMAT = 'd/m/Y'
@ -333,9 +401,6 @@ DAB_FIELD_RENDERER = \
CRISPY_TEMPLATE_PACK = 'bootstrap4'
CRISPY_ALLOWED_TEMPLATE_PACKS = 'bootstrap4'
CRISPY_FAIL_SILENTLY = not DEBUG
FLOPPY_FORMS_USE_GIS = False
FORM_RENDERER = 'django.forms.renderers.DjangoTemplates'
# suprime texto de ajuda default do django-filter
FILTERS_HELP_TEXT_FILTER = False
@ -387,12 +452,12 @@ LOGGING = {
},
'loggers': {
'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',
'propagate': True,
},
'django': {
'handlers': ['applogfile'] + ['console_verbose'] if LOGGING_CONSOLE_VERBOSE else [],
'handlers': ['applogfile'] + (['console_verbose'] if LOGGING_CONSOLE_VERBOSE else []),
'level': 'ERROR',
'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