Browse Source

Merge tag '3.1.163-RC6' into migracao

migracao
Marcio Mazza 2 years ago
parent
commit
b847e295f2
  1. 17
      .drone.yml
  2. 3
      .eslintrc.js
  3. 14
      README.rst
  4. 0
      babel.config.js
  5. 12
      dist/bin/upload_configset.sh
  6. 111
      dist/docker-compose.yml
  7. 13
      dist/solr_cloud/security.json
  8. 21
      docker/Dockerfile
  9. 6
      docker/config/nginx/nginx.conf
  10. 32
      docker/config/nginx/sapl.conf
  11. 7
      docker/docker-compose.yaml
  12. 2
      docker/gunicorn_start.sh
  13. 127
      docker/solr_cli.py
  14. 30
      docker/start.sh
  15. 0
      docker/wait-for-pg.sh
  16. 8
      docker/wait-for-solr.sh
  17. 4
      docs/instalacao31.rst
  18. 2
      docs/solr.rst
  19. BIN
      frontend/public/img/arrow.png
  20. BIN
      frontend/public/img/authenticated.png
  21. BIN
      frontend/public/img/avatar.png
  22. BIN
      frontend/public/img/beta.png
  23. BIN
      frontend/public/img/brasao_transp.gif
  24. BIN
      frontend/public/img/down_arrow_select.jpg
  25. BIN
      frontend/public/img/etiqueta.png
  26. BIN
      frontend/public/img/favicon.ico
  27. BIN
      frontend/public/img/file.png
  28. BIN
      frontend/public/img/hand-note.png
  29. BIN
      frontend/public/img/icon_comissoes.png
  30. BIN
      frontend/public/img/icon_delete_white.png
  31. BIN
      frontend/public/img/icon_materia_legislativa.png
  32. BIN
      frontend/public/img/icon_mesa_diretora.png
  33. BIN
      frontend/public/img/icon_normas_juridicas.png
  34. BIN
      frontend/public/img/icon_parlamentares.png
  35. BIN
      frontend/public/img/icon_pautas.png
  36. BIN
      frontend/public/img/icon_plenarias.png
  37. BIN
      frontend/public/img/icon_relatorios.png
  38. BIN
      frontend/public/img/icon_save_white.png
  39. BIN
      frontend/public/img/lexml.gif
  40. BIN
      frontend/public/img/logo.png
  41. BIN
      frontend/public/img/logo_cc.png
  42. BIN
      frontend/public/img/logo_interlegis.png
  43. BIN
      frontend/public/img/manual.png
  44. BIN
      frontend/public/img/pdflogo.png
  45. BIN
      frontend/public/img/perfil.png
  46. BIN
      frontend/public/img/search-gray.png
  47. BIN
      frontend/public/img/search.png
  48. BIN
      frontend/public/img/user.png
  49. 8
      frontend/src/__apps/compilacao/js/old/compilacao_edit.js
  50. 17
      frontend/src/__apps/compilacao/scss/compilacao.scss
  51. 33
      frontend/src/__global/js/tinymce/index.js
  52. 32
      frontend/src/__global/main.js
  53. 38
      frontend/src/__global/scss/_header.scss
  54. 6
      frontend/src/__global/scss/libs/bootstrap/_nav_navbar.scss
  55. 0
      frontend/src/assets/audio/ring.mp3
  56. 808
      frontend/webpack-stats.json
  57. 77
      package.json
  58. 15
      release.sh
  59. 2
      requirements/dev-requirements.txt
  60. 19
      requirements/requirements.txt
  61. 307
      sapl/api/core/__init__.py
  62. 116
      sapl/api/core/filters.py
  63. 25
      sapl/api/core/forms.py
  64. 5
      sapl/api/core/schema.py
  65. 50
      sapl/api/core/serializers.py
  66. 18
      sapl/api/deprecated.py
  67. 65
      sapl/api/forms.py
  68. 67
      sapl/api/pagination.py
  69. 64
      sapl/api/serializers.py
  70. 46
      sapl/api/urls.py
  71. 668
      sapl/api/views.py
  72. 413
      sapl/api/viewset.py
  73. 2
      sapl/audiencia/views.py
  74. 55
      sapl/base/forms.py
  75. 19
      sapl/base/migrations/0048_appconfig_tramitacao_origem_fixa.py
  76. 18
      sapl/base/migrations/0049_auto_20220728_2029.py
  77. 31
      sapl/base/migrations/0050_metadata.py
  78. 23
      sapl/base/migrations/0051_auto_20220814_2138.py
  79. 199
      sapl/base/models.py
  80. 17
      sapl/base/receivers.py
  81. 26
      sapl/base/search_indexes.py
  82. 30
      sapl/base/templatetags/common_tags.py
  83. 5
      sapl/base/urls.py
  84. 95
      sapl/base/views.py
  85. 23
      sapl/comissoes/migrations/0028_auto_20220807_2257.py
  86. 9
      sapl/comissoes/models.py
  87. 4
      sapl/comissoes/views.py
  88. 44
      sapl/compilacao/migrations/0019_auto_20220630_1420.py
  89. 38
      sapl/compilacao/models.py
  90. 80
      sapl/compilacao/views.py
  91. 26
      sapl/crispy_layout_mixin.py
  92. 33
      sapl/crud/base.py
  93. 40
      sapl/decorators.py
  94. 23
      sapl/lexml/OAIServer.py
  95. 52
      sapl/materia/forms.py
  96. 18
      sapl/materia/migrations/0080_auto_20211112_1106.py
  97. 33
      sapl/materia/migrations/0081_auto_20220321_0934.py
  98. 6
      sapl/materia/models.py
  99. 58
      sapl/materia/views.py
  100. 80
      sapl/norma/forms.py

17
.drone.yml

@ -0,0 +1,17 @@
kind: pipeline
type: kubernetes
name: default
steps:
- name: docker
image: plugins/docker
settings:
repo: porto.interlegis.leg.br/spdt/sapl
registry: porto.interlegis.leg.br
mirror: https://registrycache.interlegis.leg.br
pull: if-not-exists
dockerfile: docker/Dockerfile
auto_tag: true
username:
from_secret: porto_user
password:
from_secret: porto_pw

3
frontend/.eslintrc.js → .eslintrc.js

@ -20,7 +20,8 @@ module.exports = {
'vue' 'vue'
], ],
parserOptions: { parserOptions: {
parser: 'babel-eslint' parser: '@babel/eslint-parser'
// requireConfigFile: false
}, },
globals: { globals: {

14
README.rst

@ -6,6 +6,19 @@
SAPL - Sistema de Apoio ao Processo Legislativo SAPL - Sistema de Apoio ao Processo Legislativo
*********************************************** ***********************************************
UPDATE! [02/08/2022]: Novas alterações foram realizadas nos containers do SAPL e no docker-compose.yaml. Estas mudanças estarão funcionais a partir do próximo release. Enquanto isso não vem, continuem utilizando as versões antigas do docker-compose.yaml.
~~**UPDATE! [16/05/2022]: Devido a refatorações recentes no Solr, foi necessårio
adaptar o uso deste pelo SAPL. Para isso foram feitas mudanças no docker-compose.yml
como a adição de um container para o ZooKeeper e upload de arquivo de segurança.
Recomendamos fortemente que para a versão 3.1.162 e superior do SAPL seja feito o backup do
Banco de Dados, limpeza dos containers no host (`sudo docker system prune -a -f --volumes`),
e consequente instalação dos novos containers a partir da execução do docker-compose. É
importante frisar que o comando `docker system prune` irá apagar TODOS os containers E
TODOS os volumes (incluindo o BD) do host. Após o inicio dos novos containers, proceda
com a restauração do BD, pare os containers e reinicie novamente para indexação textual.
Além disso, o docker-compose.yml foi movido para a pasta dist/ na raiz do projeto.**~~
Esta página reúne informações úteis sobre o desenvolvimento atual do SAPL. Esta página reúne informações úteis sobre o desenvolvimento atual do SAPL.
Isso significa que toda a informação aqui apresentada aplica-se apenas para a versão 3.1 e superior. Isso significa que toda a informação aqui apresentada aplica-se apenas para a versão 3.1 e superior.
@ -73,3 +86,4 @@ Issues
* Abra todas as questões sobre o desenvolvimento atual no `Github Issue Tracker <https://github.com/interlegis/sapl/issues>`_. * Abra todas as questões sobre o desenvolvimento atual no `Github Issue Tracker <https://github.com/interlegis/sapl/issues>`_.
* Você pode escrever suas ``issues`` em Português ou Inglês (ao menos por enquanto). * Você pode escrever suas ``issues`` em Português ou Inglês (ao menos por enquanto).

0
frontend/babel.config.js → babel.config.js

12
dist/bin/upload_configset.sh

@ -0,0 +1,12 @@
#!/usr/bin/env bash
SOLR_USER=solr
SOLR_PASSWORD=SolrRocks
SOLR_HOST=localhost
SOLR_PORT=8983
CONFIGSET_NAME=sapl_configset
CONFIGSET_FILE=sapl_configset.zip
export SOLR_URL="http://$SOLR_USER:$SOLR_PASSWORD@$SOLR_HOST:$SOLR_PORT/solr/admin/configs?action=UPLOAD&name=$CONFIGSET_NAME&wt=json"
curl -X POST -L -F "file=@$CONFIGSET_FILE;type=application/zip" $SOLR_URL

111
dist/docker-compose.yml

@ -0,0 +1,111 @@
version: "3.7"
services:
sapldb:
image: postgres:10.5-alpine
restart: always
container_name: postgres
labels:
NAME: "postgres"
environment:
POSTGRES_PASSWORD: sapl
POSTGRES_USER: sapl
POSTGRES_DB: sapl
PGDATA : /var/lib/postgresql/data/
volumes:
- sapldb_data:/var/lib/postgresql/data/
ports:
- "5433:5432"
networks:
- sapl-net
solr1:
image: solr:8.11
restart: unless-stopped
command: bash -c "docker-entrypoint.sh solr zk cp file:/var/security.json zk:security.json && exec solr-foreground"
container_name: solr
labels:
NAME: "solr"
ports:
- "8983:8983"
environment:
- ZK_HOST=zoo1:2181
- SOLR_HEAP=1g
- SOLR_OPTS=-Djute.maxbuffer=50000000
networks:
- sapl-net
depends_on:
- zoo1
volumes:
- type: bind
source: ./solr_cloud/security.json
target: /var/security.json
- solr_data:/opt/solr/server/solr
- solr_configsets:/opt/solr/server/solr/configsets
sapl:
image: interlegis/sapl:3.1.163-RC2
# build:
# context: ../
# dockerfile: ./docker/Dockerfile
container_name: sapl
labels:
NAME: "sapl"
restart: always
environment:
ADMIN_PASSWORD: interlegis
ADMIN_EMAIL: email@dominio.net
DEBUG: 'False'
EMAIL_PORT: 587
EMAIL_USE_TLS: 'False'
EMAIL_HOST: smtp.dominio.net
EMAIL_HOST_USER: usuariosmtp
EMAIL_SEND_USER: usuariosmtp
EMAIL_HOST_PASSWORD: senhasmtp
USE_SOLR: 'True'
SOLR_COLLECTION: sapl
SOLR_URL: http://solr:SolrRocks@solr1:8983
TZ: America/Sao_Paulo
volumes:
- sapl_data:/var/interlegis/sapl/data
- sapl_media:/var/interlegis/sapl/media
depends_on:
- sapldb
- solr1
ports:
- "80:80"
networks:
- sapl-net
zoo1:
image: zookeeper:3.8
container_name: zoo1
hostname: zoo1
restart: unless-stopped
ports:
- 2181:2181
- 7001:7000
environment:
ZOO_MY_ID: 1
ZOOKEEPER_TICK_TIME: 2000
ZOOKEEPER_CLIENT_PORT: 2181
JVMFLAGS: "-Xmx1024m -Djute.maxbuffer=50000000"
ZOO_SERVERS: server.1=zoo1:2888:3888;2181
ZOO_LOG4J_PROP: "INFO,ROLLINGFILE"
ZOO_4LW_COMMANDS_WHITELIST: mntr, conf, ruok
ZOO_CFG_EXTRA: "metricsProvider.className=org.apache.zookeeper.metrics.prometheus.PrometheusMetricsProvider metricsProvider.httpPort=7000 metricsProvider.exportJvmInfo=true"
volumes:
- zoo_data:/data
- zoo_log:/datalog
networks:
- sapl-net
networks:
sapl-net:
name: sapl-net
driver: bridge
volumes:
sapldb_data:
sapl_data:
sapl_media:
solr_data:
solr_home:
solr_configsets:
zoo_data:
zoo_log:

13
dist/solr_cloud/security.json

@ -0,0 +1,13 @@
{
"authentication":{
"blockUnknown": true,
"class":"solr.BasicAuthPlugin",
"credentials":{"solr":"IV0EHq1OnNrj6gvRCwvFwTrZ1+z1oBbnQdiVC3otuq0= Ndd7LKvVBAaZIF0QAVi1ekCfAJXr1GGfLtRUXhgrF8c="},
"forwardCredentials": false,
"realm": "Solr Login"
},
"authorization":{
"class":"solr.RuleBasedAuthorizationPlugin",
"permissions":[{"name":"security-edit", "role":"admin"}],
"user-role":{"solr":"admin"}
}}

21
docker/Dockerfile

@ -1,20 +1,19 @@
FROM python:3.7-slim-buster FROM python:3.9-slim-buster
# Setup env # Setup env
ENV LANG C.UTF-8 ENV LANG C.UTF-8
ENV LC_ALL C.UTF-8 ENV LC_ALL C.UTF-8
ENV PYTHONDONTWRITEBYTECODE 1 ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED=1
#ENV PYTHONFAULTHANDLER 1
ENV DEBIAN_FRONTEND noninteractive ENV DEBIAN_FRONTEND noninteractive
ENV BUILD_PACKAGES apt-utils apt-file libpq-dev graphviz-dev build-essential git pkg-config \ 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 \ python3-dev libxml2-dev libjpeg-dev libssl-dev libffi-dev libxslt1-dev \
libcairo2-dev software-properties-common python3-setuptools python3-pip 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 \ ENV RUN_PACKAGES graphviz python3-lxml python3-magic postgresql-client python3-psycopg2 \
poppler-utils curl jq bash python3-venv tzdata nodejs \ poppler-utils curl jq bash vim python3-venv tzdata nodejs \
fontconfig ttf-dejavu python nginx fontconfig ttf-dejavu python nginx
RUN mkdir -p /var/interlegis/sapl RUN mkdir -p /var/interlegis/sapl
@ -33,12 +32,13 @@ RUN apt-get update && \
SUDO_FORCE_REMOVE=yes apt-get purge -y --auto-remove $BUILD_PACKAGES && \ SUDO_FORCE_REMOVE=yes apt-get purge -y --auto-remove $BUILD_PACKAGES && \
apt-get autoremove && apt-get clean && rm -rf /var/lib/apt/lists/* apt-get autoremove && apt-get clean && rm -rf /var/lib/apt/lists/*
ENV HOME=/var/interlegis/sapl WORKDIR /var/interlegis/sapl/
ADD . /var/interlegis/sapl/
COPY docker/start.sh $HOME COPY docker/start.sh $HOME
COPY docker/check_solr.sh $HOME COPY docker/solr_cli.py $HOME
COPY docker/solr_api.py $HOME COPY docker/wait-for-pg.sh $HOME
COPY docker/busy-wait.sh $HOME COPY docker/wait-for-solr.sh $HOME
COPY docker/create_admin.py $HOME COPY docker/create_admin.py $HOME
COPY docker/genkey.py $HOME COPY docker/genkey.py $HOME
COPY docker/gunicorn_start.sh $HOME COPY docker/gunicorn_start.sh $HOME
@ -54,7 +54,8 @@ RUN rm -rf /var/interlegis/sapl/sapl/.env && \
rm -rf /var/interlegis/sapl/sapl.db rm -rf /var/interlegis/sapl/sapl.db
RUN chmod +x /var/interlegis/sapl/start.sh && \ RUN chmod +x /var/interlegis/sapl/start.sh && \
chmod +x /var/interlegis/sapl/check_solr.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/stdout /var/log/nginx/access.log && \
ln -sf /dev/stderr /var/log/nginx/error.log && \ ln -sf /dev/stderr /var/log/nginx/error.log && \
mkdir /var/log/sapl/ && touch /var/interlegis/sapl/sapl.log && \ mkdir /var/log/sapl/ && touch /var/interlegis/sapl/sapl.log && \

6
docker/config/nginx/nginx.conf

@ -23,7 +23,11 @@ http {
sendfile off; sendfile off;
#tcp_nopush on; #tcp_nopush on;
keepalive_timeout 65; keepalive_timeout 300;
proxy_connect_timeout 75s;
proxy_read_timeout 300s;
gzip on; gzip on;
gzip_disable "MSIE [1-6]\\.(?!.*SV1)"; gzip_disable "MSIE [1-6]\\.(?!.*SV1)";

32
docker/config/nginx/sapl.conf

@ -11,6 +11,31 @@ server {
client_max_body_size 4G; client_max_body_size 4G;
location /api/ {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, HEAD, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Access-Control-Allow-Origin,XMLHttpRequest,Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Mx-ReqToken,X-Requested-With';
add_header 'Access-Control-Expose-Headers' 'Access-Control-Allow-Origin,XMLHttpRequest,Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Mx-ReqToken,X-Requested-With';
# handle the browser's preflight steps
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, HEAD, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Authorization,Accept,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range';
add_header 'Access-Control-Expose-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass http://sapl_server;
}
location /static/ { location /static/ {
alias /var/interlegis/sapl/collected_static/; alias /var/interlegis/sapl/collected_static/;
} }
@ -21,17 +46,12 @@ server {
location / { location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
proxy_redirect off; proxy_redirect off;
if (!-f $request_filename) {
proxy_pass http://sapl_server; proxy_pass http://sapl_server;
break;
}
} }
error_page 500 502 503 504 /500.html; error_page 500 502 503 504 /500.html;
location = /500.html { location = /500.html {
root /var/interlegis/sapl/sapl/static/; root /var/interlegis/sapl/sapl/static/;

7
docker/docker-compose.yml → docker/docker-compose.yaml

@ -18,7 +18,7 @@ services:
networks: networks:
- sapl-net - sapl-net
saplsolr: saplsolr:
image: solr:8.3 image: solr:8.11
restart: always restart: always
command: bin/solr start -c -f command: bin/solr start -c -f
container_name: solr container_name: solr
@ -32,7 +32,7 @@ services:
networks: networks:
- sapl-net - sapl-net
sapl: sapl:
image: interlegis/sapl:3.1.162-RC6 image: interlegis/sapl:3.1.163-RC6
# build: # build:
# context: ../ # context: ../
# dockerfile: ./docker/Dockerfile # dockerfile: ./docker/Dockerfile
@ -52,7 +52,8 @@ services:
EMAIL_HOST_PASSWORD: senhasmtp EMAIL_HOST_PASSWORD: senhasmtp
USE_SOLR: 'True' USE_SOLR: 'True'
SOLR_COLLECTION: sapl SOLR_COLLECTION: sapl
SOLR_URL: http://saplsolr:8983 SOLR_URL: http://solr:solr@saplsolr:8983
IS_ZK_EMBEDDED: 'True'
TZ: America/Sao_Paulo TZ: America/Sao_Paulo
volumes: volumes:
- sapl_data:/var/interlegis/sapl/data - sapl_data:/var/interlegis/sapl/data

2
docker/gunicorn_start.sh

@ -25,7 +25,7 @@ USER=`whoami` # the user to run as (*)
GROUP=`whoami` # the group to run as (*) GROUP=`whoami` # the group to run as (*)
NUM_WORKERS=3 # how many worker processes should Gunicorn spawn (*) NUM_WORKERS=3 # how many worker processes should Gunicorn spawn (*)
# NUM_WORKERS = 2 * CPUS + 1 # NUM_WORKERS = 2 * CPUS + 1
TIMEOUT=60 TIMEOUT=300
MAX_REQUESTS=100 # number of requests before restarting worker MAX_REQUESTS=100 # number of requests before restarting worker
DJANGO_SETTINGS_MODULE=sapl.settings # which settings file should Django use (*) DJANGO_SETTINGS_MODULE=sapl.settings # which settings file should Django use (*)
DJANGO_WSGI_MODULE=sapl.wsgi # WSGI module name (*) DJANGO_WSGI_MODULE=sapl.wsgi # WSGI module name (*)

127
docker/solr_api.py → docker/solr_cli.py

@ -1,18 +1,102 @@
from io import BytesIO #!/usr/bin/env python3
# -*- coding: utf-8 -*-
import argparse import argparse
import os import logging
import requests import re
import secrets
import subprocess import subprocess
import sys import sys
import zipfile import zipfile
from base64 import b64encode, b64decode
from hashlib import sha256
from io import BytesIO
from pathlib import Path from pathlib import Path
## import requests
## Este módulo deve ser executado na raiz do projeto from kazoo.client import KazooClient
##
#
# Este módulo deve ser executado na raiz do projeto
#
logging.basicConfig()
SECURITY_FILE_TEMPLATE = """
{
"authentication":{
"blockUnknown": true,
"class":"solr.BasicAuthPlugin",
"credentials":{"%s":"%s %s"},
"forwardCredentials": false,
"realm": "Solr Login"
},
"authorization":{
"class":"solr.RuleBasedAuthorizationPlugin",
"permissions":[{"name":"security-edit", "role":"admin"}],
"user-role":{"%s":"admin"}
}
}
"""
URL_PATTERN = 'https?://(([a-zA-Z0-9]+):([a-zA-Z0-9]+)@)?([a-zA-Z0-9.-]+)(:[0-9]{4})?'
def solr_hash_password(password: str, salt: str = None):
"""
Generates a password and salt to be used in Basic Auth Solr
password: clean text password string
salt (optional): base64 salt string
returns: sha256 hash of password and salt (both base64 strings)
"""
m = sha256()
if salt is None:
salt = secrets.token_bytes(32)
else:
salt = b64decode(salt)
m.update(salt + password.encode('utf-8'))
digest = m.digest()
m = sha256()
m.update(digest)
digest = m.digest()
cypher = b64encode(digest).decode('utf-8')
salt = b64encode(salt).decode('utf-8')
return cypher, salt
class SolrClient:
def create_security_file(username, password):
print("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!")
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}...")
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...")
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'))
zk.stop()
except Exception as e:
print(e)
sys.exit(-1)
class SolrClient:
LIST_CONFIGSETS = "{}/solr/admin/configs?action=LIST&omitHeader=true&wt=json" LIST_CONFIGSETS = "{}/solr/admin/configs?action=LIST&omitHeader=true&wt=json"
UPLOAD_CONFIGSET = "{}/solr/admin/configs?action=UPLOAD&name={}&wt=json" UPLOAD_CONFIGSET = "{}/solr/admin/configs?action=UPLOAD&name={}&wt=json"
LIST_COLLECTIONS = "{}/solr/admin/collections?action=LIST&wt=json" LIST_COLLECTIONS = "{}/solr/admin/collections?action=LIST&wt=json"
@ -160,6 +244,22 @@ class SolrClient:
print("Num docs: %s" % num_docs) print("Num docs: %s" % num_docs)
def setup_embedded_zk(solr_url):
match = re.match(URL_PATTERN, solr_url)
if match:
_, solr_user, solr_pwd, solr_host, solr_port = match.groups()
if solr_user and solr_pwd and solr_host:
create_security_file(solr_user, solr_pwd)
upload_security_file(solr_host)
else:
print(f"Missing Solr's username, password, and host: {solr_user}/{solr_pwd}/{solr_host}")
sys.exit(-1)
else:
print(f"Solr URL path doesn't match the required format: {solr_url}")
sys.exit(-1)
if __name__ == '__main__': if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Cria uma collection no Solr') parser = argparse.ArgumentParser(description='Cria uma collection no Solr')
@ -178,6 +278,9 @@ if __name__ == '__main__':
parser.add_argument('-ms', type=int, dest='max_shards_per_node', nargs='?', parser.add_argument('-ms', type=int, dest='max_shards_per_node', nargs='?',
help='Max shards per node (default=1)', default=1) help='Max shards per node (default=1)', default=1)
parser.add_argument("--embedded_zk", default=False, action="store_true",
help="Embedded ZooKeeper")
try: try:
args = parser.parse_args() args = parser.parse_args()
except IOError as msg: except IOError as msg:
@ -185,10 +288,17 @@ if __name__ == '__main__':
sys.exit(-1) sys.exit(-1)
url = args.url.pop() url = args.url.pop()
collection = args.collection.pop()
if args.embedded_zk:
print("Setup embedded ZooKeeper...")
setup_embedded_zk(url)
collection = args.collection.pop()
client = SolrClient(url=url) client = SolrClient(url=url)
## Add --force to force upload security.json, configset upload and collection recreation
## it will clean the solr server before proceeding
## Add --clean option to clean uploadconfig and collection
if not client.exists_collection(collection): if not client.exists_collection(collection):
print("Collection '%s' doesn't exists. Creating a new one..." % collection) print("Collection '%s' doesn't exists. Creating a new one..." % collection)
created = client.create_collection(collection, created = client.create_collection(collection,
@ -200,6 +310,7 @@ if __name__ == '__main__':
else: else:
print("Collection '%s' exists." % collection) print("Collection '%s' exists." % collection)
## Add --disable-index to disable auto index
num_docs = client.get_num_docs(collection) num_docs = client.get_num_docs(collection)
if num_docs == 0: if num_docs == 0:
print("Performing a full reindex of '%s' collection..." % collection) print("Performing a full reindex of '%s' collection..." % collection)

30
docker/start.sh

@ -22,7 +22,6 @@ create_env() {
touch $FILENAME touch $FILENAME
# explicitly use '>' to erase any previous content # explicitly use '>' to erase any previous content
echo "SECRET_KEY="$KEY > $FILENAME echo "SECRET_KEY="$KEY > $FILENAME
# now only appends # now only appends
@ -39,14 +38,14 @@ create_env() {
echo "USE_SOLR = ""${USE_SOLR-False}" >> $FILENAME echo "USE_SOLR = ""${USE_SOLR-False}" >> $FILENAME
echo "SOLR_COLLECTION = ""${SOLR_COLLECTION-sapl}" >> $FILENAME echo "SOLR_COLLECTION = ""${SOLR_COLLECTION-sapl}" >> $FILENAME
echo "SOLR_URL = ""${SOLR_URL-http://localhost:8983}" >> $FILENAME echo "SOLR_URL = ""${SOLR_URL-http://localhost:8983}" >> $FILENAME
echo "IS_ZK_EMBEDDED = ""${IS_ZK_EMBEDDED-False}" >> $FILENAME
echo "[ENV FILE] done." echo "[ENV FILE] done."
} }
create_env create_env
/bin/bash busy-wait.sh $DATABASE_URL /bin/bash wait-for-pg.sh $DATABASE_URL
yes yes | python3 manage.py migrate yes yes | python3 manage.py migrate
@ -55,39 +54,46 @@ yes yes | python3 manage.py migrate
USE_SOLR="${USE_SOLR:=False}" USE_SOLR="${USE_SOLR:=False}"
SOLR_URL="${SOLR_URL:=http://localhost:8983}" SOLR_URL="${SOLR_URL:=http://localhost:8983}"
SOLR_COLLECTION="${SOLR_COLLECTION:=sapl}" SOLR_COLLECTION="${SOLR_COLLECTION:=sapl}"
NUM_SHARDS=${NUM_SHARDS:=1} NUM_SHARDS=${NUM_SHARDS:=1}
RF=${RF:=1} RF=${RF:=1}
MAX_SHARDS_PER_NODE=${MAX_SHARDS_PER_NODE:=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 if [ "${USE_SOLR-False}" == "True" ] || [ "${USE_SOLR-False}" == "true" ]; then
echo "SOLR configurations" echo "Solr configurations"
echo "===================" echo "==================="
echo "URL: $SOLR_URL" echo "URL: $SOLR_URL"
echo "COLLECTION: $SOLR_COLLECTION" echo "COLLECTION: $SOLR_COLLECTION"
echo "NUM_SHARDS: $NUM_SHARDS" echo "NUM_SHARDS: $NUM_SHARDS"
echo "REPLICATION FACTOR: $RF" echo "REPLICATION FACTOR: $RF"
echo "MAX SHARDS PER NODE: $MAX_SHARDS_PER_NODE" echo "MAX SHARDS PER NODE: $MAX_SHARDS_PER_NODE"
echo "ASSUME ZK EMBEDDED: $IS_ZK_EMBEDDED"
echo "=========================================" echo "========================================="
echo "running solr script" echo "running Solr script"
/bin/bash check_solr.sh $SOLR_URL /bin/bash wait-for-solr.sh $SOLR_URL
CHECK_SOLR_RETURN=$? CHECK_SOLR_RETURN=$?
if [ $CHECK_SOLR_RETURN == 1 ]; then if [ $CHECK_SOLR_RETURN == 1 ]; then
echo "Connecting to solr..." echo "Connecting to Solr..."
python3 solr_api.py -u $SOLR_URL -c $SOLR_COLLECTION -s $NUM_SHARDS -rf $RF -ms $MAX_SHARDS_PER_NODE &
# python3 manage.py rebuild_index --noinput &
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 &
else else
echo "Solr is offline, not possible to connect." echo "Solr is offline, not possible to connect."
fi fi
else else
echo "Suporte a SOLR não inicializado." echo "Solr support is not initialized."
fi fi
echo "Criando usuário admin..." echo "Creating admin user..."
user_created=$(python3 create_admin.py 2>&1) user_created=$(python3 create_admin.py 2>&1)

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

8
docker/check_solr.sh → docker/wait-for-solr.sh

@ -4,10 +4,10 @@
SOLR_URL=$1 SOLR_URL=$1
RETRY_COUNT=1 RETRY_COUNT=0
RETRY_LIMIT=4 RETRY_LIMIT=60 # wait until 1 min
echo "Waiting for solr connection at $SOLR_URL ..." echo "Waiting for Solr connection at $SOLR_URL ..."
while [[ $RETRY_COUNT < $RETRY_LIMIT ]]; do while [[ $RETRY_COUNT < $RETRY_LIMIT ]]; do
echo "Attempt to connect to solr: $RETRY_COUNT of $RETRY_LIMIT" echo "Attempt to connect to solr: $RETRY_COUNT of $RETRY_LIMIT"
let RETRY_COUNT=RETRY_COUNT+1; let RETRY_COUNT=RETRY_COUNT+1;
@ -18,7 +18,7 @@ while [[ $RETRY_COUNT < $RETRY_LIMIT ]]; do
echo "Solr server is up!" echo "Solr server is up!"
exit 1 exit 1
else else
sleep 3 sleep 1
fi fi
done done
echo "Solr connection failed." echo "Solr connection failed."

4
docs/instalacao31.rst

@ -221,10 +221,10 @@ Frontend do SAPL
Preparação do ambiente:: Preparação do ambiente::
---------------------- ----------------------
* **Instalação do NodeJs LTS 10.15.x**:: * **Instalação do NodeJs LTS 14.x**::
sudo apt-get install curl sudo apt-get install curl
curl -sL https://deb.nodesource.com/setup_10.x | sudo -E bash - curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash -
sudo apt-get install -y nodejs sudo apt-get install -y nodejs
* **Instalação do Yarn**:: * **Instalação do Yarn**::

2
docs/solr.rst

@ -7,7 +7,7 @@ Instruções para instalar o Solr
Solr é uma plataforma open source de indexação e busca textual utilizada pelo SAPL 3.1 para indexar documentos (normas jurídicas, matérias legislativas e documentos acessórios). Solr é uma plataforma open source de indexação e busca textual utilizada pelo SAPL 3.1 para indexar documentos (normas jurídicas, matérias legislativas e documentos acessórios).
Observação: Se a execução do SAPL for mediante containers Docker então use o arquivo *docker-compose.yml* disponível em Observação: Se a execução do SAPL for mediante containers Docker então use o arquivo *docker-compose.yml* disponível em
*https://github.com/interlegis/sapl/blob/3.1.x/solr/docker-compose.yml* (verifique os mapeamentos de volume estão corretos, a verso do SAPL referenciada no arquivo docker-compose.yml, e realize o backup de seu BD **antes** de qualquer tentativa de substituição do arquivo *docker-compose.yml* em uso corrente); *https://github.com/interlegis/sapl/blob/3.1.x/dist/docker-compose.yml* (verifique os mapeamentos de volume estão corretos, a verso do SAPL referenciada no arquivo docker-compose.yml, e realize o backup de seu BD **antes** de qualquer tentativa de substituição do arquivo *docker-compose.yml* em uso corrente);
1) Faça o download da distribuição *binária* do Apache Solr do site oficial do projeto **http://lucene.apache.org/solr** 1) Faça o download da distribuição *binária* do Apache Solr do site oficial do projeto **http://lucene.apache.org/solr**

BIN
frontend/public/img/arrow.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 B

BIN
frontend/public/img/authenticated.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

BIN
frontend/public/img/avatar.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

BIN
frontend/public/img/beta.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

BIN
frontend/public/img/brasao_transp.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

BIN
frontend/public/img/down_arrow_select.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 682 B

BIN
frontend/public/img/etiqueta.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 694 B

BIN
frontend/public/img/favicon.ico

Binary file not shown.

Before

Width:  |  Height:  |  Size: 975 B

BIN
frontend/public/img/file.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1021 B

BIN
frontend/public/img/hand-note.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 502 B

BIN
frontend/public/img/icon_comissoes.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

BIN
frontend/public/img/icon_delete_white.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

BIN
frontend/public/img/icon_materia_legislativa.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

BIN
frontend/public/img/icon_mesa_diretora.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

BIN
frontend/public/img/icon_normas_juridicas.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

BIN
frontend/public/img/icon_parlamentares.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

BIN
frontend/public/img/icon_pautas.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

BIN
frontend/public/img/icon_plenarias.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

BIN
frontend/public/img/icon_relatorios.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

BIN
frontend/public/img/icon_save_white.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

BIN
frontend/public/img/lexml.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 568 B

BIN
frontend/public/img/logo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

BIN
frontend/public/img/logo_cc.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

BIN
frontend/public/img/logo_interlegis.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

BIN
frontend/public/img/manual.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 343 B

BIN
frontend/public/img/pdflogo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 238 KiB

BIN
frontend/public/img/perfil.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

BIN
frontend/public/img/search-gray.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

BIN
frontend/public/img/search.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 367 B

BIN
frontend/public/img/user.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 290 B

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

@ -245,7 +245,7 @@ window.DispositivoEdit = function () {
if (editortype !== 'construct') { if (editortype !== 'construct') {
dpt_form.html(data) dpt_form.html(data)
if (editortype === 'tinymce') { if (editortype === 'tinymce') {
window.initTextRichEditor() window.initTextRichEditor(null, false, true)
} }
// OptionalCustomFrontEnd().init() // OptionalCustomFrontEnd().init()
} }
@ -431,9 +431,9 @@ window.DispositivoEdit = function () {
const form_data = { const form_data = {
csrfmiddlewaretoken: this.csrfmiddlewaretoken.value, csrfmiddlewaretoken: this.csrfmiddlewaretoken.value,
texto: texto, texto,
texto_atualizador: texto_atualizador, texto_atualizador,
visibilidade: visibilidade, visibilidade,
formtype: 'get_form_base' formtype: 'get_form_base'
} }

17
frontend/src/__apps/compilacao/scss/compilacao.scss

@ -367,6 +367,14 @@ a:link:after, a:visited:after {
max-width: 100%; max-width: 100%;
} }
} }
.dtxt {
display: inline;
:first-child {
display: inline !important;
}
}
.ementa { .ementa {
padding: 2em 0em 2em 35%; padding: 2em 0em 2em 35%;
font-weight: bold; font-weight: bold;
@ -424,6 +432,7 @@ a:link:after, a:visited:after {
float:left; float:left;
.dptt { .dptt {
position: relative; position: relative;
} }
} }
@ -490,7 +499,6 @@ a:link:after, a:visited:after {
} }
} }
} }
.card-header { .card-header {
font-size: 1.7rem; font-size: 1.7rem;
} }
@ -672,6 +680,9 @@ a:link:after, a:visited:after {
} }
} }
} }
} /* and dpt */ } /* and dpt */
.tipo-vigencias { .tipo-vigencias {
@ -1381,7 +1392,7 @@ a:link:after, a:visited:after {
&::before { &::before {
z-index: 20; z-index: 20;
position: absolute; position: absolute;
background: url(/static/img/icon_delete_white.png) no-repeat 50% 50%; background: url(@/assets/img/icon_delete_white.png) no-repeat 50% 50%;
content:""; content:"";
top: 0; top: 0;
left: 0; left: 0;
@ -1403,7 +1414,7 @@ a:link:after, a:visited:after {
color: white; color: white;
} }
&::before { &::before {
background: url(/static/img/icon_save_white.png) no-repeat 50% 50%; background: url(@/assets/img/icon_save_white.png) no-repeat 50% 50%;
} }
} }
span { span {

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

@ -1,37 +1,28 @@
import tinymce from 'tinymce'
import tinymce from 'tinymce/tinymce'
import './langs/pt_BR.js'
import 'tinymce/themes/silver' import 'tinymce/themes/silver'
import 'tinymce/icons/default' import 'tinymce/icons/default'
import 'tinymce/models/dom/index'
import 'tinymce/skins/ui/oxide/skin.min.css'
import 'tinymce/plugins/table'
import 'tinymce/plugins/lists'
import 'tinymce/plugins/code' import 'tinymce/plugins/code'
import 'tinymce/plugins/visualblocks' import 'tinymce/plugins/advlist'
import 'tinymce/plugins/link'
import 'tinymce/plugins/lists'
import 'tinymce/plugins/table'
import 'tinymce/skins/ui/oxide/skin.css' import './langs/pt_BR.js'
window.tinymce = tinymce window.tinymce = tinymce
window.removeTinymce = function () {
while (window.tinymce.editors.length > 0) {
window.tinymce.remove(window.tinymce.editors[0])
}
}
window.initTextRichEditor = function (elements, readonly = false) { window.initTextRichEditor = function (elements, readonly = false) {
window.removeTinymce()
const configTinymce = { const configTinymce = {
selector: elements === null || elements === undefined ? 'textarea' : elements, selector: elements === null || elements === undefined ? 'textarea' : elements,
forced_root_block: '',
min_height: 200,
language: 'pt_BR', language: 'pt_BR',
branding: false, branding: false,
content_css: 'default', forced_root_block: 'p',
plugins: ['lists table code visualblocks'], plugins: 'table lists advlist link code',
menubar: 'edit view format table tools', toolbar: 'undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link | code',
toolbar: 'undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent' menubar: 'file edit view insert format table'
} }
if (readonly) { if (readonly) {
configTinymce.readonly = 1 configTinymce.readonly = 1

32
frontend/src/__global/main.js

@ -1,32 +1,24 @@
// app - global
// é uma app fundamental para o layout do sapl tradicional.
// é importada pelo backend em seus templates
import '@fortawesome/fontawesome-free/css/all.css'
import 'bootstrap' import 'bootstrap'
import 'jquery-mask-plugin'
import 'webpack-jquery-ui/dialog' import 'jquery-ui/dist/jquery-ui'
import 'webpack-jquery-ui/sortable' import 'jquery-ui/ui/widgets/dialog'
import 'webpack-jquery-ui/datepicker' import 'jquery-ui/ui/widgets/sortable'
import 'webpack-jquery-ui/autocomplete' import 'jquery-ui/ui/widgets/datepicker'
import 'jquery-ui/ui/widgets/autocomplete'
import 'jquery-ui/ui/i18n/datepicker-pt-BR' import 'jquery-ui/ui/i18n/datepicker-pt-BR'
import 'jquery-ui-themes/themes/cupertino/jquery-ui.min.css' import * as moment from 'moment'
import 'moment/locale/pt-br'
import 'jquery-mask-plugin'
import './scss/app.scss'
import './js/tinymce' import './js/tinymce'
import './js/image_cropping' import './js/image_cropping'
import './js/functions' import './js/functions'
import './js/jquery.runner'
import * as moment from 'moment' import '@fortawesome/fontawesome-free/css/all.css'
import 'moment/locale/pt-br' import 'jquery-ui-themes/themes/cupertino/jquery-ui.min.css'
import './scss/app.scss'
// eslint-disable-next-line
require('imports-loader?window.jQuery=jquery!./js/jquery.runner.js')
window.$ = $ window.$ = $
window.jQuery = $ window.jQuery = $

38
frontend/src/__global/scss/_header.scss

@ -2,42 +2,8 @@
$blue: #02baf2 !default; $blue: #02baf2 !default;
$red: #f84545 !default; $red: #f84545 !default;
@import "~bootstrap/scss/functions"; @import "~bootstrap/scss/bootstrap";
@import "~bootstrap/scss/variables";
@import "~bootstrap/scss/mixins";
@import "~bootstrap/scss/reboot";
@import "~bootstrap/scss/type";
@import "~bootstrap/scss/images";
@import "~bootstrap/scss/code";
@import "~bootstrap/scss/grid";
@import "~bootstrap/scss/tables";
@import "~bootstrap/scss/forms";
@import "~bootstrap/scss/buttons";
@import "~bootstrap/scss/transitions";
@import "~bootstrap/scss/dropdown";
@import "~bootstrap/scss/button-group";
@import "~bootstrap/scss/input-group";
@import "~bootstrap/scss/custom-forms";
@import "~bootstrap/scss/nav";
@import "~bootstrap/scss/navbar";
@import "~bootstrap/scss/card";
@import "~bootstrap/scss/breadcrumb";
@import "~bootstrap/scss/pagination";
@import "~bootstrap/scss/badge";
@import "~bootstrap/scss/jumbotron";
@import "~bootstrap/scss/alert";
@import "~bootstrap/scss/progress";
@import "~bootstrap/scss/media";
@import "~bootstrap/scss/list-group";
@import "~bootstrap/scss/close";
@import "~bootstrap/scss/toasts";
@import "~bootstrap/scss/modal";
@import "~bootstrap/scss/tooltip";
@import "~bootstrap/scss/popover";
@import "~bootstrap/scss/carousel";
@import "~bootstrap/scss/spinners";
@import "~bootstrap/scss/utilities";
@import "~bootstrap/scss/print";
@each $color, $value in $theme-colors { @each $color, $value in $theme-colors {
.btn-outline-#{$color} { .btn-outline-#{$color} {

6
frontend/src/__global/scss/libs/bootstrap/_nav_navbar.scss

@ -1,3 +1,5 @@
@import "~bootstrap/scss/variables";
.navbar { .navbar {
padding: 0; padding: 0;
} }
@ -26,7 +28,7 @@
border-radius: 0; border-radius: 0;
} }
a { a {
padding: 0 $grid-gutter-width / 2; padding: 0 calc($grid-gutter-width / 2);
line-height: 2.3rem; line-height: 2.3rem;
display: block; display: block;
text-decoration: none; text-decoration: none;
@ -45,7 +47,7 @@
} }
} }
.search-form { .search-form {
padding: $grid-gutter-width / 3; padding: calc($grid-gutter-width / 3);
min-width: 20%;; min-width: 20%;;
} }
a:not([href]):not([tabindex]) { a:not([href]):not([tabindex]) {

0
frontend/public/audio/ring.mp3 → frontend/src/assets/audio/ring.mp3

808
frontend/webpack-stats.json

@ -1 +1,807 @@
{"status":"done","publicPath":"/static/sapl/frontend/","chunks":{"chunk-vendors":[{"name":"css/chunk-vendors.e8d8c6de.css","publicPath":"/static/sapl/frontend/css/chunk-vendors.e8d8c6de.css","path":"sapl/static/sapl/frontend/css/chunk-vendors.e8d8c6de.css"},{"name":"js/chunk-vendors.8a6bb3dd.js","publicPath":"/static/sapl/frontend/js/chunk-vendors.8a6bb3dd.js","path":"sapl/static/sapl/frontend/js/chunk-vendors.8a6bb3dd.js"},{"name":"css/chunk-vendors.e8d8c6de.css.map","publicPath":"/static/sapl/frontend/css/chunk-vendors.e8d8c6de.css.map","path":"sapl/static/sapl/frontend/css/chunk-vendors.e8d8c6de.css.map"},{"name":"js/chunk-vendors.8a6bb3dd.js.map","publicPath":"/static/sapl/frontend/js/chunk-vendors.8a6bb3dd.js.map","path":"sapl/static/sapl/frontend/js/chunk-vendors.8a6bb3dd.js.map"}],"compilacao":[{"name":"css/compilacao.90ba9ac3.css","publicPath":"/static/sapl/frontend/css/compilacao.90ba9ac3.css","path":"sapl/static/sapl/frontend/css/compilacao.90ba9ac3.css"},{"name":"js/compilacao.2659b00e.js","publicPath":"/static/sapl/frontend/js/compilacao.2659b00e.js","path":"sapl/static/sapl/frontend/js/compilacao.2659b00e.js"},{"name":"css/compilacao.90ba9ac3.css.map","publicPath":"/static/sapl/frontend/css/compilacao.90ba9ac3.css.map","path":"sapl/static/sapl/frontend/css/compilacao.90ba9ac3.css.map"},{"name":"js/compilacao.2659b00e.js.map","publicPath":"/static/sapl/frontend/js/compilacao.2659b00e.js.map","path":"sapl/static/sapl/frontend/js/compilacao.2659b00e.js.map"}],"global":[{"name":"css/global.80b7564c.css","publicPath":"/static/sapl/frontend/css/global.80b7564c.css","path":"sapl/static/sapl/frontend/css/global.80b7564c.css"},{"name":"js/global.9079a4fb.js","publicPath":"/static/sapl/frontend/js/global.9079a4fb.js","path":"sapl/static/sapl/frontend/js/global.9079a4fb.js"},{"name":"css/global.80b7564c.css.map","publicPath":"/static/sapl/frontend/css/global.80b7564c.css.map","path":"sapl/static/sapl/frontend/css/global.80b7564c.css.map"},{"name":"js/global.9079a4fb.js.map","publicPath":"/static/sapl/frontend/js/global.9079a4fb.js.map","path":"sapl/static/sapl/frontend/js/global.9079a4fb.js.map"}],"painel":[{"name":"css/painel.5d957a9b.css","publicPath":"/static/sapl/frontend/css/painel.5d957a9b.css","path":"sapl/static/sapl/frontend/css/painel.5d957a9b.css"},{"name":"js/painel.37936654.js","publicPath":"/static/sapl/frontend/js/painel.37936654.js","path":"sapl/static/sapl/frontend/js/painel.37936654.js"},{"name":"css/painel.5d957a9b.css.map","publicPath":"/static/sapl/frontend/css/painel.5d957a9b.css.map","path":"sapl/static/sapl/frontend/css/painel.5d957a9b.css.map"},{"name":"js/painel.37936654.js.map","publicPath":"/static/sapl/frontend/js/painel.37936654.js.map","path":"sapl/static/sapl/frontend/js/painel.37936654.js.map"}],"parlamentar":[{"name":"css/parlamentar.0e433876.css","publicPath":"/static/sapl/frontend/css/parlamentar.0e433876.css","path":"sapl/static/sapl/frontend/css/parlamentar.0e433876.css"},{"name":"js/parlamentar.84997ad7.js","publicPath":"/static/sapl/frontend/js/parlamentar.84997ad7.js","path":"sapl/static/sapl/frontend/js/parlamentar.84997ad7.js"},{"name":"css/parlamentar.0e433876.css.map","publicPath":"/static/sapl/frontend/css/parlamentar.0e433876.css.map","path":"sapl/static/sapl/frontend/css/parlamentar.0e433876.css.map"},{"name":"js/parlamentar.84997ad7.js.map","publicPath":"/static/sapl/frontend/js/parlamentar.84997ad7.js.map","path":"sapl/static/sapl/frontend/js/parlamentar.84997ad7.js.map"}]}} {
"status": "done",
"assets": {
"fonts/fa-brands-400.86c7e1fa.woff2": {
"name": "fonts/fa-brands-400.86c7e1fa.woff2",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/fonts/fa-brands-400.86c7e1fa.woff2",
"publicPath": "/static/sapl/frontend/fonts/fa-brands-400.86c7e1fa.woff2"
},
"fonts/fa-brands-400.f5defc2e.ttf": {
"name": "fonts/fa-brands-400.f5defc2e.ttf",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/fonts/fa-brands-400.f5defc2e.ttf",
"publicPath": "/static/sapl/frontend/fonts/fa-brands-400.f5defc2e.ttf"
},
"fonts/fa-regular-400.e0550912.woff2": {
"name": "fonts/fa-regular-400.e0550912.woff2",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/fonts/fa-regular-400.e0550912.woff2",
"publicPath": "/static/sapl/frontend/fonts/fa-regular-400.e0550912.woff2"
},
"fonts/fa-regular-400.3edb9004.ttf": {
"name": "fonts/fa-regular-400.3edb9004.ttf",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/fonts/fa-regular-400.3edb9004.ttf",
"publicPath": "/static/sapl/frontend/fonts/fa-regular-400.3edb9004.ttf"
},
"fonts/fa-solid-900.64d5644d.woff2": {
"name": "fonts/fa-solid-900.64d5644d.woff2",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/fonts/fa-solid-900.64d5644d.woff2",
"publicPath": "/static/sapl/frontend/fonts/fa-solid-900.64d5644d.woff2"
},
"fonts/fa-solid-900.f418d876.ttf": {
"name": "fonts/fa-solid-900.f418d876.ttf",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/fonts/fa-solid-900.f418d876.ttf",
"publicPath": "/static/sapl/frontend/fonts/fa-solid-900.f418d876.ttf"
},
"fonts/fa-v4compatibility.7e7e1dad.ttf": {
"name": "fonts/fa-v4compatibility.7e7e1dad.ttf",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/fonts/fa-v4compatibility.7e7e1dad.ttf",
"publicPath": "/static/sapl/frontend/fonts/fa-v4compatibility.7e7e1dad.ttf"
},
"css/global.45591136.css": {
"name": "css/global.45591136.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/css/global.45591136.css",
"publicPath": "/static/sapl/frontend/css/global.45591136.css"
},
"js/global.babaa14f.js": {
"name": "js/global.babaa14f.js",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/global.babaa14f.js",
"publicPath": "/static/sapl/frontend/js/global.babaa14f.js"
},
"css/parlamentar.cd5dc5a8.css": {
"name": "css/parlamentar.cd5dc5a8.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/css/parlamentar.cd5dc5a8.css",
"publicPath": "/static/sapl/frontend/css/parlamentar.cd5dc5a8.css"
},
"js/parlamentar.25e7f0fa.js": {
"name": "js/parlamentar.25e7f0fa.js",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/parlamentar.25e7f0fa.js",
"publicPath": "/static/sapl/frontend/js/parlamentar.25e7f0fa.js"
},
"css/painel.e2b9504e.css": {
"name": "css/painel.e2b9504e.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/css/painel.e2b9504e.css",
"publicPath": "/static/sapl/frontend/css/painel.e2b9504e.css"
},
"js/painel.7aa779e9.js": {
"name": "js/painel.7aa779e9.js",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/painel.7aa779e9.js",
"publicPath": "/static/sapl/frontend/js/painel.7aa779e9.js"
},
"css/compilacao.991aa842.css": {
"name": "css/compilacao.991aa842.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/css/compilacao.991aa842.css",
"publicPath": "/static/sapl/frontend/css/compilacao.991aa842.css"
},
"js/compilacao.1c9473f1.js": {
"name": "js/compilacao.1c9473f1.js",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/compilacao.1c9473f1.js",
"publicPath": "/static/sapl/frontend/js/compilacao.1c9473f1.js"
},
"css/chunk-vendors.9904f9d0.css": {
"name": "css/chunk-vendors.9904f9d0.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/css/chunk-vendors.9904f9d0.css",
"publicPath": "/static/sapl/frontend/css/chunk-vendors.9904f9d0.css"
},
"js/chunk-vendors.874df7f4.js": {
"name": "js/chunk-vendors.874df7f4.js",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/chunk-vendors.874df7f4.js",
"publicPath": "/static/sapl/frontend/js/chunk-vendors.874df7f4.js"
},
"audio/ring.mp3": {
"name": "audio/ring.mp3",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/audio/ring.mp3",
"publicPath": "/static/sapl/frontend/audio/ring.mp3"
},
"img/arrow.png": {
"name": "img/arrow.png",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/arrow.png",
"publicPath": "/static/sapl/frontend/img/arrow.png"
},
"img/authenticated.png": {
"name": "img/authenticated.png",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/authenticated.png",
"publicPath": "/static/sapl/frontend/img/authenticated.png"
},
"img/avatar.png": {
"name": "img/avatar.png",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/avatar.png",
"publicPath": "/static/sapl/frontend/img/avatar.png"
},
"img/beta.png": {
"name": "img/beta.png",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/beta.png",
"publicPath": "/static/sapl/frontend/img/beta.png"
},
"img/bg.png": {
"name": "img/bg.png",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/bg.png",
"publicPath": "/static/sapl/frontend/img/bg.png"
},
"img/brasao_transp.gif": {
"name": "img/brasao_transp.gif",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/brasao_transp.gif",
"publicPath": "/static/sapl/frontend/img/brasao_transp.gif"
},
"img/down_arrow_select.jpg": {
"name": "img/down_arrow_select.jpg",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/down_arrow_select.jpg",
"publicPath": "/static/sapl/frontend/img/down_arrow_select.jpg"
},
"img/etiqueta.png": {
"name": "img/etiqueta.png",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/etiqueta.png",
"publicPath": "/static/sapl/frontend/img/etiqueta.png"
},
"img/favicon.ico": {
"name": "img/favicon.ico",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/favicon.ico",
"publicPath": "/static/sapl/frontend/img/favicon.ico"
},
"img/file.png": {
"name": "img/file.png",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/file.png",
"publicPath": "/static/sapl/frontend/img/file.png"
},
"img/hand-note.png": {
"name": "img/hand-note.png",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/hand-note.png",
"publicPath": "/static/sapl/frontend/img/hand-note.png"
},
"img/icon_comissoes.png": {
"name": "img/icon_comissoes.png",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/icon_comissoes.png",
"publicPath": "/static/sapl/frontend/img/icon_comissoes.png"
},
"img/icon_delete_white.png": {
"name": "img/icon_delete_white.png",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/icon_delete_white.png",
"publicPath": "/static/sapl/frontend/img/icon_delete_white.png"
},
"img/icon_materia_legislativa.png": {
"name": "img/icon_materia_legislativa.png",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/icon_materia_legislativa.png",
"publicPath": "/static/sapl/frontend/img/icon_materia_legislativa.png"
},
"img/icon_mesa_diretora.png": {
"name": "img/icon_mesa_diretora.png",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/icon_mesa_diretora.png",
"publicPath": "/static/sapl/frontend/img/icon_mesa_diretora.png"
},
"img/icon_normas_juridicas.png": {
"name": "img/icon_normas_juridicas.png",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/icon_normas_juridicas.png",
"publicPath": "/static/sapl/frontend/img/icon_normas_juridicas.png"
},
"img/icon_normas_juridicas_destaque.png": {
"name": "img/icon_normas_juridicas_destaque.png",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/icon_normas_juridicas_destaque.png",
"publicPath": "/static/sapl/frontend/img/icon_normas_juridicas_destaque.png"
},
"img/icon_parlamentares.png": {
"name": "img/icon_parlamentares.png",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/icon_parlamentares.png",
"publicPath": "/static/sapl/frontend/img/icon_parlamentares.png"
},
"img/icon_pautas.png": {
"name": "img/icon_pautas.png",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/icon_pautas.png",
"publicPath": "/static/sapl/frontend/img/icon_pautas.png"
},
"img/icon_plenarias.png": {
"name": "img/icon_plenarias.png",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/icon_plenarias.png",
"publicPath": "/static/sapl/frontend/img/icon_plenarias.png"
},
"img/icon_relatorios.png": {
"name": "img/icon_relatorios.png",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/icon_relatorios.png",
"publicPath": "/static/sapl/frontend/img/icon_relatorios.png"
},
"img/icon_save_white.png": {
"name": "img/icon_save_white.png",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/icon_save_white.png",
"publicPath": "/static/sapl/frontend/img/icon_save_white.png"
},
"img/lexml.gif": {
"name": "img/lexml.gif",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/lexml.gif",
"publicPath": "/static/sapl/frontend/img/lexml.gif"
},
"img/logo.png": {
"name": "img/logo.png",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/logo.png",
"publicPath": "/static/sapl/frontend/img/logo.png"
},
"img/logo_cc.png": {
"name": "img/logo_cc.png",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/logo_cc.png",
"publicPath": "/static/sapl/frontend/img/logo_cc.png"
},
"img/logo_interlegis.png": {
"name": "img/logo_interlegis.png",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/logo_interlegis.png",
"publicPath": "/static/sapl/frontend/img/logo_interlegis.png"
},
"img/manual.png": {
"name": "img/manual.png",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/manual.png",
"publicPath": "/static/sapl/frontend/img/manual.png"
},
"img/pdflogo.png": {
"name": "img/pdflogo.png",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/pdflogo.png",
"publicPath": "/static/sapl/frontend/img/pdflogo.png"
},
"img/perfil.png": {
"name": "img/perfil.png",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/perfil.png",
"publicPath": "/static/sapl/frontend/img/perfil.png"
},
"img/search-gray.png": {
"name": "img/search-gray.png",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/search-gray.png",
"publicPath": "/static/sapl/frontend/img/search-gray.png"
},
"img/search.png": {
"name": "img/search.png",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/search.png",
"publicPath": "/static/sapl/frontend/img/search.png"
},
"img/user.png": {
"name": "img/user.png",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/user.png",
"publicPath": "/static/sapl/frontend/img/user.png"
},
"js/skins/content/dark/content.css": {
"name": "js/skins/content/dark/content.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/dark/content.css",
"publicPath": "/static/sapl/frontend/js/skins/content/dark/content.css"
},
"js/skins/content/dark/content.min.css": {
"name": "js/skins/content/dark/content.min.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/dark/content.min.css",
"publicPath": "/static/sapl/frontend/js/skins/content/dark/content.min.css"
},
"js/skins/content/default/content.css": {
"name": "js/skins/content/default/content.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/default/content.css",
"publicPath": "/static/sapl/frontend/js/skins/content/default/content.css"
},
"js/skins/content/default/content.min.css": {
"name": "js/skins/content/default/content.min.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/default/content.min.css",
"publicPath": "/static/sapl/frontend/js/skins/content/default/content.min.css"
},
"js/skins/content/document/content.css": {
"name": "js/skins/content/document/content.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/document/content.css",
"publicPath": "/static/sapl/frontend/js/skins/content/document/content.css"
},
"js/skins/content/document/content.min.css": {
"name": "js/skins/content/document/content.min.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/document/content.min.css",
"publicPath": "/static/sapl/frontend/js/skins/content/document/content.min.css"
},
"js/skins/content/tinymce-5/content.css": {
"name": "js/skins/content/tinymce-5/content.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/tinymce-5/content.css",
"publicPath": "/static/sapl/frontend/js/skins/content/tinymce-5/content.css"
},
"js/skins/content/tinymce-5/content.min.css": {
"name": "js/skins/content/tinymce-5/content.min.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/tinymce-5/content.min.css",
"publicPath": "/static/sapl/frontend/js/skins/content/tinymce-5/content.min.css"
},
"js/skins/content/tinymce-5-dark/content.css": {
"name": "js/skins/content/tinymce-5-dark/content.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/tinymce-5-dark/content.css",
"publicPath": "/static/sapl/frontend/js/skins/content/tinymce-5-dark/content.css"
},
"js/skins/content/tinymce-5-dark/content.min.css": {
"name": "js/skins/content/tinymce-5-dark/content.min.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/tinymce-5-dark/content.min.css",
"publicPath": "/static/sapl/frontend/js/skins/content/tinymce-5-dark/content.min.css"
},
"js/skins/content/writer/content.css": {
"name": "js/skins/content/writer/content.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/writer/content.css",
"publicPath": "/static/sapl/frontend/js/skins/content/writer/content.css"
},
"js/skins/content/writer/content.min.css": {
"name": "js/skins/content/writer/content.min.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/writer/content.min.css",
"publicPath": "/static/sapl/frontend/js/skins/content/writer/content.min.css"
},
"js/skins/ui/oxide/content.css": {
"name": "js/skins/ui/oxide/content.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/content.css",
"publicPath": "/static/sapl/frontend/js/skins/ui/oxide/content.css"
},
"js/skins/ui/oxide/content.inline.css": {
"name": "js/skins/ui/oxide/content.inline.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/content.inline.css",
"publicPath": "/static/sapl/frontend/js/skins/ui/oxide/content.inline.css"
},
"js/skins/ui/oxide/content.inline.min.css": {
"name": "js/skins/ui/oxide/content.inline.min.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/content.inline.min.css",
"publicPath": "/static/sapl/frontend/js/skins/ui/oxide/content.inline.min.css"
},
"js/skins/ui/oxide/content.min.css": {
"name": "js/skins/ui/oxide/content.min.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/content.min.css",
"publicPath": "/static/sapl/frontend/js/skins/ui/oxide/content.min.css"
},
"js/skins/ui/oxide/skin.css": {
"name": "js/skins/ui/oxide/skin.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/skin.css",
"publicPath": "/static/sapl/frontend/js/skins/ui/oxide/skin.css"
},
"js/skins/ui/oxide/skin.min.css": {
"name": "js/skins/ui/oxide/skin.min.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/skin.min.css",
"publicPath": "/static/sapl/frontend/js/skins/ui/oxide/skin.min.css"
},
"js/skins/ui/oxide/skin.shadowdom.css": {
"name": "js/skins/ui/oxide/skin.shadowdom.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/skin.shadowdom.css",
"publicPath": "/static/sapl/frontend/js/skins/ui/oxide/skin.shadowdom.css"
},
"js/skins/ui/oxide/skin.shadowdom.min.css": {
"name": "js/skins/ui/oxide/skin.shadowdom.min.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/skin.shadowdom.min.css",
"publicPath": "/static/sapl/frontend/js/skins/ui/oxide/skin.shadowdom.min.css"
},
"js/skins/ui/oxide-dark/content.css": {
"name": "js/skins/ui/oxide-dark/content.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/content.css",
"publicPath": "/static/sapl/frontend/js/skins/ui/oxide-dark/content.css"
},
"js/skins/ui/oxide-dark/content.inline.css": {
"name": "js/skins/ui/oxide-dark/content.inline.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/content.inline.css",
"publicPath": "/static/sapl/frontend/js/skins/ui/oxide-dark/content.inline.css"
},
"js/skins/ui/oxide-dark/content.inline.min.css": {
"name": "js/skins/ui/oxide-dark/content.inline.min.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/content.inline.min.css",
"publicPath": "/static/sapl/frontend/js/skins/ui/oxide-dark/content.inline.min.css"
},
"js/skins/ui/oxide-dark/content.min.css": {
"name": "js/skins/ui/oxide-dark/content.min.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/content.min.css",
"publicPath": "/static/sapl/frontend/js/skins/ui/oxide-dark/content.min.css"
},
"js/skins/ui/oxide-dark/skin.css": {
"name": "js/skins/ui/oxide-dark/skin.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/skin.css",
"publicPath": "/static/sapl/frontend/js/skins/ui/oxide-dark/skin.css"
},
"js/skins/ui/oxide-dark/skin.min.css": {
"name": "js/skins/ui/oxide-dark/skin.min.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/skin.min.css",
"publicPath": "/static/sapl/frontend/js/skins/ui/oxide-dark/skin.min.css"
},
"js/skins/ui/oxide-dark/skin.shadowdom.css": {
"name": "js/skins/ui/oxide-dark/skin.shadowdom.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/skin.shadowdom.css",
"publicPath": "/static/sapl/frontend/js/skins/ui/oxide-dark/skin.shadowdom.css"
},
"js/skins/ui/oxide-dark/skin.shadowdom.min.css": {
"name": "js/skins/ui/oxide-dark/skin.shadowdom.min.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/skin.shadowdom.min.css",
"publicPath": "/static/sapl/frontend/js/skins/ui/oxide-dark/skin.shadowdom.min.css"
},
"js/skins/ui/tinymce-5/content.css": {
"name": "js/skins/ui/tinymce-5/content.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/content.css",
"publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5/content.css"
},
"js/skins/ui/tinymce-5/content.inline.css": {
"name": "js/skins/ui/tinymce-5/content.inline.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/content.inline.css",
"publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5/content.inline.css"
},
"js/skins/ui/tinymce-5/content.inline.min.css": {
"name": "js/skins/ui/tinymce-5/content.inline.min.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/content.inline.min.css",
"publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5/content.inline.min.css"
},
"js/skins/ui/tinymce-5/content.min.css": {
"name": "js/skins/ui/tinymce-5/content.min.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/content.min.css",
"publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5/content.min.css"
},
"js/skins/ui/tinymce-5/skin.css": {
"name": "js/skins/ui/tinymce-5/skin.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/skin.css",
"publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5/skin.css"
},
"js/skins/ui/tinymce-5/skin.min.css": {
"name": "js/skins/ui/tinymce-5/skin.min.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/skin.min.css",
"publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5/skin.min.css"
},
"js/skins/ui/tinymce-5/skin.shadowdom.css": {
"name": "js/skins/ui/tinymce-5/skin.shadowdom.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/skin.shadowdom.css",
"publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5/skin.shadowdom.css"
},
"js/skins/ui/tinymce-5/skin.shadowdom.min.css": {
"name": "js/skins/ui/tinymce-5/skin.shadowdom.min.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/skin.shadowdom.min.css",
"publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5/skin.shadowdom.min.css"
},
"js/skins/ui/tinymce-5-dark/content.css": {
"name": "js/skins/ui/tinymce-5-dark/content.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.css",
"publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.css"
},
"js/skins/ui/tinymce-5-dark/content.inline.css": {
"name": "js/skins/ui/tinymce-5-dark/content.inline.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.inline.css",
"publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.inline.css"
},
"js/skins/ui/tinymce-5-dark/content.inline.min.css": {
"name": "js/skins/ui/tinymce-5-dark/content.inline.min.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.inline.min.css",
"publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.inline.min.css"
},
"js/skins/ui/tinymce-5-dark/content.min.css": {
"name": "js/skins/ui/tinymce-5-dark/content.min.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.min.css",
"publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.min.css"
},
"js/skins/ui/tinymce-5-dark/skin.css": {
"name": "js/skins/ui/tinymce-5-dark/skin.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.css",
"publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.css"
},
"js/skins/ui/tinymce-5-dark/skin.min.css": {
"name": "js/skins/ui/tinymce-5-dark/skin.min.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.min.css",
"publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.min.css"
},
"js/skins/ui/tinymce-5-dark/skin.shadowdom.css": {
"name": "js/skins/ui/tinymce-5-dark/skin.shadowdom.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.shadowdom.css",
"publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.shadowdom.css"
},
"js/skins/ui/tinymce-5-dark/skin.shadowdom.min.css": {
"name": "js/skins/ui/tinymce-5-dark/skin.shadowdom.min.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.shadowdom.min.css",
"publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.shadowdom.min.css"
},
"js/chunk-vendors.874df7f4.js.LICENSE.txt": {
"name": "js/chunk-vendors.874df7f4.js.LICENSE.txt",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/chunk-vendors.874df7f4.js.LICENSE.txt",
"publicPath": "/static/sapl/frontend/js/chunk-vendors.874df7f4.js.LICENSE.txt"
},
"js/global.babaa14f.js.LICENSE.txt": {
"name": "js/global.babaa14f.js.LICENSE.txt",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/global.babaa14f.js.LICENSE.txt",
"publicPath": "/static/sapl/frontend/js/global.babaa14f.js.LICENSE.txt"
},
"fonts/fa-v4compatibility.7e7e1dad.ttf.gz": {
"name": "fonts/fa-v4compatibility.7e7e1dad.ttf.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/fonts/fa-v4compatibility.7e7e1dad.ttf.gz",
"publicPath": "/static/sapl/frontend/fonts/fa-v4compatibility.7e7e1dad.ttf.gz"
},
"js/parlamentar.25e7f0fa.js.gz": {
"name": "js/parlamentar.25e7f0fa.js.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/parlamentar.25e7f0fa.js.gz",
"publicPath": "/static/sapl/frontend/js/parlamentar.25e7f0fa.js.gz"
},
"css/painel.e2b9504e.css.gz": {
"name": "css/painel.e2b9504e.css.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/css/painel.e2b9504e.css.gz",
"publicPath": "/static/sapl/frontend/css/painel.e2b9504e.css.gz"
},
"js/painel.7aa779e9.js.gz": {
"name": "js/painel.7aa779e9.js.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/painel.7aa779e9.js.gz",
"publicPath": "/static/sapl/frontend/js/painel.7aa779e9.js.gz"
},
"css/compilacao.991aa842.css.gz": {
"name": "css/compilacao.991aa842.css.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/css/compilacao.991aa842.css.gz",
"publicPath": "/static/sapl/frontend/css/compilacao.991aa842.css.gz"
},
"js/compilacao.1c9473f1.js.gz": {
"name": "js/compilacao.1c9473f1.js.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/compilacao.1c9473f1.js.gz",
"publicPath": "/static/sapl/frontend/js/compilacao.1c9473f1.js.gz"
},
"js/global.babaa14f.js.gz": {
"name": "js/global.babaa14f.js.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/global.babaa14f.js.gz",
"publicPath": "/static/sapl/frontend/js/global.babaa14f.js.gz"
},
"img/down_arrow_select.jpg.gz": {
"name": "img/down_arrow_select.jpg.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/img/down_arrow_select.jpg.gz",
"publicPath": "/static/sapl/frontend/img/down_arrow_select.jpg.gz"
},
"js/skins/content/dark/content.css.gz": {
"name": "js/skins/content/dark/content.css.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/dark/content.css.gz",
"publicPath": "/static/sapl/frontend/js/skins/content/dark/content.css.gz"
},
"js/skins/content/dark/content.min.css.gz": {
"name": "js/skins/content/dark/content.min.css.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/dark/content.min.css.gz",
"publicPath": "/static/sapl/frontend/js/skins/content/dark/content.min.css.gz"
},
"js/skins/content/default/content.css.gz": {
"name": "js/skins/content/default/content.css.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/default/content.css.gz",
"publicPath": "/static/sapl/frontend/js/skins/content/default/content.css.gz"
},
"js/skins/content/default/content.min.css.gz": {
"name": "js/skins/content/default/content.min.css.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/default/content.min.css.gz",
"publicPath": "/static/sapl/frontend/js/skins/content/default/content.min.css.gz"
},
"js/skins/content/document/content.css.gz": {
"name": "js/skins/content/document/content.css.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/document/content.css.gz",
"publicPath": "/static/sapl/frontend/js/skins/content/document/content.css.gz"
},
"js/skins/content/document/content.min.css.gz": {
"name": "js/skins/content/document/content.min.css.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/document/content.min.css.gz",
"publicPath": "/static/sapl/frontend/js/skins/content/document/content.min.css.gz"
},
"js/skins/content/tinymce-5/content.css.gz": {
"name": "js/skins/content/tinymce-5/content.css.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/tinymce-5/content.css.gz",
"publicPath": "/static/sapl/frontend/js/skins/content/tinymce-5/content.css.gz"
},
"js/skins/content/tinymce-5/content.min.css.gz": {
"name": "js/skins/content/tinymce-5/content.min.css.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/tinymce-5/content.min.css.gz",
"publicPath": "/static/sapl/frontend/js/skins/content/tinymce-5/content.min.css.gz"
},
"js/skins/content/tinymce-5-dark/content.css.gz": {
"name": "js/skins/content/tinymce-5-dark/content.css.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/tinymce-5-dark/content.css.gz",
"publicPath": "/static/sapl/frontend/js/skins/content/tinymce-5-dark/content.css.gz"
},
"js/skins/content/tinymce-5-dark/content.min.css.gz": {
"name": "js/skins/content/tinymce-5-dark/content.min.css.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/tinymce-5-dark/content.min.css.gz",
"publicPath": "/static/sapl/frontend/js/skins/content/tinymce-5-dark/content.min.css.gz"
},
"js/skins/content/writer/content.css.gz": {
"name": "js/skins/content/writer/content.css.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/writer/content.css.gz",
"publicPath": "/static/sapl/frontend/js/skins/content/writer/content.css.gz"
},
"js/skins/content/writer/content.min.css.gz": {
"name": "js/skins/content/writer/content.min.css.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/content/writer/content.min.css.gz",
"publicPath": "/static/sapl/frontend/js/skins/content/writer/content.min.css.gz"
},
"js/skins/ui/oxide/content.inline.css.gz": {
"name": "js/skins/ui/oxide/content.inline.css.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/content.inline.css.gz",
"publicPath": "/static/sapl/frontend/js/skins/ui/oxide/content.inline.css.gz"
},
"js/skins/ui/oxide/content.inline.min.css.gz": {
"name": "js/skins/ui/oxide/content.inline.min.css.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/content.inline.min.css.gz",
"publicPath": "/static/sapl/frontend/js/skins/ui/oxide/content.inline.min.css.gz"
},
"js/skins/ui/oxide/content.min.css.gz": {
"name": "js/skins/ui/oxide/content.min.css.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/content.min.css.gz",
"publicPath": "/static/sapl/frontend/js/skins/ui/oxide/content.min.css.gz"
},
"js/skins/ui/oxide/skin.shadowdom.css.gz": {
"name": "js/skins/ui/oxide/skin.shadowdom.css.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/skin.shadowdom.css.gz",
"publicPath": "/static/sapl/frontend/js/skins/ui/oxide/skin.shadowdom.css.gz"
},
"js/skins/ui/oxide/skin.shadowdom.min.css.gz": {
"name": "js/skins/ui/oxide/skin.shadowdom.min.css.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/skin.shadowdom.min.css.gz",
"publicPath": "/static/sapl/frontend/js/skins/ui/oxide/skin.shadowdom.min.css.gz"
},
"js/skins/ui/oxide-dark/content.css.gz": {
"name": "js/skins/ui/oxide-dark/content.css.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/content.css.gz",
"publicPath": "/static/sapl/frontend/js/skins/ui/oxide-dark/content.css.gz"
},
"js/skins/ui/oxide-dark/content.inline.css.gz": {
"name": "js/skins/ui/oxide-dark/content.inline.css.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/content.inline.css.gz",
"publicPath": "/static/sapl/frontend/js/skins/ui/oxide-dark/content.inline.css.gz"
},
"js/skins/ui/oxide-dark/content.inline.min.css.gz": {
"name": "js/skins/ui/oxide-dark/content.inline.min.css.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/content.inline.min.css.gz",
"publicPath": "/static/sapl/frontend/js/skins/ui/oxide-dark/content.inline.min.css.gz"
},
"js/skins/ui/oxide-dark/content.min.css.gz": {
"name": "js/skins/ui/oxide-dark/content.min.css.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/content.min.css.gz",
"publicPath": "/static/sapl/frontend/js/skins/ui/oxide-dark/content.min.css.gz"
},
"js/skins/ui/oxide/skin.css.gz": {
"name": "js/skins/ui/oxide/skin.css.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/skin.css.gz",
"publicPath": "/static/sapl/frontend/js/skins/ui/oxide/skin.css.gz"
},
"js/skins/ui/oxide-dark/skin.css.gz": {
"name": "js/skins/ui/oxide-dark/skin.css.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/skin.css.gz",
"publicPath": "/static/sapl/frontend/js/skins/ui/oxide-dark/skin.css.gz"
},
"js/skins/ui/oxide-dark/skin.shadowdom.css.gz": {
"name": "js/skins/ui/oxide-dark/skin.shadowdom.css.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/skin.shadowdom.css.gz",
"publicPath": "/static/sapl/frontend/js/skins/ui/oxide-dark/skin.shadowdom.css.gz"
},
"js/skins/ui/oxide-dark/skin.shadowdom.min.css.gz": {
"name": "js/skins/ui/oxide-dark/skin.shadowdom.min.css.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/skin.shadowdom.min.css.gz",
"publicPath": "/static/sapl/frontend/js/skins/ui/oxide-dark/skin.shadowdom.min.css.gz"
},
"js/skins/ui/oxide-dark/skin.min.css.gz": {
"name": "js/skins/ui/oxide-dark/skin.min.css.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide-dark/skin.min.css.gz",
"publicPath": "/static/sapl/frontend/js/skins/ui/oxide-dark/skin.min.css.gz"
},
"js/skins/ui/tinymce-5/content.css.gz": {
"name": "js/skins/ui/tinymce-5/content.css.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/content.css.gz",
"publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5/content.css.gz"
},
"js/skins/ui/tinymce-5/content.inline.css.gz": {
"name": "js/skins/ui/tinymce-5/content.inline.css.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/content.inline.css.gz",
"publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5/content.inline.css.gz"
},
"js/skins/ui/tinymce-5/content.inline.min.css.gz": {
"name": "js/skins/ui/tinymce-5/content.inline.min.css.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/content.inline.min.css.gz",
"publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5/content.inline.min.css.gz"
},
"js/skins/ui/tinymce-5/content.min.css.gz": {
"name": "js/skins/ui/tinymce-5/content.min.css.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/content.min.css.gz",
"publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5/content.min.css.gz"
},
"js/skins/ui/oxide/content.css.gz": {
"name": "js/skins/ui/oxide/content.css.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/content.css.gz",
"publicPath": "/static/sapl/frontend/js/skins/ui/oxide/content.css.gz"
},
"js/skins/ui/tinymce-5/skin.shadowdom.css.gz": {
"name": "js/skins/ui/tinymce-5/skin.shadowdom.css.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/skin.shadowdom.css.gz",
"publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5/skin.shadowdom.css.gz"
},
"js/skins/ui/tinymce-5/skin.shadowdom.min.css.gz": {
"name": "js/skins/ui/tinymce-5/skin.shadowdom.min.css.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/skin.shadowdom.min.css.gz",
"publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5/skin.shadowdom.min.css.gz"
},
"js/skins/ui/tinymce-5-dark/content.css.gz": {
"name": "js/skins/ui/tinymce-5-dark/content.css.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.css.gz",
"publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.css.gz"
},
"js/skins/ui/tinymce-5-dark/content.inline.css.gz": {
"name": "js/skins/ui/tinymce-5-dark/content.inline.css.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.inline.css.gz",
"publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.inline.css.gz"
},
"js/skins/ui/tinymce-5-dark/content.inline.min.css.gz": {
"name": "js/skins/ui/tinymce-5-dark/content.inline.min.css.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.inline.min.css.gz",
"publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.inline.min.css.gz"
},
"js/skins/ui/oxide/skin.min.css.gz": {
"name": "js/skins/ui/oxide/skin.min.css.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/oxide/skin.min.css.gz",
"publicPath": "/static/sapl/frontend/js/skins/ui/oxide/skin.min.css.gz"
},
"js/skins/ui/tinymce-5-dark/content.min.css.gz": {
"name": "js/skins/ui/tinymce-5-dark/content.min.css.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.min.css.gz",
"publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5-dark/content.min.css.gz"
},
"js/skins/ui/tinymce-5/skin.css.gz": {
"name": "js/skins/ui/tinymce-5/skin.css.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/skin.css.gz",
"publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5/skin.css.gz"
},
"js/skins/ui/tinymce-5-dark/skin.shadowdom.css.gz": {
"name": "js/skins/ui/tinymce-5-dark/skin.shadowdom.css.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.shadowdom.css.gz",
"publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.shadowdom.css.gz"
},
"js/skins/ui/tinymce-5-dark/skin.shadowdom.min.css.gz": {
"name": "js/skins/ui/tinymce-5-dark/skin.shadowdom.min.css.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.shadowdom.min.css.gz",
"publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.shadowdom.min.css.gz"
},
"js/chunk-vendors.874df7f4.js.LICENSE.txt.gz": {
"name": "js/chunk-vendors.874df7f4.js.LICENSE.txt.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/chunk-vendors.874df7f4.js.LICENSE.txt.gz",
"publicPath": "/static/sapl/frontend/js/chunk-vendors.874df7f4.js.LICENSE.txt.gz"
},
"js/skins/ui/tinymce-5-dark/skin.css.gz": {
"name": "js/skins/ui/tinymce-5-dark/skin.css.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.css.gz",
"publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.css.gz"
},
"js/skins/ui/tinymce-5-dark/skin.min.css.gz": {
"name": "js/skins/ui/tinymce-5-dark/skin.min.css.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.min.css.gz",
"publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5-dark/skin.min.css.gz"
},
"fonts/fa-regular-400.3edb9004.ttf.gz": {
"name": "fonts/fa-regular-400.3edb9004.ttf.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/fonts/fa-regular-400.3edb9004.ttf.gz",
"publicPath": "/static/sapl/frontend/fonts/fa-regular-400.3edb9004.ttf.gz"
},
"css/global.45591136.css.gz": {
"name": "css/global.45591136.css.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/css/global.45591136.css.gz",
"publicPath": "/static/sapl/frontend/css/global.45591136.css.gz"
},
"js/skins/ui/tinymce-5/skin.min.css.gz": {
"name": "js/skins/ui/tinymce-5/skin.min.css.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/skins/ui/tinymce-5/skin.min.css.gz",
"publicPath": "/static/sapl/frontend/js/skins/ui/tinymce-5/skin.min.css.gz"
},
"css/chunk-vendors.9904f9d0.css.gz": {
"name": "css/chunk-vendors.9904f9d0.css.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/css/chunk-vendors.9904f9d0.css.gz",
"publicPath": "/static/sapl/frontend/css/chunk-vendors.9904f9d0.css.gz"
},
"fonts/fa-brands-400.f5defc2e.ttf.gz": {
"name": "fonts/fa-brands-400.f5defc2e.ttf.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/fonts/fa-brands-400.f5defc2e.ttf.gz",
"publicPath": "/static/sapl/frontend/fonts/fa-brands-400.f5defc2e.ttf.gz"
},
"fonts/fa-solid-900.f418d876.ttf.gz": {
"name": "fonts/fa-solid-900.f418d876.ttf.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/fonts/fa-solid-900.f418d876.ttf.gz",
"publicPath": "/static/sapl/frontend/fonts/fa-solid-900.f418d876.ttf.gz"
},
"js/chunk-vendors.874df7f4.js.gz": {
"name": "js/chunk-vendors.874df7f4.js.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/chunk-vendors.874df7f4.js.gz",
"publicPath": "/static/sapl/frontend/js/chunk-vendors.874df7f4.js.gz"
}
},
"chunks": {
"global": [
"css/chunk-vendors.9904f9d0.css",
"js/chunk-vendors.874df7f4.js",
"css/global.45591136.css",
"js/global.babaa14f.js"
],
"parlamentar": [
"css/chunk-vendors.9904f9d0.css",
"js/chunk-vendors.874df7f4.js",
"css/parlamentar.cd5dc5a8.css",
"js/parlamentar.25e7f0fa.js"
],
"painel": [
"css/chunk-vendors.9904f9d0.css",
"js/chunk-vendors.874df7f4.js",
"css/painel.e2b9504e.css",
"js/painel.7aa779e9.js"
],
"compilacao": [
"css/chunk-vendors.9904f9d0.css",
"js/chunk-vendors.874df7f4.js",
"css/compilacao.991aa842.css",
"js/compilacao.1c9473f1.js"
]
},
"publicPath": "/static/sapl/frontend/"
}

77
package.json

@ -8,50 +8,51 @@
"lint": "vue-cli-service lint" "lint": "vue-cli-service lint"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^5.13.0", "@fortawesome/fontawesome-free": "^6.1.2",
"axios": "^0.21.1", "axios": "^0.27.2",
"axios-progress-bar": "^1.2.0", "bootstrap": "^4.6.2",
"bootstrap": "^4.4.1", "bootstrap-vue": "^2.22.0",
"bootstrap-vue": "^2.12.0", "diff": "^5.1.0",
"diff": "^4.0.1", "jquery": "^3.6.0",
"dotenv": "^6.2.0",
"exports-loader": "^0.7.0",
"imports-loader": "^0.8.0",
"jquery": "^3.5.1",
"jquery-mask-plugin": "^1.14.16", "jquery-mask-plugin": "^1.14.16",
"jquery-ui": "^1.13.2",
"jquery-ui-themes": "^1.12.0", "jquery-ui-themes": "^1.12.0",
"lodash": "^4.17.19", "lodash": "^4.17.21",
"moment": "^2.24.0", "moment": "^2.29.4",
"moment-locales-webpack-plugin": "^1.1.2", "moment-locales-webpack-plugin": "^1.2.0",
"popper.js": "^1.16.1", "popper.js": "^1.16.1",
"serialize-javascript": "^3.1.0", "tinymce": "^6.1.2",
"terser": "^4.6.11", "vue": "^2.7.9"
"tinymce": "^5.6.2",
"vue": "^2.6.11",
"webpack": "^4.43.0",
"webpack-jquery-ui": "^2.0.1",
"websocket-extensions": "^0.1.4"
}, },
"devDependencies": { "devDependencies": {
"@vue/cli-plugin-babel": "^4.3.1", "@babel/core": "^7.18.13",
"@vue/cli-plugin-eslint": "^4.3.1", "@babel/eslint-parser": "^7.18.9",
"@vue/cli-service": "^4.3.1", "@vue/cli-plugin-babel": "^5.0.8",
"babel-eslint": "^10.1.0", "@vue/cli-service": "^5.0.8",
"compression-webpack-plugin": "^3.1.0", "compression-webpack-plugin": "^10.0.0",
"css-loader": "^3.5.2", "copy-webpack-plugin": "^11.0.0",
"eslint": "^6.8.0", "css-loader": "^6.7.1",
"eslint-config-standard": "^14.1.1", "dotenv": "^16.0.1",
"eslint": "^8.22.0",
"eslint-config-standard": "^17.0.0",
"eslint-friendly-formatter": "^4.0.1", "eslint-friendly-formatter": "^4.0.1",
"eslint-loader": "^4.0.0", "eslint-plugin-import": "^2.26.0",
"eslint-plugin-import": "^2.20.2", "eslint-plugin-n": "^15.2.5",
"eslint-plugin-node": "^11.1.0", "eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1", "eslint-plugin-promise": "^6.0.0",
"eslint-plugin-standard": "^4.0.1", "eslint-plugin-vue": "^9.3.0",
"eslint-plugin-vue": "^6.2.2", "eslint-webpack-plugin": "^3.2.0",
"node-sass": "^4.13.1", "html-webpack-plugin": "^5.5.0",
"sass-loader": "^8.0.2", "imports-loader": "^4.0.1",
"shelljs": "^0.8.4", "mini-css-extract-plugin": "^2.6.1",
"vue-template-compiler": "^2.6.11", "sass": "^1.54.5",
"webpack-bundle-tracker": "^0.4.3" "sass-loader": "^13.0.2",
"shelljs": "^0.8.5",
"style-loader": "^3.3.1",
"terser-webpack-plugin": "^5.3.5",
"uglify-js": "^3.17.0",
"vue-template-compiler": "^2.7.9",
"webpack": "^5.74.0",
"webpack-bundle-tracker": "^1.6.0"
} }
} }

15
release.sh

@ -30,17 +30,17 @@ FINAL_VERSION=
function change_files { function change_files {
OLD_VERSION=$(grep -E 'interlegis/sapl:'$VERSION_PATTERN docker/docker-compose.yml | cut -d':' -f3) OLD_VERSION=$(grep -E 'interlegis/sapl:'$VERSION_PATTERN docker/docker-compose.yaml | cut -d':' -f3)
echo "Atualizando de "$OLD_VERSION" para "$FINAL_VERSION echo "Atualizando de "$OLD_VERSION" para "$FINAL_VERSION
sed -E -i "s|$OLD_VERSION|$FINAL_VERSION|g" docker/docker-compose.yml 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" setup.py
sed -E -i "s|$OLD_VERSION|$FINAL_VERSION|g" sapl/templates/base.html sed -E -i "" "s|$OLD_VERSION|$FINAL_VERSION|g" sapl/templates/base.html
sed -E -i "s|$OLD_VERSION|$FINAL_VERSION|g" sapl/settings.py sed -E -i "" "s|$OLD_VERSION|$FINAL_VERSION|g" sapl/settings.py
} }
function set_major_version { function set_major_version {
@ -61,11 +61,14 @@ function set_rc_version {
fi fi
FINAL_VERSION=$NEXT_RC_VERSION FINAL_VERSION=$NEXT_RC_VERSION
echo "OLD_VERSION: $OLD_VERSION"
echo "FINAL_VERSION: $FINAL_VERSION"
} }
function commit_and_push { function commit_and_push {
echo "committing..." echo "committing..."
git add docker/docker-compose.yml setup.py sapl/settings.py sapl/templates/base.html git add docker/docker-compose.yaml setup.py sapl/settings.py sapl/templates/base.html
git commit -m "Release: $FINAL_VERSION" git commit -m "Release: $FINAL_VERSION"
git tag $FINAL_VERSION git tag $FINAL_VERSION

2
requirements/dev-requirements.txt

@ -2,7 +2,7 @@
autopep8==1.2.4 autopep8==1.2.4
beautifulsoup4==4.9.1 beautifulsoup4==4.9.1
django-debug-toolbar==1.8 django-debug-toolbar==1.11.1
ipdb==0.13.3 ipdb==0.13.3
pdbpp==0.9.2 pdbpp==0.9.2
pip-review==0.4 pip-review==0.4

19
requirements/requirements.txt

@ -1,10 +1,11 @@
django==2.2.18 django==2.2.28
django-haystack==2.8.1 django-haystack==3.1.1
django-filter==2.0.0 django-filter==2.4.0
djangorestframework==3.11.2 djangorestframework==3.12.4
dj-database-url==0.5.0 dj-database-url==0.5.0
django-braces==1.14.0 django-braces==1.14.0
django-crispy-forms==1.7.2 django-crispy-forms==1.7.2
django-contrib-postgres==0.0.1
django-floppyforms==1.8.0 django-floppyforms==1.8.0
django-extra-views==0.12.0 django-extra-views==0.12.0
django-model-utils==3.1.2 django-model-utils==3.1.2
@ -13,17 +14,18 @@ django-reversion-compare==0.8.6
django-speedinfo==1.4.0 django-speedinfo==1.4.0
django-extensions==2.1.4 django-extensions==2.1.4
django-image-cropping==1.2 django-image-cropping==1.2
django-webpack-loader==0.6.0 django-webpack-loader==1.6.0
drf-yasg==1.20.0 drf-spectacular==0.18.2
django-ratelimit==3.0.1
easy-thumbnails==2.5 easy-thumbnails==2.5
python-decouple==3.1 python-decouple==3.1
psycopg2-binary==2.8.6 psycopg2-binary==2.8.6
pyyaml==5.3.1 pyyaml==5.4
pytz==2019.3 pytz==2019.3
python-magic==0.4.15 python-magic==0.4.15
unipath==1.1 unipath==1.1
WeasyPrint==51 WeasyPrint==51
Pillow==8.1.1 Pillow==9.0.1
gunicorn==19.9.0 gunicorn==19.9.0
more-itertools==8.2.0 more-itertools==8.2.0
pysolr==3.6.0 pysolr==3.6.0
@ -31,6 +33,7 @@ PyPDF4==1.27.0
pyoai==2.5.0 pyoai==2.5.0
Unidecode==1.1.1 Unidecode==1.1.1
whitenoise==5.1.0 whitenoise==5.1.0
kazoo==2.8.0
git+https://github.com/interlegis/trml2pdf git+https://github.com/interlegis/trml2pdf
git+https://github.com/interlegis/django-admin-bootstrapped git+https://github.com/interlegis/django-admin-bootstrapped

307
sapl/api/core/__init__.py

@ -0,0 +1,307 @@
import logging
from django import apps
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Q
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.utils import timezone
from django.utils.decorators import classonlymethod
from django.utils.translation import ugettext_lazy as _
from django_filters.rest_framework.backends import DjangoFilterBackend
from rest_framework import serializers as rest_serializers
from rest_framework.authtoken.models import Token
from rest_framework.decorators import action, api_view, permission_classes
from rest_framework.fields import SerializerMethodField
from rest_framework.permissions import IsAuthenticated, IsAdminUser
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.viewsets import ModelViewSet
from sapl.api.core.filters import SaplFilterSetMixin
from sapl.api.permissions import SaplModelPermissions
from sapl.base.models import Metadata
# ATENÇÃO: MUDANÇAS NO CORE DEVEM SER REALIZADAS COM
# EXTREMA CAUTELA
class BusinessRulesNotImplementedMixin:
def create(self, request, *args, **kwargs):
raise Exception(_("POST Create não implementado"))
def update(self, request, *args, **kwargs):
raise Exception(_("PUT and PATCH não implementado"))
def delete(self, request, *args, **kwargs):
raise Exception(_("DELETE Delete não implementado"))
class SaplApiViewSetConstrutor():
class SaplApiViewSet(ModelViewSet):
filter_backends = (DjangoFilterBackend,)
_built_sets = {}
@classonlymethod
def get_class_for_model(cls, model):
return cls._built_sets[model._meta.app_config][model]
@classonlymethod
def build_class(cls):
import inspect
from sapl.api.core import serializers
# Carrega todas as classes de sapl.api.serializers que possuam
# "Serializer" como Sufixo.
serializers_classes = inspect.getmembers(serializers)
serializers_classes = {i[0]: i[1] for i in filter(
lambda x: x[0].endswith('Serializer'),
serializers_classes
)}
# Carrega todas as classes de sapl.api.forms que possuam
# "FilterSet" como Sufixo.
from sapl.api.core import forms
filters_classes = inspect.getmembers(forms)
filters_classes = {i[0]: i[1] for i in filter(
lambda x: x[0].endswith('FilterSet'),
filters_classes
)}
built_sets = {}
def build(_model):
object_name = _model._meta.object_name
# Caso Exista, pega a classe sapl.api.serializers.{model}Serializer
# ou utiliza a base do drf para gerar uma automática para o model
serializer_name = f'{object_name}Serializer'
_serializer_class = serializers_classes.get(
serializer_name, rest_serializers.ModelSerializer)
# Caso Exista, pega a classe sapl.api.core.forms.{model}FilterSet
# ou utiliza a base definida em
# sapl.api.core.filters.SaplFilterSetMixin
filter_name = f'{object_name}FilterSet'
_filterset_class = filters_classes.get(
filter_name, SaplFilterSetMixin)
def create_class():
_meta_serializer = object if not hasattr(
_serializer_class, 'Meta') else _serializer_class.Meta
# Define uma classe padrão para serializer caso não tenha sido
# criada a classe sapl.api.core.serializers.{model}Serializer
class SaplSerializer(_serializer_class):
__str__ = SerializerMethodField()
metadata = SerializerMethodField()
class Meta(_meta_serializer):
if not hasattr(_meta_serializer, 'ref_name'):
ref_name = f'{object_name}Serializer'
if not hasattr(_meta_serializer, 'model'):
model = _model
if hasattr(_meta_serializer, 'exclude'):
exclude = _meta_serializer.exclude
else:
if not hasattr(_meta_serializer, 'fields'):
fields = '__all__'
elif _meta_serializer.fields != '__all__':
fields = list(_meta_serializer.fields) + [
'__str__', 'metadata']
else:
fields = _meta_serializer.fields
def get___str__(self, obj) -> str:
return str(obj)
def get_metadata(self, obj):
try:
metadata = Metadata.objects.get(
content_type=ContentType.objects.get_for_model(
obj._meta.model),
object_id=obj.id
).metadata
except:
metadata = {}
finally:
return metadata
_meta_filterset = object if not hasattr(
_filterset_class, 'Meta') else _filterset_class.Meta
# Define uma classe padrão para filtro caso não tenha sido
# criada a classe sapl.api.forms.{model}FilterSet
class SaplFilterSet(_filterset_class):
class Meta(_meta_filterset):
if not hasattr(_meta_filterset, 'model'):
model = _model
# Define uma classe padrão ModelViewSet de DRF
class ModelSaplViewSet(SaplApiViewSetConstrutor.SaplApiViewSet):
queryset = _model.objects.all()
# Utiliza o filtro customizado pela classe
# sapl.api.core.forms.{model}FilterSet
# ou utiliza o trivial SaplFilterSet definido acima
filterset_class = SaplFilterSet
# Utiliza o serializer customizado pela classe
# sapl.api.core.serializers.{model}Serializer
# ou utiliza o trivial SaplSerializer definido acima
serializer_class = SaplSerializer
return ModelSaplViewSet
viewset = create_class()
viewset.__name__ = '%sModelSaplViewSet' % _model.__name__
return viewset
apps_sapl = [apps.apps.get_app_config(
n[5:]) for n in settings.SAPL_APPS]
for app in apps_sapl:
cls._built_sets[app] = {}
for model in app.get_models():
cls._built_sets[app][model] = build(model)
return cls
"""
1. Constroi uma rest_framework.viewsets.ModelViewSet para
todos os models de todas as apps do sapl
2. Define DjangoFilterBackend como ferramenta de filtro dos campos
3. Define Serializer como a seguir:
3.1 - Define um Serializer genérico para cada módel
3.2 - Recupera Serializer customizado em sapl.api.core.serializers
3.3 - Para todo model é opcional a existência de
sapl.api.core.serializers.{model}Serializer.
Caso não seja definido um Serializer customizado, utiliza-se o trivial
4. Define um FilterSet como a seguir:
4.1 - Define um FilterSet genérico para cada módel
4.2 - Recupera FilterSet customizado em sapl.api.core.forms
4.3 - Para todo model é opcional a existência de
sapl.api.core.forms.{model}FilterSet.
Caso não seja definido um FilterSet customizado, utiliza-se o trivial
4.4 - todos os campos que aceitam lookup 'exact'
podem ser filtrados por default
5. SaplApiViewSetConstrutor não cria padrões e/ou exige conhecimento alem dos
exigidos pela DRF.
6. As rotas são criadas seguindo nome da app e nome do model
http://localhost:9000/api/{applabel}/{model_name}/
e seguem as variações definidas em:
https://www.django-rest-framework.org/api-guide/routers/#defaultrouter
7. Todas as viewsets construídas por SaplApiViewSetConstrutor e suas rotas
(paginate list, detail, edit, create, delete)
bem como testes em ambiente de desenvolvimento podem ser conferidas em:
http://localhost:9000/api/
desde que settings.DEBUG=True
**SaplApiViewSetConstrutor._built_sets** é um dict de dicts de models conforme:
{
...
'audiencia': {
'tipoaudienciapublica': TipoAudienciaPublicaViewSet,
'audienciapublica': AudienciaPublicaViewSet,
'anexoaudienciapublica': AnexoAudienciaPublicaViewSet
...
},
...
'base': {
'casalegislativa': CasaLegislativaViewSet,
'appconfig': AppConfigViewSet,
...
}
...
}
"""
# Toda Classe construida acima, pode ser redefinida e aplicado quaisquer
# das possibilidades para uma classe normal criada a partir de
# rest_framework.viewsets.ModelViewSet conforme exemplo para a classe autor
# decorator que processa um endpoint detail trivial com base no model passado,
# Um endpoint detail geralmente é um conteúdo baseado numa FK com outros possíveis filtros
# e os passados pelo proprio cliente, além de o serializer e o filterset
# ser desse model passado
class wrapper_queryset_response_for_drf_action(object):
def __init__(self, model):
self.model = model
def __call__(self, cls):
def wrapper(instance_view, *args, **kwargs):
# recupera a viewset do model anotado
iv = instance_view
viewset_from_model = SaplApiViewSetConstrutor._built_sets[
self.model._meta.app_config][self.model]
# apossa da instancia da viewset mae do action
# em uma viewset que processa dados do model passado no decorator
iv.queryset = viewset_from_model.queryset
iv.serializer_class = viewset_from_model.serializer_class
iv.filterset_class = viewset_from_model.filterset_class
iv.queryset = instance_view.filter_queryset(
iv.get_queryset())
# chama efetivamente o metodo anotado que deve devolver um queryset
# com os filtros específicos definido pelo programador customizador
qs = cls(instance_view, *args, **kwargs)
page = iv.paginate_queryset(qs)
data = iv.get_serializer(
page if page is not None else qs, many=True).data
return iv.get_paginated_response(
data) if page is not None else Response(data)
return wrapper
# decorator para recuperar e transformar o default
class customize(object):
def __init__(self, model):
self.model = model
def __call__(self, cls):
class _SaplApiViewSet(
cls,
SaplApiViewSetConstrutor._built_sets[
self.model._meta.app_config][self.model]
):
pass
if hasattr(_SaplApiViewSet, 'build'):
_SaplApiViewSet = _SaplApiViewSet.build()
SaplApiViewSetConstrutor._built_sets[
self.model._meta.app_config][self.model] = _SaplApiViewSet
return _SaplApiViewSet

116
sapl/api/core/filters.py

@ -0,0 +1,116 @@
from collections import OrderedDict
from django.contrib.postgres.fields.jsonb import JSONField
from django.db.models.fields.files import FileField
from django.template.defaultfilters import capfirst
import django_filters
from django_filters.constants import ALL_FIELDS
from django_filters.filters import CharFilter
from django_filters.filterset import FilterSet
from django_filters.utils import resolve_field, get_all_model_fields
# ATENÇÃO: MUDANÇAS NO CORE DEVEM SER REALIZADAS COM
# EXTREMA CAUTELA E CONSCIENTE DOS IMPACTOS NA API
class SaplFilterSetMixin(FilterSet):
o = CharFilter(method='filter_o')
class Meta:
fields = '__all__'
filter_overrides = {
FileField: {
'filter_class': django_filters.CharFilter,
'extra': lambda f: {
'lookup_expr': 'exact',
},
},
JSONField: {
'filter_class': django_filters.CharFilter,
'extra': lambda f: {
'lookup_expr': 'exact',
},
},
}
def filter_o(self, queryset, name, value):
try:
return queryset.order_by(
*map(str.strip, value.split(',')))
except:
return queryset
@classmethod
def get_fields(cls):
model = cls._meta.model
fields_model = get_all_model_fields(model)
fields_filter = cls._meta.fields
exclude = cls._meta.exclude
if exclude is not None and fields_filter is None:
fields_filter = ALL_FIELDS
fields = fields_filter if isinstance(fields_filter, dict) else {}
for f_str in fields_model:
if f_str not in fields:
f = model._meta.get_field(f_str)
if f.many_to_many:
fields[f_str] = ['exact']
continue
fields[f_str] = ['exact']
def get_keys_lookups(cl, sub_f):
r = []
for lk, lv in cl.items():
if lk == 'contained_by':
continue
sflk = f'{sub_f}{"__" if sub_f else ""}{lk}'
r.append(sflk)
if hasattr(lv, 'class_lookups'):
r += get_keys_lookups(lv.class_lookups, sflk)
if hasattr(lv, 'output_field') and hasattr(lv, 'output_field.class_lookups'):
r.append(f'{sflk}{"__" if sflk else ""}range')
r += get_keys_lookups(lv.output_field.class_lookups, sflk)
return r
fields[f_str] = list(
set(fields[f_str] + get_keys_lookups(f.class_lookups, '')))
# Remove excluded fields
exclude = exclude or []
fields = [(f, lookups)
for f, lookups in fields.items() if f not in exclude]
return OrderedDict(fields)
@classmethod
def filter_for_field(cls, f, name, lookup_expr='exact'):
# Redefine método estático para ignorar filtro para
# fields que não possuam lookup_expr informado
f, lookup_type = resolve_field(f, lookup_expr)
default = {
'field_name': name,
'label': capfirst(f.verbose_name),
'lookup_expr': lookup_expr
}
filter_class, params = cls.filter_for_lookup(
f, lookup_type)
default.update(params)
if filter_class is not None:
return filter_class(**default)
return None

25
sapl/api/core/forms.py

@ -0,0 +1,25 @@
from sapl.api.core.filters import SaplFilterSetMixin
from sapl.sessao.models import SessaoPlenaria
# ATENÇÃO: MUDANÇAS NO CORE DEVEM SER REALIZADAS COM
# EXTREMA CAUTELA E CONSCIENTE DOS IMPACTOS NA API
# FILTER SET dentro do core devem ser criados se o intuíto é um filter-set
# para o list da api.
# filter_set para actions, devem ser criados fora do core.
# A CLASSE SessaoPlenariaFilterSet não é necessária
# o construtor da api construiría uma igual
# mas está aqui para demonstrar que caso queira customizar um filter_set
# que a api consiga recuperá-lo, para os endpoints básicos
# deve seguir os critérios de nomenclatura e herança
# class [Model]FilterSet(SaplFilterSetMixin):
# class Meta(SaplFilterSetMixin.Meta):
class SessaoPlenariaFilterSet(SaplFilterSetMixin):
class Meta(SaplFilterSetMixin.Meta):
model = SessaoPlenaria

5
sapl/api/core/schema.py

@ -0,0 +1,5 @@
from drf_spectacular.openapi import AutoSchema
class Schema(AutoSchema):
pass

50
sapl/api/core/serializers.py

@ -0,0 +1,50 @@
import logging
from django.conf import settings
from rest_framework import serializers
from rest_framework.relations import StringRelatedField
from sapl.base.models import CasaLegislativa
class IntRelatedField(StringRelatedField):
def to_representation(self, value):
return int(value)
class ChoiceSerializer(serializers.Serializer):
value = serializers.SerializerMethodField()
text = serializers.SerializerMethodField()
def get_text(self, obj):
return obj[1]
def get_value(self, obj):
return obj[0]
class ModelChoiceSerializer(ChoiceSerializer):
def get_text(self, obj):
return str(obj)
def get_value(self, obj):
return obj.id
class ModelChoiceObjectRelatedField(serializers.RelatedField):
def to_representation(self, value):
return ModelChoiceSerializer(value).data
class CasaLegislativaSerializer(serializers.ModelSerializer):
version = serializers.SerializerMethodField()
def get_version(self, obj):
return settings.SAPL_VERSION
class Meta:
model = CasaLegislativa
fields = '__all__'

18
sapl/api/deprecated.py

@ -1,5 +1,4 @@
import logging
import logging import logging
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
@ -10,20 +9,18 @@ from django.forms.widgets import MultiWidget, TextInput
from django.http import Http404 from django.http import Http404
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext_lazy as _
from django_filters.filters import CharFilter, ModelChoiceFilter, DateFilter from django_filters.filters import CharFilter, ModelChoiceFilter, DateFilter
from django_filters.rest_framework.backends import DjangoFilterBackend from django_filters.rest_framework.backends import DjangoFilterBackend
from django_filters.rest_framework.filterset import FilterSet from django_filters.rest_framework.filterset import FilterSet
from rest_framework import serializers from rest_framework import serializers
from rest_framework import serializers
from rest_framework.generics import ListAPIView from rest_framework.generics import ListAPIView
from rest_framework.mixins import ListModelMixin, RetrieveModelMixin from rest_framework.mixins import ListModelMixin, RetrieveModelMixin
from rest_framework.permissions import (IsAuthenticated, from rest_framework.permissions import (IsAuthenticated,
IsAuthenticatedOrReadOnly, AllowAny) IsAuthenticatedOrReadOnly, AllowAny)
from rest_framework.viewsets import GenericViewSet from rest_framework.viewsets import GenericViewSet
from sapl.api.serializers import ModelChoiceSerializer, AutorSerializer,\ from sapl.api.core.serializers import ModelChoiceSerializer, ChoiceSerializer
ChoiceSerializer from sapl.api.serializers import AutorSerializer
from sapl.base.models import TipoAutor, Autor, CasaLegislativa from sapl.base.models import TipoAutor, Autor, CasaLegislativa
from sapl.materia.models import MateriaLegislativa from sapl.materia.models import MateriaLegislativa
from sapl.parlamentares.models import Legislatura from sapl.parlamentares.models import Legislatura
@ -210,7 +207,16 @@ class AutoresPossiveisFilterSet(FilterSet):
if legislatura_relativa.atual(): if legislatura_relativa.atual():
q = q & Q(parlamentar_set__ativo=True) q = q & Q(parlamentar_set__ativo=True)
return queryset.filter(q) legislatura_anterior = self.request.GET.get('legislatura_anterior', 'False')
if legislatura_anterior.lower() == 'true':
legislaturas = Legislatura.objects.filter(
data_fim__lte=data_relativa).order_by('-data_fim')[:2]
if len(legislaturas) == 2:
_, leg_anterior = legislaturas
q = q | Q(parlamentar_set__mandato__data_inicio_mandato__gte=leg_anterior.data_inicio)
qs = queryset.filter(q)
return qs
def filter_comissao(self, queryset, data_relativa): def filter_comissao(self, queryset, data_relativa):
return queryset.filter( return queryset.filter(

65
sapl/api/forms.py

@ -1,65 +0,0 @@
from django.db.models.fields.files import FileField
from django.template.defaultfilters import capfirst
import django_filters
from django_filters.filters import CharFilter, NumberFilter
from django_filters.rest_framework.filterset import FilterSet
from django_filters.utils import resolve_field
from sapl.sessao.models import SessaoPlenaria
class SaplFilterSetMixin(FilterSet):
o = CharFilter(method='filter_o')
class Meta:
fields = '__all__'
filter_overrides = {
FileField: {
'filter_class': django_filters.CharFilter,
'extra': lambda f: {
'lookup_expr': 'exact',
},
},
}
def filter_o(self, queryset, name, value):
try:
return queryset.order_by(
*map(str.strip, value.split(',')))
except:
return queryset
@classmethod
def filter_for_field(cls, f, name, lookup_expr='exact'):
# Redefine método estático para ignorar filtro para
# fields que não possuam lookup_expr informado
f, lookup_type = resolve_field(f, lookup_expr)
default = {
'field_name': name,
'label': capfirst(f.verbose_name),
'lookup_expr': lookup_expr
}
filter_class, params = cls.filter_for_lookup(
f, lookup_type)
default.update(params)
if filter_class is not None:
return filter_class(**default)
return None
class SessaoPlenariaFilterSet(SaplFilterSetMixin):
year = NumberFilter(method='filter_year')
month = NumberFilter(method='filter_month')
class Meta(SaplFilterSetMixin.Meta):
model = SessaoPlenaria
def filter_year(self, queryset, name, value):
qs = queryset.filter(data_inicio__year=value)
return qs
def filter_month(self, queryset, name, value):
qs = queryset.filter(data_inicio__month=value)
return qs

67
sapl/api/pagination.py

@ -6,13 +6,74 @@ from rest_framework.response import Response
class StandardPagination(pagination.PageNumberPagination): class StandardPagination(pagination.PageNumberPagination):
page_size = 10 page_size = 10
page_size_query_param = 'page_size' page_size_query_param = 'page_size'
max_page_size = 50 max_page_size = 100
def paginate_queryset(self, queryset, request, view=None): def paginate_queryset(self, queryset, request, view=None):
if request.query_params.get('get_all', '').lower() == 'true': if request.query_params.get('get_all', '').lower() == 'true':
return None return None
return super().paginate_queryset(queryset, request, view=view) return super().paginate_queryset(queryset, request, view=view)
def get_paginated_response_schema(self, schema):
r = {
'type': 'object',
'properties': {
'pagination': {
'type': 'object',
'properties': {
'links': {
'type': 'object',
'properties': {
'next': {
'type': 'string',
'nullable': True,
'format': 'uri',
'example': 'http://api.example.org/accounts/?{page_query_param}=4'.format(
page_query_param=self.page_query_param)
},
'previous': {
'type': 'string',
'nullable': True,
'format': 'uri',
'example': 'http://api.example.org/accounts/?{page_query_param}=2'.format(
page_query_param=self.page_query_param)
},
}
},
'previous_page': {
'type': 'integer',
'example': 123,
},
'next_page': {
'type': 'integer',
'example': 123,
},
'start_index': {
'type': 'integer',
'example': 123,
},
'end_index': {
'type': 'integer',
'example': 123,
},
'total_entries': {
'type': 'integer',
'example': 123,
},
'total_pages': {
'type': 'integer',
'example': 123,
},
'page': {
'type': 'integer',
'example': 123,
},
}
},
'results': schema,
},
}
return r
def get_paginated_response(self, data): def get_paginated_response(self, data):
try: try:
previous_page_number = self.page.previous_page_number() previous_page_number = self.page.previous_page_number()
@ -26,6 +87,10 @@ class StandardPagination(pagination.PageNumberPagination):
return Response({ return Response({
'pagination': { 'pagination': {
'links': {
'next': self.get_next_link(),
'previous': self.get_previous_link(),
},
'previous_page': previous_page_number, 'previous_page': previous_page_number,
'next_page': next_page_number, 'next_page': next_page_number,
'start_index': self.page.start_index(), 'start_index': self.page.start_index(),

64
sapl/api/serializers.py

@ -1,44 +1,14 @@
import logging import logging
from django.conf import settings from django.conf import settings
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
from django.db.models import F, Q from django.db.models import Q
from rest_framework import serializers
from rest_framework.relations import StringRelatedField
from sapl.parlamentares.models import Parlamentar, Mandato, Filiacao, Legislatura
from sapl.base.models import Autor, CasaLegislativa
from sapl.utils import filiacao_data
from image_cropping.utils import get_backend from image_cropping.utils import get_backend
from rest_framework import serializers
from sapl.api.core.serializers import ModelChoiceObjectRelatedField
class IntRelatedField(StringRelatedField): from sapl.base.models import Autor
def to_representation(self, value): from sapl.parlamentares.models import Parlamentar, Mandato, Legislatura
return int(value)
class ChoiceSerializer(serializers.Serializer):
value = serializers.SerializerMethodField()
text = serializers.SerializerMethodField()
def get_text(self, obj):
return obj[1]
def get_value(self, obj):
return obj[0]
class ModelChoiceSerializer(ChoiceSerializer):
def get_text(self, obj):
return str(obj)
def get_value(self, obj):
return obj.id
class ModelChoiceObjectRelatedField(serializers.RelatedField):
def to_representation(self, value):
return ModelChoiceSerializer(value).data
class AutorSerializer(serializers.ModelSerializer): class AutorSerializer(serializers.ModelSerializer):
@ -52,18 +22,7 @@ class AutorSerializer(serializers.ModelSerializer):
fields = '__all__' fields = '__all__'
class CasaLegislativaSerializer(serializers.ModelSerializer): class ParlamentarSerializerPublic(serializers.ModelSerializer):
version = serializers.SerializerMethodField()
def get_version(self, obj):
return settings.SAPL_VERSION
class Meta:
model = CasaLegislativa
fields = '__all__'
class ParlamentarSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Parlamentar model = Parlamentar
@ -73,14 +32,7 @@ class ParlamentarSerializer(serializers.ModelSerializer):
"telefone_residencia", "titulo_eleitor", "fax_residencia"] "telefone_residencia", "titulo_eleitor", "fax_residencia"]
class ParlamentarEditSerializer(serializers.ModelSerializer): class ParlamentarSerializerVerbose(serializers.ModelSerializer):
class Meta:
model = Parlamentar
fields = '__all__'
class ParlamentarResumeSerializer(serializers.ModelSerializer):
titular = serializers.SerializerMethodField('check_titular') titular = serializers.SerializerMethodField('check_titular')
partido = serializers.SerializerMethodField('check_partido') partido = serializers.SerializerMethodField('check_partido')
fotografia_cropped = serializers.SerializerMethodField('crop_fotografia') fotografia_cropped = serializers.SerializerMethodField('crop_fotografia')

46
sapl/api/urls.py

@ -1,54 +1,40 @@
from django.conf import settings
from django.conf.urls import include, url from django.conf.urls import include, url
from rest_framework import permissions from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView, \
SpectacularRedocView
from rest_framework.authtoken.views import obtain_auth_token
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
from sapl.api.deprecated import MateriaLegislativaViewSet, SessaoPlenariaViewSet, \ from sapl.api.deprecated import MateriaLegislativaViewSet, SessaoPlenariaViewSet, \
AutoresProvaveisListView, AutoresPossiveisListView, AutorListView, \ AutoresProvaveisListView, AutoresPossiveisListView, AutorListView, \
ModelChoiceView ModelChoiceView
from sapl.api.views import SaplApiViewSetConstrutor, AppVersionView, recria_token from sapl.api.views import AppVersionView, recria_token
from sapl.api.viewset import SaplApiViewSetConstrutor
from .apps import AppConfig from .apps import AppConfig
app_name = AppConfig.name app_name = AppConfig.name
router = DefaultRouter() router = DefaultRouter()
router.register(r'materia$', MateriaLegislativaViewSet) router.register(r'materia$', MateriaLegislativaViewSet)
router.register(r'sessao-plenaria', SessaoPlenariaViewSet) router.register(r'sessao-plenaria', SessaoPlenariaViewSet)
for app, built_sets in SaplApiViewSetConstrutor._built_sets.items(): for app, built_sets in SaplApiViewSetConstrutor._built_sets.items():
for view_prefix, viewset in built_sets.items(): for view_prefix, viewset in built_sets.items():
router.register(app.label + '/' + router.register(app.label + '/' +
view_prefix._meta.model_name, viewset) view_prefix._meta.model_name, viewset)
urlpatterns_router = router.urls urlpatterns_router = router.urls
urlpatterns_api_doc = []
if 'drf_yasg' in settings.INSTALLED_APPS:
from drf_yasg import openapi
from drf_yasg.views import get_schema_view
schema_view = get_schema_view(
openapi.Info(
title="Sapl API - docs",
default_version='v1',
description="Sapl API - Docs - Configuração Básica",
),
url=settings.SITE_URL,
public=True,
permission_classes=(permissions.AllowAny,),
)
urlpatterns_api_doc = [ urlpatterns_api_doc = [
url(r'^docs/swagger(?P<format>\.json|\.yaml)$', # Optional UI:
schema_view.without_ui(cache_timeout=0), name='schema-json'), url('^schema/swagger-ui/',
url(r'^docs/swagger/$', SpectacularSwaggerView.as_view(url_name='sapl.api:schema_api'), name='swagger_ui_schema_api'),
schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), url('^schema/redoc/',
url(r'^docs/redoc/$', SpectacularRedocView.as_view(url_name='sapl.api:schema_api'), name='redoc_schema_api'),
schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), # YOUR PATTERNS
url('^schema/', SpectacularAPIView.as_view(), name='schema_api'),
] ]
# TODO: refatorar para customização da api automática # TODO: refatorar para customização da api automática
@ -57,13 +43,9 @@ deprecated_urlpatterns_api = [
AutoresProvaveisListView.as_view(), name='autores_provaveis_list'), AutoresProvaveisListView.as_view(), name='autores_provaveis_list'),
url(r'^autor/possiveis', url(r'^autor/possiveis',
AutoresPossiveisListView.as_view(), name='autores_possiveis_list'), AutoresPossiveisListView.as_view(), name='autores_possiveis_list'),
url(r'^autor', AutorListView.as_view(), name='autor_list'), url(r'^autor', AutorListView.as_view(), name='autor_list'),
url(r'^model/(?P<content_type>\d+)/(?P<pk>\d*)$', url(r'^model/(?P<content_type>\d+)/(?P<pk>\d*)$',
ModelChoiceView.as_view(), name='model_list'), ModelChoiceView.as_view(), name='model_list'),
] ]
urlpatterns = [ urlpatterns = [
@ -73,6 +55,8 @@ urlpatterns = [
url(r'^api/version', AppVersionView.as_view()), url(r'^api/version', AppVersionView.as_view()),
url(r'^api/recriar-token/(?P<pk>\d*)$', recria_token, name="recria_token"), url(r'^api/recriar-token/(?P<pk>\d*)$', recria_token, name="recria_token"),
url(r'^api/auth/token$', obtain_auth_token),
# implementar caminho para autenticação # implementar caminho para autenticação
# https://www.django-rest-framework.org/tutorial/4-authentication-and-permissions/ # https://www.django-rest-framework.org/tutorial/4-authentication-and-permissions/
# url(r'^api/auth/', include('rest_framework.urls', namespace='rest_framework')), # url(r'^api/auth/', include('rest_framework.urls', namespace='rest_framework')),

668
sapl/api/views.py

@ -1,39 +1,14 @@
import logging import logging
from django import apps
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Q
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
from django.utils import timezone
from django.utils.decorators import classonlymethod
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django_filters.rest_framework.backends import DjangoFilterBackend
from rest_framework import serializers as rest_serializers
from rest_framework.authtoken.models import Token from rest_framework.authtoken.models import Token
from rest_framework.decorators import action, api_view, permission_classes from rest_framework.decorators import api_view, permission_classes
from rest_framework.fields import SerializerMethodField
from rest_framework.permissions import IsAuthenticated, IsAdminUser from rest_framework.permissions import IsAuthenticated, IsAdminUser
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.viewsets import ModelViewSet
from sapl.api.forms import SaplFilterSetMixin
from sapl.api.permissions import SaplModelPermissions
from sapl.api.serializers import ChoiceSerializer, ParlamentarSerializer,\
ParlamentarEditSerializer, ParlamentarResumeSerializer
from sapl.base.models import Autor, AppConfig, DOC_ADM_OSTENSIVO
from sapl.materia.models import Proposicao, TipoMateriaLegislativa,\
MateriaLegislativa, Tramitacao
from sapl.norma.models import NormaJuridica
from sapl.parlamentares.models import Mandato, Legislatura
from sapl.parlamentares.models import Parlamentar
from sapl.protocoloadm.models import DocumentoAdministrativo,\
DocumentoAcessorioAdministrativo, TramitacaoAdministrativo, Anexado
from sapl.sessao.models import SessaoPlenaria, ExpedienteSessao
from sapl.utils import models_with_gr_for_model, choice_anos_com_sessaoplenaria
@receiver(post_save, sender=settings.AUTH_USER_MODEL) @receiver(post_save, sender=settings.AUTH_USER_MODEL)
@ -51,647 +26,6 @@ def recria_token(request, pk):
return Response({"message": "Token recriado com sucesso!", "token": token.key}) return Response({"message": "Token recriado com sucesso!", "token": token.key})
class BusinessRulesNotImplementedMixin:
def create(self, request, *args, **kwargs):
raise Exception(_("POST Create não implementado"))
def update(self, request, *args, **kwargs):
raise Exception(_("PUT and PATCH não implementado"))
def delete(self, request, *args, **kwargs):
raise Exception(_("DELETE Delete não implementado"))
class SaplApiViewSetConstrutor():
class SaplApiViewSet(ModelViewSet):
filter_backends = (DjangoFilterBackend,)
_built_sets = {}
@classonlymethod
def get_class_for_model(cls, model):
return cls._built_sets[model._meta.app_config][model]
@classonlymethod
def build_class(cls):
import inspect
from sapl.api import serializers
# Carrega todas as classes de sapl.api.serializers que possuam
# "Serializer" como Sufixo.
serializers_classes = inspect.getmembers(serializers)
serializers_classes = {i[0]: i[1] for i in filter(
lambda x: x[0].endswith('Serializer'),
serializers_classes
)}
# Carrega todas as classes de sapl.api.forms que possuam
# "FilterSet" como Sufixo.
from sapl.api import forms
filters_classes = inspect.getmembers(forms)
filters_classes = {i[0]: i[1] for i in filter(
lambda x: x[0].endswith('FilterSet'),
filters_classes
)}
built_sets = {}
def build(_model):
object_name = _model._meta.object_name
# Caso Exista, pega a classe sapl.api.serializers.{model}Serializer
# ou utiliza a base do drf para gerar uma automática para o model
serializer_name = f'{object_name}Serializer'
_serializer_class = serializers_classes.get(
serializer_name, rest_serializers.ModelSerializer)
# Caso Exista, pega a classe sapl.api.forms.{model}FilterSet
# ou utiliza a base definida em sapl.forms.SaplFilterSetMixin
filter_name = f'{object_name}FilterSet'
_filterset_class = filters_classes.get(
filter_name, SaplFilterSetMixin)
def create_class():
_meta_serializer = object if not hasattr(
_serializer_class, 'Meta') else _serializer_class.Meta
# Define uma classe padrão para serializer caso não tenha sido
# criada a classe sapl.api.serializers.{model}Serializer
class SaplSerializer(_serializer_class):
__str__ = SerializerMethodField()
class Meta(_meta_serializer):
if not hasattr(_meta_serializer, 'ref_name'):
ref_name = None
if not hasattr(_meta_serializer, 'model'):
model = _model
if hasattr(_meta_serializer, 'exclude'):
exclude = _meta_serializer.exclude
else:
if not hasattr(_meta_serializer, 'fields'):
fields = '__all__'
elif _meta_serializer.fields != '__all__':
fields = list(
_meta_serializer.fields) + ['__str__', ]
else:
fields = _meta_serializer.fields
def get___str__(self, obj):
return str(obj)
_meta_filterset = object if not hasattr(
_filterset_class, 'Meta') else _filterset_class.Meta
# Define uma classe padrão para filtro caso não tenha sido
# criada a classe sapl.api.forms.{model}FilterSet
class SaplFilterSet(_filterset_class):
class Meta(_meta_filterset):
if not hasattr(_meta_filterset, 'model'):
model = _model
# Define uma classe padrão ModelViewSet de DRF
class ModelSaplViewSet(SaplApiViewSetConstrutor.SaplApiViewSet):
queryset = _model.objects.all()
# Utiliza o filtro customizado pela classe
# sapl.api.forms.{model}FilterSet
# ou utiliza o trivial SaplFilterSet definido acima
filterset_class = SaplFilterSet
# Utiliza o serializer customizado pela classe
# sapl.api.serializers.{model}Serializer
# ou utiliza o trivial SaplSerializer definido acima
serializer_class = SaplSerializer
return ModelSaplViewSet
viewset = create_class()
viewset.__name__ = '%sModelSaplViewSet' % _model.__name__
return viewset
apps_sapl = [apps.apps.get_app_config(
n[5:]) for n in settings.SAPL_APPS]
for app in apps_sapl:
cls._built_sets[app] = {}
for model in app.get_models():
cls._built_sets[app][model] = build(model)
SaplApiViewSetConstrutor.build_class()
"""
1. Constroi uma rest_framework.viewsets.ModelViewSet para
todos os models de todas as apps do sapl
2. Define DjangoFilterBackend como ferramenta de filtro dos campos
3. Define Serializer como a seguir:
3.1 - Define um Serializer genérico para cada módel
3.2 - Recupera Serializer customizado em sapl.api.serializers
3.3 - Para todo model é opcional a existência de
sapl.api.serializers.{model}Serializer.
Caso não seja definido um Serializer customizado, utiliza-se o trivial
4. Define um FilterSet como a seguir:
4.1 - Define um FilterSet genérico para cada módel
4.2 - Recupera FilterSet customizado em sapl.api.forms
4.3 - Para todo model é opcional a existência de
sapl.api.forms.{model}FilterSet.
Caso não seja definido um FilterSet customizado, utiliza-se o trivial
4.4 - todos os campos que aceitam lookup 'exact'
podem ser filtrados por default
5. SaplApiViewSetConstrutor não cria padrões e/ou exige conhecimento alem dos
exigidos pela DRF.
6. As rotas são criadas seguindo nome da app e nome do model
http://localhost:9000/api/{applabel}/{model_name}/
e seguem as variações definidas em:
https://www.django-rest-framework.org/api-guide/routers/#defaultrouter
7. Todas as viewsets construídas por SaplApiViewSetConstrutor e suas rotas
(paginate list, detail, edit, create, delete)
bem como testes em ambiente de desenvolvimento podem ser conferidas em:
http://localhost:9000/api/
desde que settings.DEBUG=True
**SaplApiViewSetConstrutor._built_sets** é um dict de dicts de models conforme:
{
...
'audiencia': {
'tipoaudienciapublica': TipoAudienciaPublicaViewSet,
'audienciapublica': AudienciaPublicaViewSet,
'anexoaudienciapublica': AnexoAudienciaPublicaViewSet
...
},
...
'base': {
'casalegislativa': CasaLegislativaViewSet,
'appconfig': AppConfigViewSet,
...
}
...
}
"""
# Toda Classe construida acima, pode ser redefinida e aplicado quaisquer
# das possibilidades para uma classe normal criada a partir de
# rest_framework.viewsets.ModelViewSet conforme exemplo para a classe autor
# decorator que processa um endpoint detail trivial com base no model passado,
# Um endpoint detail geralmente é um conteúdo baseado numa FK com outros possíveis filtros
# e os passados pelo proprio cliente, além de o serializer e o filterset
# ser desse model passado
class wrapper_queryset_response_for_drf_action(object):
def __init__(self, model):
self.model = model
def __call__(self, cls):
def wrapper(instance_view, *args, **kwargs):
# recupera a viewset do model anotado
iv = instance_view
viewset_from_model = SaplApiViewSetConstrutor._built_sets[
self.model._meta.app_config][self.model]
# apossa da instancia da viewset mae do action
# em uma viewset que processa dados do model passado no decorator
iv.queryset = viewset_from_model.queryset
iv.serializer_class = viewset_from_model.serializer_class
iv.filterset_class = viewset_from_model.filterset_class
iv.queryset = instance_view.filter_queryset(
iv.get_queryset())
# chama efetivamente o metodo anotado que deve devolver um queryset
# com os filtros específicos definido pelo programador customizador
qs = cls(instance_view, *args, **kwargs)
page = iv.paginate_queryset(qs)
data = iv.get_serializer(
page if page is not None else qs, many=True).data
return iv.get_paginated_response(
data) if page is not None else Response(data)
return wrapper
# decorator para recuperar e transformar o default
class customize(object):
def __init__(self, model):
self.model = model
def __call__(self, cls):
class _SaplApiViewSet(
cls,
SaplApiViewSetConstrutor._built_sets[
self.model._meta.app_config][self.model]
):
pass
if hasattr(_SaplApiViewSet, 'build'):
_SaplApiViewSet = _SaplApiViewSet.build()
SaplApiViewSetConstrutor._built_sets[
self.model._meta.app_config][self.model] = _SaplApiViewSet
return _SaplApiViewSet
# Customização para AutorViewSet com implementação de actions específicas
@customize(Autor)
class _AutorViewSet:
"""
Neste exemplo de customização do que foi criado em
SaplApiViewSetConstrutor além do ofertado por
rest_framework.viewsets.ModelViewSet, dentre outras customizações
possíveis, foi adicionado as rotas referentes aos relacionamentos genéricos
* padrão de ModelViewSet
/api/base/autor/ POST - create
/api/base/autor/ GET - list
/api/base/autor/{pk}/ GET - detail
/api/base/autor/{pk}/ PUT - update
/api/base/autor/{pk}/ PATCH - partial_update
/api/base/autor/{pk}/ DELETE - destroy
* rotas desta classe local criadas pelo método build:
/api/base/autor/parlamentar
devolve apenas autores que são parlamentares
/api/base/autor/comissao
devolve apenas autores que são comissões
/api/base/autor/bloco
devolve apenas autores que são blocos parlamentares
/api/base/autor/bancada
devolve apenas autores que são bancadas parlamentares
/api/base/autor/frente
devolve apenas autores que são Frene parlamentares
/api/base/autor/orgao
devolve apenas autores que são Órgãos
"""
def list_for_content_type(self, content_type):
qs = self.get_queryset()
qs = qs.filter(content_type=content_type)
page = self.paginate_queryset(qs)
if page is not None:
serializer = self.serializer_class(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(page, many=True)
return Response(serializer.data)
@classonlymethod
def build(cls):
models_with_gr_for_autor = models_with_gr_for_model(Autor)
for _model in models_with_gr_for_autor:
@action(detail=False, name=_model._meta.model_name)
def actionclass(self, request, *args, **kwargs):
model = getattr(self, self.action)._AutorViewSet__model
content_type = ContentType.objects.get_for_model(model)
return self.list_for_content_type(content_type)
func = actionclass
func.mapping['get'] = func.kwargs['name']
func.url_name = func.kwargs['name']
func.url_path = func.kwargs['name']
func.__model = _model
setattr(cls, _model._meta.model_name, func)
return cls
@customize(Parlamentar)
class _ParlamentarViewSet:
class ParlamentarPermission(SaplModelPermissions):
def has_permission(self, request, view):
if request.method == 'GET':
return True
else:
perm = super().has_permission(request, view)
return perm
permission_classes = (ParlamentarPermission, )
def get_serializer(self, *args, **kwargs):
if self.request.user.has_perm('parlamentares.add_parlamentar'):
self.serializer_class = ParlamentarEditSerializer
return super().get_serializer(*args, **kwargs)
@action(detail=True)
def proposicoes(self, request, *args, **kwargs):
"""
Lista de proposições públicas de parlamentar específico
:param int id: - Identificador do parlamentar que se quer recuperar as proposições
:return: uma lista de proposições
"""
# /api/parlamentares/parlamentar/{id}/proposicoes/
# recupera proposições enviadas e incorporadas do parlamentar
# deve coincidir com
# /parlamentar/{pk}/proposicao
return self.get_proposicoes(**kwargs)
@wrapper_queryset_response_for_drf_action(model=Proposicao)
def get_proposicoes(self, **kwargs):
return self.get_queryset().filter(
data_envio__isnull=False,
data_recebimento__isnull=False,
cancelado=False,
autor__object_id=kwargs['pk'],
autor__content_type=ContentType.objects.get_for_model(Parlamentar)
)
@action(detail=False, methods=['GET'])
def search_parlamentares(self, request, *args, **kwargs):
nome = request.query_params.get('nome_parlamentar', '')
parlamentares = Parlamentar.objects.filter(
nome_parlamentar__icontains=nome)
serializer_class = ParlamentarResumeSerializer(
parlamentares, many=True, context={'request': request})
return Response(serializer_class.data)
@customize(Legislatura)
class _LegislaturaViewSet:
@action(detail=True)
def parlamentares(self, request, *args, **kwargs):
def get_serializer_context():
return {
'request': self.request, 'legislatura': kwargs['pk']
}
def get_serializer_class():
return ParlamentarResumeSerializer
self.get_serializer_context = get_serializer_context
self.get_serializer_class = get_serializer_class
return self.get_parlamentares()
@wrapper_queryset_response_for_drf_action(model=Parlamentar)
def get_parlamentares(self):
try:
legislatura = Legislatura.objects.get(pk=self.kwargs['pk'])
except ObjectDoesNotExist:
return Response("")
data_atual = timezone.localdate()
filter_params = {
'legislatura': legislatura,
'data_inicio_mandato__gte': legislatura.data_inicio,
'data_fim_mandato__lte': legislatura.data_fim,
}
mandatos = Mandato.objects.filter(
**filter_params).order_by('-data_inicio_mandato')
parlamentares = self.get_queryset().filter(
mandato__in=mandatos).distinct()
return parlamentares
@customize(Proposicao)
class _ProposicaoViewSet:
"""
list:
Retorna lista de Proposições
* Permissões:
* Usuário Dono:
* Pode listar todas suas Proposições
* Usuário Conectado ou Anônimo:
* Pode listar todas as Proposições incorporadas
retrieve:
Retorna uma proposição passada pelo 'id'
* Permissões:
* Usuário Dono:
* Pode recuperar qualquer de suas Proposições
* Usuário Conectado ou Anônimo:
* Pode recuperar qualquer das proposições incorporadas
"""
class ProposicaoPermission(SaplModelPermissions):
def has_permission(self, request, view):
if request.method == 'GET':
return True
# se a solicitação é list ou detail, libera o teste de permissão
# e deixa o get_queryset filtrar de acordo com a regra de
# visibilidade das proposições, ou seja:
# 1. proposição incorporada é proposição pública
# 2. não incorporada só o autor pode ver
else:
perm = super().has_permission(request, view)
return perm
# não é list ou detail, então passa pelas regras de permissão e,
# depois disso ainda passa pelo filtro de get_queryset
permission_classes = (ProposicaoPermission, )
def get_queryset(self):
qs = super().get_queryset()
q = Q(data_recebimento__isnull=False, object_id__isnull=False)
if not self.request.user.is_anonymous:
autor_do_usuario_logado = self.request.user.autor_set.first()
# se usuário logado é operador de algum autor
if autor_do_usuario_logado:
q = Q(autor=autor_do_usuario_logado)
# se é operador de protocolo, ve qualquer coisa enviada
if self.request.user.has_perm('protocoloadm.list_protocolo'):
q = Q(data_envio__isnull=False) | Q(
data_devolucao__isnull=False)
qs = qs.filter(q)
return qs
@customize(MateriaLegislativa)
class _MateriaLegislativaViewSet:
class Meta:
ordering = ['-ano', 'tipo', 'numero']
@action(detail=True, methods=['GET'])
def ultima_tramitacao(self, request, *args, **kwargs):
materia = self.get_object()
if not materia.tramitacao_set.exists():
return Response({})
ultima_tramitacao = materia.tramitacao_set.order_by(
'-data_tramitacao', '-id').first()
serializer_class = SaplApiViewSetConstrutor.get_class_for_model(
Tramitacao).serializer_class(ultima_tramitacao)
return Response(serializer_class.data)
@action(detail=True, methods=['GET'])
def anexadas(self, request, *args, **kwargs):
self.queryset = self.get_object().anexadas.all()
return self.list(request, *args, **kwargs)
@customize(TipoMateriaLegislativa)
class _TipoMateriaLegislativaViewSet:
@action(detail=True, methods=['POST'])
def change_position(self, request, *args, **kwargs):
result = {
'status': 200,
'message': 'OK'
}
d = request.data
if 'pos_ini' in d and 'pos_fim' in d:
if d['pos_ini'] != d['pos_fim']:
pk = kwargs['pk']
TipoMateriaLegislativa.objects.reposicione(pk, d['pos_fim'])
return Response(result)
@customize(DocumentoAdministrativo)
class _DocumentoAdministrativoViewSet:
class DocumentoAdministrativoPermission(SaplModelPermissions):
def has_permission(self, request, view):
if request.method == 'GET':
comportamento = AppConfig.attr('documentos_administrativos')
if comportamento == DOC_ADM_OSTENSIVO:
return True
"""
Diante da lógica implementada na manutenção de documentos
administrativos:
- Se o comportamento é doc adm ostensivo, deve passar pelo
teste de permissões sem avaliá-las
- se o comportamento é doc adm restritivo, deve passar pelo
teste de permissões avaliando-as
"""
return super().has_permission(request, view)
permission_classes = (DocumentoAdministrativoPermission, )
def get_queryset(self):
"""
mesmo tendo passado pelo teste de permissões, deve ser filtrado,
pelo campo restrito. Sendo este igual a True, disponibilizar apenas
a um usuário conectado. Apenas isso, sem critérios outros de permissão,
conforme implementado em DocumentoAdministrativoCrud
"""
qs = super().get_queryset()
if self.request.user.is_anonymous:
qs = qs.exclude(restrito=True)
return qs
@customize(DocumentoAcessorioAdministrativo)
class _DocumentoAcessorioAdministrativoViewSet:
permission_classes = (
_DocumentoAdministrativoViewSet.DocumentoAdministrativoPermission, )
def get_queryset(self):
qs = super().get_queryset()
if self.request.user.is_anonymous:
qs = qs.exclude(documento__restrito=True)
return qs
@customize(TramitacaoAdministrativo)
class _TramitacaoAdministrativoViewSet(BusinessRulesNotImplementedMixin):
# TODO: Implementar regras de manutenção das tramitações de docs adms
permission_classes = (
_DocumentoAdministrativoViewSet.DocumentoAdministrativoPermission, )
def get_queryset(self):
qs = super().get_queryset()
if self.request.user.is_anonymous:
qs = qs.exclude(documento__restrito=True)
return qs
@customize(Anexado)
class _AnexadoViewSet(BusinessRulesNotImplementedMixin):
permission_classes = (
_DocumentoAdministrativoViewSet.DocumentoAdministrativoPermission, )
def get_queryset(self):
qs = super().get_queryset()
if self.request.user.is_anonymous:
qs = qs.exclude(documento__restrito=True)
return qs
@customize(SessaoPlenaria)
class _SessaoPlenariaViewSet:
@action(detail=False)
def years(self, request, *args, **kwargs):
years = choice_anos_com_sessaoplenaria()
serializer = ChoiceSerializer(years, many=True)
return Response(serializer.data)
@action(detail=True)
def expedientes(self, request, *args, **kwargs):
return self.get_expedientes()
@wrapper_queryset_response_for_drf_action(model=ExpedienteSessao)
def get_expedientes(self):
return self.get_queryset().filter(sessao_plenaria_id=self.kwargs['pk'])
@customize(NormaJuridica)
class _NormaJuridicaViewset:
@action(detail=False, methods=['GET'])
def destaques(self, request, *args, **kwargs):
self.queryset = self.get_queryset().filter(norma_de_destaque=True)
return self.list(request, *args, **kwargs)
class AppVersionView(APIView): class AppVersionView(APIView):
permission_classes = (IsAuthenticated,) permission_classes = (IsAuthenticated,)

413
sapl/api/viewset.py

@ -0,0 +1,413 @@
import logging
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Q
from django.utils.decorators import classonlymethod
from django.utils.translation import ugettext_lazy as _
from rest_framework.decorators import action
from rest_framework.response import Response
from sapl.api.core import customize, SaplApiViewSetConstrutor, \
wrapper_queryset_response_for_drf_action, \
BusinessRulesNotImplementedMixin
from sapl.api.core.serializers import ChoiceSerializer
from sapl.api.permissions import SaplModelPermissions
from sapl.api.serializers import ParlamentarSerializerVerbose, \
ParlamentarSerializerPublic
from sapl.base.models import Autor, AppConfig, DOC_ADM_OSTENSIVO
from sapl.materia.models import Proposicao, TipoMateriaLegislativa, \
MateriaLegislativa, Tramitacao
from sapl.norma.models import NormaJuridica
from sapl.parlamentares.models import Mandato, Legislatura
from sapl.parlamentares.models import Parlamentar
from sapl.protocoloadm.models import DocumentoAdministrativo, \
DocumentoAcessorioAdministrativo, TramitacaoAdministrativo, Anexado
from sapl.sessao.models import SessaoPlenaria, ExpedienteSessao
from sapl.utils import models_with_gr_for_model, choice_anos_com_sessaoplenaria
SaplApiViewSetConstrutor = SaplApiViewSetConstrutor.build_class()
@customize(Autor)
class _AutorViewSet:
# Customização para AutorViewSet com implementação de actions específicas
"""
Nesta customização do que foi criado em
SaplApiViewSetConstrutor além do ofertado por
rest_framework.viewsets.ModelViewSet, dentre outras customizações
possíveis, foi adicionado as rotas referentes aos relacionamentos genéricos
* padrão de ModelViewSet
/api/base/autor/ POST - create
/api/base/autor/ GET - list
/api/base/autor/{pk}/ GET - detail
/api/base/autor/{pk}/ PUT - update
/api/base/autor/{pk}/ PATCH - partial_update
/api/base/autor/{pk}/ DELETE - destroy
* rotas desta classe local criadas pelo método build:
/api/base/autor/parlamentar
devolve apenas autores que são parlamentares
/api/base/autor/comissao
devolve apenas autores que são comissões
/api/base/autor/bloco
devolve apenas autores que são blocos parlamentares
/api/base/autor/bancada
devolve apenas autores que são bancadas parlamentares
/api/base/autor/frente
devolve apenas autores que são Frene parlamentares
/api/base/autor/orgao
devolve apenas autores que são Órgãos
"""
def list_for_content_type(self, content_type):
qs = self.get_queryset()
qs = qs.filter(content_type=content_type)
page = self.paginate_queryset(qs)
if page is not None:
serializer = self.serializer_class(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(page, many=True)
return Response(serializer.data)
@classonlymethod
def build(cls):
models_with_gr_for_autor = models_with_gr_for_model(Autor)
for _model in models_with_gr_for_autor:
@action(detail=False, name=_model._meta.model_name)
def actionclass(self, request, *args, **kwargs):
model = getattr(self, self.action)._AutorViewSet__model
content_type = ContentType.objects.get_for_model(model)
return self.list_for_content_type(content_type)
func = actionclass
func.mapping['get'] = func.kwargs['name']
func.url_name = func.kwargs['name']
func.url_path = func.kwargs['name']
func.__name__ = func.kwargs['name']
func.__model = _model
setattr(cls, _model._meta.model_name, func)
return cls
@customize(Parlamentar)
class _ParlamentarViewSet:
class ParlamentarPermission(SaplModelPermissions):
def has_permission(self, request, view):
if request.method == 'GET':
return True
else:
perm = super().has_permission(request, view)
return perm
permission_classes = (ParlamentarPermission,)
def get_serializer(self, *args, **kwargs):
if not self.request.user.has_perm('parlamentares.add_parlamentar'):
self.serializer_class = ParlamentarSerializerPublic
return super().get_serializer(*args, **kwargs)
@action(detail=True)
def proposicoes(self, request, *args, **kwargs):
"""
Lista de proposições públicas de parlamentar específico
:param int id: - Identificador do parlamentar que se quer recuperar as proposições
:return: uma lista de proposições
"""
# /api/parlamentares/parlamentar/{id}/proposicoes/
# recupera proposições enviadas e incorporadas do parlamentar
# deve coincidir com
# /parlamentar/{pk}/proposicao
return self.get_proposicoes(**kwargs)
@wrapper_queryset_response_for_drf_action(model=Proposicao)
def get_proposicoes(self, **kwargs):
return self.get_queryset().filter(
data_envio__isnull=False,
data_recebimento__isnull=False,
cancelado=False,
autor__object_id=kwargs['pk'],
autor__content_type=ContentType.objects.get_for_model(Parlamentar)
)
@action(detail=False, methods=['GET'])
def search_parlamentares(self, request, *args, **kwargs):
nome = request.query_params.get('nome_parlamentar', '')
parlamentares = Parlamentar.objects.filter(
nome_parlamentar__icontains=nome)
serializer_class = ParlamentarSerializerVerbose(
parlamentares, many=True, context={'request': request})
return Response(serializer_class.data)
@customize(Legislatura)
class _LegislaturaViewSet:
@action(detail=True)
def parlamentares(self, request, *args, **kwargs):
def get_serializer_context():
return {
'request': self.request, 'legislatura': kwargs['pk']
}
def get_serializer_class():
return ParlamentarSerializerVerbose
self.get_serializer_context = get_serializer_context
self.get_serializer_class = get_serializer_class
return self.get_parlamentares()
@wrapper_queryset_response_for_drf_action(model=Parlamentar)
def get_parlamentares(self):
try:
legislatura = Legislatura.objects.get(pk=self.kwargs['pk'])
except ObjectDoesNotExist:
return Response("")
filter_params = {
'legislatura': legislatura,
'data_inicio_mandato__gte': legislatura.data_inicio,
'data_fim_mandato__lte': legislatura.data_fim,
}
mandatos = Mandato.objects.filter(
**filter_params).order_by('-data_inicio_mandato')
parlamentares = self.get_queryset().filter(
mandato__in=mandatos).distinct()
return parlamentares
@customize(Proposicao)
class _ProposicaoViewSet:
"""
list:
Retorna lista de Proposições
* Permissões:
* Usuário Dono:
* Pode listar todas suas Proposições
* Usuário Conectado ou Anônimo:
* Pode listar todas as Proposições incorporadas
retrieve:
Retorna uma proposição passada pelo 'id'
* Permissões:
* Usuário Dono:
* Pode recuperar qualquer de suas Proposições
* Usuário Conectado ou Anônimo:
* Pode recuperar qualquer das proposições incorporadas
"""
class ProposicaoPermission(SaplModelPermissions):
def has_permission(self, request, view):
if request.method == 'GET':
return True
# se a solicitação é list ou detail, libera o teste de permissão
# e deixa o get_queryset filtrar de acordo com a regra de
# visibilidade das proposições, ou seja:
# 1. proposição incorporada é proposição pública
# 2. não incorporada só o autor pode ver
else:
perm = super().has_permission(request, view)
return perm
# não é list ou detail, então passa pelas regras de permissão e,
# depois disso ainda passa pelo filtro de get_queryset
permission_classes = (ProposicaoPermission,)
def get_queryset(self):
qs = super().get_queryset()
q = Q(data_recebimento__isnull=False, object_id__isnull=False)
if not self.request.user.is_anonymous:
autor_do_usuario_logado = self.request.user.autor_set.first()
# se usuário logado é operador de algum autor
if autor_do_usuario_logado:
q = Q(autor=autor_do_usuario_logado)
# se é operador de protocolo, ve qualquer coisa enviada
if self.request.user.has_perm('protocoloadm.list_protocolo'):
q = Q(data_envio__isnull=False) | Q(
data_devolucao__isnull=False)
qs = qs.filter(q)
return qs
@customize(MateriaLegislativa)
class _MateriaLegislativaViewSet:
class Meta:
ordering = ['-ano', 'tipo', 'numero']
@action(detail=True, methods=['GET'])
def ultima_tramitacao(self, request, *args, **kwargs):
materia = self.get_object()
if not materia.tramitacao_set.exists():
return Response({})
ultima_tramitacao = materia.tramitacao_set.order_by(
'-data_tramitacao', '-id').first()
serializer_class = SaplApiViewSetConstrutor.get_class_for_model(
Tramitacao).serializer_class(ultima_tramitacao)
return Response(serializer_class.data)
@action(detail=True, methods=['GET'])
def anexadas(self, request, *args, **kwargs):
self.queryset = self.get_object().anexadas.all()
return self.list(request, *args, **kwargs)
@customize(TipoMateriaLegislativa)
class _TipoMateriaLegislativaViewSet:
@action(detail=True, methods=['POST'])
def change_position(self, request, *args, **kwargs):
result = {
'status': 200,
'message': 'OK'
}
d = request.data
if 'pos_ini' in d and 'pos_fim' in d:
if d['pos_ini'] != d['pos_fim']:
pk = kwargs['pk']
TipoMateriaLegislativa.objects.reposicione(pk, d['pos_fim'])
return Response(result)
@customize(DocumentoAdministrativo)
class _DocumentoAdministrativoViewSet:
class DocumentoAdministrativoPermission(SaplModelPermissions):
def has_permission(self, request, view):
if request.method == 'GET':
comportamento = AppConfig.attr('documentos_administrativos')
if comportamento == DOC_ADM_OSTENSIVO:
return True
"""
Diante da lógica implementada na manutenção de documentos
administrativos:
- Se o comportamento é doc adm ostensivo, deve passar pelo
teste de permissões sem avaliá-las
- se o comportamento é doc adm restritivo, deve passar pelo
teste de permissões avaliando-as
"""
return super().has_permission(request, view)
permission_classes = (DocumentoAdministrativoPermission,)
def get_queryset(self):
"""
mesmo tendo passado pelo teste de permissões, deve ser filtrado,
pelo campo restrito. Sendo este igual a True, disponibilizar apenas
a um usuário conectado. Apenas isso, sem critérios outros de permissão,
conforme implementado em DocumentoAdministrativoCrud
"""
qs = super().get_queryset()
if self.request.user.is_anonymous:
qs = qs.exclude(restrito=True)
return qs
@customize(DocumentoAcessorioAdministrativo)
class _DocumentoAcessorioAdministrativoViewSet:
permission_classes = (
_DocumentoAdministrativoViewSet.DocumentoAdministrativoPermission,)
def get_queryset(self):
qs = super().get_queryset()
if self.request.user.is_anonymous:
qs = qs.exclude(documento__restrito=True)
return qs
@customize(TramitacaoAdministrativo)
class _TramitacaoAdministrativoViewSet(BusinessRulesNotImplementedMixin):
# TODO: Implementar regras de manutenção das tramitações de docs adms
permission_classes = (
_DocumentoAdministrativoViewSet.DocumentoAdministrativoPermission,)
def get_queryset(self):
qs = super().get_queryset()
if self.request.user.is_anonymous:
qs = qs.exclude(documento__restrito=True)
return qs
@customize(Anexado)
class _AnexadoViewSet(BusinessRulesNotImplementedMixin):
permission_classes = (
_DocumentoAdministrativoViewSet.DocumentoAdministrativoPermission,)
def get_queryset(self):
qs = super().get_queryset()
if self.request.user.is_anonymous:
qs = qs.exclude(documento__restrito=True)
return qs
@customize(SessaoPlenaria)
class _SessaoPlenariaViewSet:
@action(detail=False)
def years(self, request, *args, **kwargs):
years = choice_anos_com_sessaoplenaria()
serializer = ChoiceSerializer(years, many=True)
return Response(serializer.data)
@action(detail=True)
def expedientes(self, request, *args, **kwargs):
return self.get_expedientes()
@wrapper_queryset_response_for_drf_action(model=ExpedienteSessao)
def get_expedientes(self):
return self.get_queryset().filter(sessao_plenaria_id=self.kwargs['pk'])
@customize(NormaJuridica)
class _NormaJuridicaViewset:
@action(detail=False, methods=['GET'])
def destaques(self, request, *args, **kwargs):
self.queryset = self.get_queryset().filter(norma_de_destaque=True)
return self.list(request, *args, **kwargs)

2
sapl/audiencia/views.py

@ -37,7 +37,7 @@ class AudienciaCrud(Crud):
coluna_materia = row[2] # Se mudar a ordem de listagem, mudar aqui. coluna_materia = row[2] # Se mudar a ordem de listagem, mudar aqui.
if coluna_materia[0]: if coluna_materia[0]:
materia = audiencia_materia[audiencia_id][0] materia = audiencia_materia[audiencia_id][0]
if materia: if materia is not None:
url_materia = reverse('sapl.materia:materialegislativa_detail', kwargs={'pk': materia.id}) url_materia = reverse('sapl.materia:materialegislativa_detail', kwargs={'pk': materia.id})
else: else:
url_materia = None url_materia = None

55
sapl/base/forms.py

@ -2,6 +2,7 @@ import logging
import os import os
from crispy_forms.bootstrap import FieldWithButtons, InlineRadios, StrictButton, FormActions from crispy_forms.bootstrap import FieldWithButtons, InlineRadios, StrictButton, FormActions
from crispy_forms.helper import FormHelper
from crispy_forms.layout import HTML, Button, Div, Field, Fieldset, Layout, Row, Submit from crispy_forms.layout import HTML, Button, Div, Field, Fieldset, Layout, Row, Submit
from django import forms from django import forms
from django.conf import settings from django.conf import settings
@ -17,6 +18,7 @@ from django.utils import timezone
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
import django_filters import django_filters
from haystack.forms import ModelSearchForm
from sapl.audiencia.models import AudienciaPublica from sapl.audiencia.models import AudienciaPublica
from sapl.base.models import Autor, TipoAutor, OperadorAutor from sapl.base.models import Autor, TipoAutor, OperadorAutor
@ -37,7 +39,7 @@ from sapl.utils import (autor_label, autor_modal, ChoiceWithoutValidationField,
choice_anos_com_normas, choice_anos_com_materias, choice_anos_com_normas, choice_anos_com_materias,
FilterOverridesMetaMixin, FileFieldCheckMixin, FilterOverridesMetaMixin, FileFieldCheckMixin,
ImageThumbnailFileInput, qs_override_django_filter, ImageThumbnailFileInput, qs_override_django_filter,
RANGE_ANOS, YES_NO_CHOICES, RANGE_ANOS, YES_NO_CHOICES, choice_tipos_normas,
GoogleRecapthaMixin, parlamentares_ativos) GoogleRecapthaMixin, parlamentares_ativos)
from .models import AppConfig, CasaLegislativa from .models import AppConfig, CasaLegislativa
@ -879,6 +881,11 @@ class RelatorioNormasMesFilterSet(django_filters.FilterSet):
choices=choice_anos_com_normas, choices=choice_anos_com_normas,
initial=ultimo_ano_com_norma) initial=ultimo_ano_com_norma)
tipo = django_filters.ChoiceFilter(required=False,
label='Tipo Norma',
choices=choice_tipos_normas,
initial=0)
class Meta: class Meta:
model = NormaJuridica model = NormaJuridica
fields = ['ano'] fields = ['ano']
@ -890,7 +897,7 @@ class RelatorioNormasMesFilterSet(django_filters.FilterSet):
self.filters['ano'].label = 'Ano' self.filters['ano'].label = 'Ano'
self.form.fields['ano'].required = True self.form.fields['ano'].required = True
row1 = to_row([('ano', 12)]) row1 = to_row([('ano', 6), ('tipo', 6)])
buttons = FormActions( buttons = FormActions(
*[ *[
@ -969,6 +976,11 @@ class RelatorioNormasVigenciaFilterSet(django_filters.FilterSet):
choices=choice_anos_com_normas, choices=choice_anos_com_normas,
initial=ultimo_ano_com_norma) initial=ultimo_ano_com_norma)
tipo = django_filters.ChoiceFilter(required=False,
label='Tipo Norma',
choices=choice_tipos_normas,
initial=0)
vigencia = forms.ChoiceField( vigencia = forms.ChoiceField(
label=_('Vigência'), label=_('Vigência'),
choices=[(True, "Vigente"), (False, "Não vigente")], choices=[(True, "Vigente"), (False, "Não vigente")],
@ -984,7 +996,7 @@ class RelatorioNormasVigenciaFilterSet(django_filters.FilterSet):
self.form.fields['ano'].required = True self.form.fields['ano'].required = True
self.form.fields['vigencia'] = self.vigencia self.form.fields['vigencia'] = self.vigencia
row1 = to_row([('ano', 12)]) row1 = to_row([('ano', 6), ('tipo', 6)])
row2 = to_row([('vigencia', 12)]) row2 = to_row([('vigencia', 12)])
buttons = FormActions( buttons = FormActions(
@ -1150,6 +1162,10 @@ class RelatorioHistoricoTramitacaoFilterSet(django_filters.FilterSet):
class RelatorioDataFimPrazoTramitacaoFilterSet(django_filters.FilterSet): class RelatorioDataFimPrazoTramitacaoFilterSet(django_filters.FilterSet):
ano = django_filters.ChoiceFilter(required=False,
label='Ano da Matéria',
choices=choice_anos_com_materias)
@property @property
def qs(self): def qs(self):
parent = super(RelatorioDataFimPrazoTramitacaoFilterSet, self).qs parent = super(RelatorioDataFimPrazoTramitacaoFilterSet, self).qs
@ -1171,10 +1187,11 @@ class RelatorioDataFimPrazoTramitacaoFilterSet(django_filters.FilterSet):
self.filters['tramitacao__unidade_tramitacao_destino'].label = 'Unidade Destino' self.filters['tramitacao__unidade_tramitacao_destino'].label = 'Unidade Destino'
self.filters['tramitacao__status'].label = 'Status de tramitação' self.filters['tramitacao__status'].label = 'Status de tramitação'
row1 = to_row([('tramitacao__data_fim_prazo', 12)]) row1 = to_row([('ano', 12)])
row2 = to_row([('tramitacao__unidade_tramitacao_local', 6), row2 = to_row([('tramitacao__data_fim_prazo', 12)])
row3 = to_row([('tramitacao__unidade_tramitacao_local', 6),
('tramitacao__unidade_tramitacao_destino', 6)]) ('tramitacao__unidade_tramitacao_destino', 6)])
row3 = to_row( row4 = to_row(
[('tipo', 6), [('tipo', 6),
('tramitacao__status', 6)]) ('tramitacao__status', 6)])
@ -1196,7 +1213,7 @@ class RelatorioDataFimPrazoTramitacaoFilterSet(django_filters.FilterSet):
self.form.helper.form_method = 'GET' self.form.helper.form_method = 'GET'
self.form.helper.layout = Layout( self.form.helper.layout = Layout(
Fieldset(_('Tramitações'), Fieldset(_('Tramitações'),
row1, row2, row3, row1, row2, row3, row4,
buttons, ) buttons, )
) )
@ -1561,11 +1578,13 @@ class ConfiguracoesAppForm(ModelForm):
'assinatura_ata', 'assinatura_ata',
'estatisticas_acesso_normas', 'estatisticas_acesso_normas',
'escolher_numero_materia_proposicao', 'escolher_numero_materia_proposicao',
'tramitacao_origem_fixa',
'tramitacao_materia', 'tramitacao_materia',
'tramitacao_documento', 'tramitacao_documento',
'google_recaptcha_site_key', 'google_recaptcha_site_key',
'google_recaptcha_secret_key', 'google_recaptcha_secret_key',
'sapl_as_sapn'] 'sapl_as_sapn',
'identificacao_de_documentos']
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(ConfiguracoesAppForm, self).__init__(*args, **kwargs) super(ConfiguracoesAppForm, self).__init__(*args, **kwargs)
@ -1888,3 +1907,23 @@ class RelatorioNormasPorAutorFilterSet(django_filters.FilterSet):
row3, row3,
form_actions(label='Pesquisar')) form_actions(label='Pesquisar'))
) )
class SaplSearchForm(ModelSearchForm):
def search(self):
sqs = super().search()
return sqs.order_by('-last_update')
"""def get_models(self):
Return a list of the selected models.
search_models = []
if self.is_valid():
for model in self.cleaned_data['models']:
search_models.append(haystack_get_model(*model.split('.')))
return search_models
return ModelSearchForm.get_models(self)"""

19
sapl/base/migrations/0048_appconfig_tramitacao_origem_fixa.py

@ -0,0 +1,19 @@
# Generated by Django 2.2.28 on 2022-06-27 11:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('base', '0047_auto_20210315_1522'),
]
operations = [
migrations.AddField(
model_name='appconfig',
name='tramitacao_origem_fixa',
field=models.BooleanField(choices=[(True, 'Sim'), (False, 'Não')], default=True,
verbose_name='Fixar Origem das tramitações como sendo a tramitação de destino da última tramitação?'),
),
]

18
sapl/base/migrations/0049_auto_20220728_2029.py

@ -0,0 +1,18 @@
# Generated by Django 2.2.20 on 2022-07-28 23:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('base', '0048_appconfig_tramitacao_origem_fixa'),
]
operations = [
migrations.AlterField(
model_name='appconfig',
name='tramitacao_origem_fixa',
field=models.BooleanField(choices=[(True, 'Sim'), (False, 'Não')], default=True, help_text='Ao utilizar a opção NÂO, você compreende que os controles de origem e destino das tramitações são anulados, podendo seu operador registrar quaisquer origem e destino para as tramitações. Se você colocar Não, fizer tramitações aleatórias e voltar para SIM, o destino da tramitação mais recente será utilizado para a origem de uma nova inserção!', verbose_name='Fixar origem de novas tramitações como sendo a tramitação de destino da última tramitação?'),
),
]

31
sapl/base/migrations/0050_metadata.py

@ -0,0 +1,31 @@
# Generated by Django 2.2.20 on 2022-07-29 01:02
import django.contrib.postgres.fields.jsonb
import django.core.serializers.json
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('base', '0049_auto_20220728_2029'),
]
operations = [
migrations.CreateModel(
name='Metadata',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('object_id', models.PositiveIntegerField(blank=True, default=None, null=True)),
('metadata', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=None, encoder=django.core.serializers.json.DjangoJSONEncoder, null=True, verbose_name='Metadados')),
('content_type', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.PROTECT, to='contenttypes.ContentType')),
],
options={
'verbose_name': 'Metadado',
'verbose_name_plural': 'Metadados',
'unique_together': {('content_type', 'object_id')},
},
),
]

23
sapl/base/migrations/0051_auto_20220814_2138.py

@ -0,0 +1,23 @@
# Generated by Django 2.2.28 on 2022-08-15 00:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('base', '0050_metadata'),
]
operations = [
migrations.AddField(
model_name='appconfig',
name='identificacao_de_documentos',
field=models.CharField(default='{sigla}{numero}/{ano}{-}{complemento} - {nome}', help_text='\n Como mostrar a identificação dos documentos administrativos?\n Você pode usar um conjunto de combinações que pretender.\n Ao fazer sua edição, será mostrado logo abaixo o último documento cadastrado, como exemplo de resultado de sua edição.\n Em caso de erro, nenhum documento será mostrado e aparecerá apenas o formato padrão mínimo, que é este: "{sigla}{numero}/{ano}{-}{complemento} - {nome}".\n Muito importante, use as chaves "{}", sem elas, você estará inserindo um texto qualquer e não o valor de um campo.\n Você pode combinar as seguintes campos: {sigla} {nome} {numero} {ano} {complemento} {assunto}\n Ainda pode ser usado {/}, {-}, {.} se você quiser que uma barra, traço, ou ponto\n seja adicionado apenas se o próximo campo que será usado tenha algum conteúdo\n (não use dois destes destes condicionais em sequência, somente o último será considerado).\n ', max_length=254, verbose_name='Formato da identificação dos documentos'),
),
migrations.AlterField(
model_name='appconfig',
name='protocolo_manual',
field=models.BooleanField(choices=[(True, 'Sim'), (False, 'Não')], default=False, verbose_name='Permitir informe manual de data e hora de protocolo?'),
),
]

199
sapl/base/models.py

@ -1,5 +1,8 @@
from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.fields.jsonb import JSONField
from django.core.cache import cache
from django.core.serializers.json import DjangoJSONEncoder
from django.db import models from django.db import models
from django.db.models.deletion import CASCADE from django.db.models.deletion import CASCADE
from django.db.models.signals import post_migrate from django.db.models.signals import post_migrate
@ -91,63 +94,124 @@ class AppConfig(models.Model):
('N', _('Nunca Protocolar ao incorporar uma proposição')), ('N', _('Nunca Protocolar ao incorporar uma proposição')),
) )
documentos_administrativos = models.CharField( # MANTENHA A SEQUÊNCIA EQUIVALENTE COM /sapl/templates/base/layout.yaml
max_length=1, # AppConfig:
verbose_name=_('Visibilidade dos Documentos Administrativos'),
choices=TIPO_DOCUMENTO_ADMINISTRATIVO, default='O')
estatisticas_acesso_normas = models.CharField( # CONFIGURAÇÕES GERAIS
# Linha 1 ------------
esfera_federacao = models.CharField(
max_length=1, max_length=1,
verbose_name=_('Estatísticas de acesso a normas'), blank=True,
choices=RELATORIO_ATOS_ACESSADOS, default='N') default="",
verbose_name=_('Esfera Federação'),
choices=ESFERA_FEDERACAO_CHOICES)
sapl_as_sapn = models.BooleanField(
verbose_name=_(
'Utilizar SAPL como SAPN?'),
choices=YES_NO_CHOICES, default=False)
sequencia_numeracao_proposicao = models.CharField( # MÓDULO PARLAMENTARES
max_length=1,
verbose_name=_('Sequência de numeração de proposições'),
choices=SEQUENCIA_NUMERACAO_PROPOSICAO, default='A')
# MÓDULO MESA DIRETORA
# MÓDULO COMISSÕES
# MÓDULO BANCADAS PARLAMENTARES
# MÓDULO DOCUMENTOS ADMINISTRATIVOS
# Linha 1 -------------------------
documentos_administrativos = models.CharField(
max_length=1,
verbose_name=_('Visibilidade dos Documentos Administrativos'),
choices=TIPO_DOCUMENTO_ADMINISTRATIVO, default='O')
tramitacao_documento = models.BooleanField(
verbose_name=_(
'Tramitar documentos anexados junto com os documentos principais?'),
choices=YES_NO_CHOICES, default=True)
# Linha 2 -------------------------
protocolo_manual = models.BooleanField(
verbose_name=_('Permitir informe manual de data e hora de protocolo?'),
choices=YES_NO_CHOICES, default=False)
sequencia_numeracao_protocolo = models.CharField( sequencia_numeracao_protocolo = models.CharField(
max_length=1, max_length=1,
verbose_name=_('Sequência de numeração de protocolos'), verbose_name=_('Sequência de numeração de protocolos'),
choices=SEQUENCIA_NUMERACAO_PROTOCOLO, default='A') choices=SEQUENCIA_NUMERACAO_PROTOCOLO, default='A')
inicio_numeracao_protocolo = models.PositiveIntegerField( inicio_numeracao_protocolo = models.PositiveIntegerField(
verbose_name=_('Início da numeração de protocolo'), verbose_name=_('Início da numeração de protocolo'),
default=1 default=1
) )
# Linha 3 -------------------------
identificacao_de_documentos = models.CharField(
max_length=254,
verbose_name=_('Formato da identificação dos documentos'),
default='{sigla}{numero}/{ano}{-}{complemento} - {nome}',
help_text="""
Como mostrar a identificação dos documentos administrativos?
Você pode usar um conjunto de combinações que pretender.
Ao fazer sua edição, será mostrado logo abaixo o último documento cadastrado, como exemplo de resultado de sua edição.
Em caso de erro, nenhum documento será mostrado e aparecerá apenas o formato padrão mínimo, que é este: "{sigla}{numero}/{ano}{-}{complemento} - {nome}".
Muito importante, use as chaves "{}", sem elas, você estará inserindo um texto qualquer e não o valor de um campo.
Você pode combinar as seguintes campos: {sigla} {nome} {numero} {ano} {complemento} {assunto}
Ainda pode ser usado {/}, {-}, {.} se você quiser que uma barra, traço, ou ponto
seja adicionado apenas se o próximo campo que será usado tenha algum conteúdo
(não use dois destes destes condicionais em sequência, somente o último será considerado).
"""
)
esfera_federacao = models.CharField( # MÓDULO PROPOSIÇÕES
# Linha 1 ----------
sequencia_numeracao_proposicao = models.CharField(
max_length=1, max_length=1,
blank=True, verbose_name=_('Sequência de numeração de proposições'),
default="", choices=SEQUENCIA_NUMERACAO_PROPOSICAO, default='A')
verbose_name=_('Esfera Federação'), receber_recibo_proposicao = models.BooleanField(
choices=ESFERA_FEDERACAO_CHOICES) verbose_name=_('Protocolar proposição somente com recibo?'),
choices=YES_NO_CHOICES, default=True)
proposicao_incorporacao_obrigatoria = models.CharField(
verbose_name=_('Regra de incorporação de proposições e protocolo'),
max_length=1, choices=POLITICA_PROTOCOLO_CHOICES, default='O')
escolher_numero_materia_proposicao = models.BooleanField(
verbose_name=_(
'Indicar número da matéria a ser gerada na proposição?'),
choices=YES_NO_CHOICES, default=False)
# TODO: a ser implementado na versão 3.2 # MÓDULO MATÉRIA LEGISLATIVA
# painel_aberto = models.BooleanField( # Linha 1 ------------------
# verbose_name=_('Painel aberto para usuário anônimo'), tramitacao_origem_fixa = models.BooleanField(
# choices=YES_NO_CHOICES, default=False) verbose_name=_(
'Fixar origem de novas tramitações como sendo a tramitação de destino da última tramitação?'),
choices=YES_NO_CHOICES,
default=True,
help_text=_('Ao utilizar a opção NÂO, você compreende que os controles '
'de origem e destino das tramitações são anulados, '
'podendo seu operador registrar quaisquer origem e '
'destino para as tramitações. Se você colocar Não, '
'fizer tramitações aleatórias e voltar para SIM, '
'o destino da tramitação mais recente será utilizado '
'para a origem de uma nova inserção!'))
tramitacao_materia = models.BooleanField(
verbose_name=_(
'Tramitar matérias anexadas junto com as matérias principais?'),
choices=YES_NO_CHOICES, default=True)
# MÓDULO NORMAS JURÍDICAS
# MÓDULO TEXTOS ARTICULADOS
# Linha 1 -----------------
texto_articulado_proposicao = models.BooleanField( texto_articulado_proposicao = models.BooleanField(
verbose_name=_('Usar Textos Articulados para Proposições'), verbose_name=_('Usar Textos Articulados para Proposições'),
choices=YES_NO_CHOICES, default=False) choices=YES_NO_CHOICES, default=False)
texto_articulado_materia = models.BooleanField( texto_articulado_materia = models.BooleanField(
verbose_name=_('Usar Textos Articulados para Matérias'), verbose_name=_('Usar Textos Articulados para Matérias'),
choices=YES_NO_CHOICES, default=False) choices=YES_NO_CHOICES, default=False)
texto_articulado_norma = models.BooleanField( texto_articulado_norma = models.BooleanField(
verbose_name=_('Usar Textos Articulados para Normas'), verbose_name=_('Usar Textos Articulados para Normas'),
choices=YES_NO_CHOICES, default=True) choices=YES_NO_CHOICES, default=True)
proposicao_incorporacao_obrigatoria = models.CharField( # MÓDULO SESSÃO PLENÁRIA
verbose_name=_('Regra de incorporação de proposições e protocolo'),
max_length=1, choices=POLITICA_PROTOCOLO_CHOICES, default='O')
assinatura_ata = models.CharField( assinatura_ata = models.CharField(
verbose_name=_('Quem deve assinar a ata'), verbose_name=_('Quem deve assinar a ata'),
max_length=1, choices=ASSINATURA_ATA_CHOICES, default='T') max_length=1, choices=ASSINATURA_ATA_CHOICES, default='T')
# MÓDULO PAINEL
cronometro_discurso = models.DurationField( cronometro_discurso = models.DurationField(
verbose_name=_('Cronômetro do Discurso'), verbose_name=_('Cronômetro do Discurso'),
blank=True, blank=True,
@ -172,28 +236,20 @@ class AppConfig(models.Model):
default=False, default=False,
verbose_name=_('Mostrar brasão da Casa no painel?')) verbose_name=_('Mostrar brasão da Casa no painel?'))
receber_recibo_proposicao = models.BooleanField( # MÓDULO ESTATÍSTICAS DE ACESSO
verbose_name=_('Protocolar proposição somente com recibo?'), estatisticas_acesso_normas = models.CharField(
choices=YES_NO_CHOICES, default=True) max_length=1,
verbose_name=_('Estatísticas de acesso a normas'),
protocolo_manual = models.BooleanField( choices=RELATORIO_ATOS_ACESSADOS, default='N')
verbose_name=_('Informar data e hora de protocolo?'),
choices=YES_NO_CHOICES, default=False)
escolher_numero_materia_proposicao = models.BooleanField( # MÓDULO SEGURANÇA
verbose_name=_(
'Indicar número da matéria a ser gerada na proposição?'),
choices=YES_NO_CHOICES, default=False)
tramitacao_materia = models.BooleanField( # MÓDULO LEXML
verbose_name=_(
'Tramitar matérias anexadas junto com as matérias principais?'),
choices=YES_NO_CHOICES, default=True)
tramitacao_documento = models.BooleanField( # TODO: a ser implementado na versão 3.2
verbose_name=_( # painel_aberto = models.BooleanField(
'Tramitar documentos anexados junto com os documentos principais?'), # verbose_name=_('Painel aberto para usuário anônimo'),
choices=YES_NO_CHOICES, default=True) # choices=YES_NO_CHOICES, default=False)
google_recaptcha_site_key = models.CharField( google_recaptcha_site_key = models.CharField(
verbose_name=_('Chave pública gerada pelo Google Recaptcha'), verbose_name=_('Chave pública gerada pelo Google Recaptcha'),
@ -202,11 +258,6 @@ class AppConfig(models.Model):
verbose_name=_('Chave privada gerada pelo Google Recaptcha'), verbose_name=_('Chave privada gerada pelo Google Recaptcha'),
max_length=256, default='') max_length=256, default='')
sapl_as_sapn = models.BooleanField(
verbose_name=_(
'Utilizar SAPL como SAPN?'),
choices=YES_NO_CHOICES, default=False)
class Meta: class Meta:
verbose_name = _('Configurações da Aplicação') verbose_name = _('Configurações da Aplicação')
verbose_name_plural = _('Configurações da Aplicação') verbose_name_plural = _('Configurações da Aplicação')
@ -216,15 +267,31 @@ class AppConfig(models.Model):
) )
ordering = ('-id',) ordering = ('-id',)
def save(self, force_insert=False, force_update=False, using=None,
update_fields=None):
fields = self._meta.get_fields()
for f in fields:
if f.name != 'id' and not cache.get(f'sapl_{f.name}') is None:
cache.set(f'sapl_{f.name}', getattr(self, f.name), 600)
return models.Model.save(self, force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)
@classmethod @classmethod
def attr(cls, attr): def attr(cls, attr):
value = cache.get(f'sapl_{attr}')
if not value is None:
return value
config = AppConfig.objects.first() config = AppConfig.objects.first()
if not config: if not config:
config = AppConfig() config = AppConfig()
config.save() config.save()
return getattr(config, attr) value = getattr(config, attr)
cache.set(f'sapl_{attr}', value, 600)
return value
def __str__(self): def __str__(self):
return _('Configurações da Aplicação - %(id)s') % { return _('Configurações da Aplicação - %(id)s') % {
@ -376,3 +443,29 @@ class AuditLog(models.Model):
self.model_name, self.model_name,
self.username, self.username,
) )
class Metadata(models.Model):
content_type = models.ForeignKey(
ContentType,
blank=True,
null=True,
default=None,
on_delete=models.PROTECT)
object_id = models.PositiveIntegerField(
blank=True,
null=True,
default=None)
content_object = GenericForeignKey('content_type', 'object_id')
metadata = JSONField(
verbose_name=_('Metadados'),
blank=True, null=True, default=None, encoder=DjangoJSONEncoder)
class Meta:
verbose_name = _('Metadado')
verbose_name_plural = _('Metadados')
unique_together = (('content_type', 'object_id'), )
def __str__(self):
return f'Metadata de {self.content_object}'

17
sapl/base/receivers.py

@ -13,10 +13,23 @@ from django.utils.translation import ugettext_lazy as _
from sapl.base.email_utils import do_envia_email_tramitacao from sapl.base.email_utils import do_envia_email_tramitacao
from sapl.base.models import AuditLog, TipoAutor, Autor from sapl.base.models import AuditLog, TipoAutor, Autor
from sapl.decorators import receiver_multi_senders
from sapl.materia.models import Tramitacao from sapl.materia.models import Tramitacao
from sapl.protocoloadm.models import TramitacaoAdministrativo from sapl.protocoloadm.models import TramitacaoAdministrativo
from sapl.utils import get_base_url, models_with_gr_for_model from sapl.utils import get_base_url, models_with_gr_for_model
models_with_gr_for_autor = models_with_gr_for_model(Autor)
@receiver_multi_senders(post_save, senders=models_with_gr_for_autor)
def handle_update_autor_related(sender, **kwargs):
# for m in models_with_gr_for_autor:
instance = kwargs.get('instance')
autor = instance.autor.first()
if autor:
autor.nome = str(instance)
autor.save()
@receiver(post_save, sender=Tramitacao) @receiver(post_save, sender=Tramitacao)
@receiver(post_save, sender=TramitacaoAdministrativo) @receiver(post_save, sender=TramitacaoAdministrativo)
@ -143,11 +156,9 @@ def audit_log_post_save(sender, **kwargs):
def cria_models_tipo_autor(app_config=None, verbosity=2, interactive=True, def cria_models_tipo_autor(app_config=None, verbosity=2, interactive=True,
using=DEFAULT_DB_ALIAS, **kwargs): using=DEFAULT_DB_ALIAS, **kwargs):
models = models_with_gr_for_model(Autor)
print("\n\033[93m\033[1m{}\033[0m".format( print("\n\033[93m\033[1m{}\033[0m".format(
_('Atualizando registros TipoAutor do SAPL:'))) _('Atualizando registros TipoAutor do SAPL:')))
for model in models: for model in models_with_gr_for_autor:
content_type = ContentType.objects.get_for_model(model) content_type = ContentType.objects.get_for_model(model)
tipo_autor = TipoAutor.objects.filter( tipo_autor = TipoAutor.objects.filter(
content_type=content_type.id).exists() content_type=content_type.id).exists()

26
sapl/base/search_indexes.py

@ -1,5 +1,5 @@
import os.path
import logging import logging
import os.path
from django.db.models import F, Q, Value from django.db.models import F, Q, Value
from django.db.models.fields import TextField from django.db.models.fields import TextField
@ -15,6 +15,7 @@ from sapl.compilacao.models import (STATUS_TA_IMMUTABLE_PUBLIC,
STATUS_TA_PUBLIC, Dispositivo) STATUS_TA_PUBLIC, Dispositivo)
from sapl.materia.models import DocumentoAcessorio, MateriaLegislativa from sapl.materia.models import DocumentoAcessorio, MateriaLegislativa
from sapl.norma.models import NormaJuridica from sapl.norma.models import NormaJuridica
from sapl.sessao.models import SessaoPlenaria
from sapl.settings import SOLR_URL from sapl.settings import SOLR_URL
from sapl.utils import RemoveTag from sapl.utils import RemoveTag
@ -37,9 +38,14 @@ class TextExtractField(CharField):
try: try:
with open(arquivo.path, 'rb') as f: with open(arquivo.path, 'rb') as f:
content = self.backend.extract_file_contents(f) content = self.backend.extract_file_contents(f)
if not content or not content['contents']: data = ''
return '' if content:
data = content['contents'] # update from Solr 7.5 to 8.9
if content['contents']:
data += content['contents']
if content['file']:
data += content['file']
return data
except Exception as e: except Exception as e:
print('erro processando arquivo: ' % arquivo.path) print('erro processando arquivo: ' % arquivo.path)
self.logger.error(arquivo.path) self.logger.error(arquivo.path)
@ -168,3 +174,15 @@ class MateriaLegislativaIndex(DocumentoAcessorioIndex):
('observacao', 'string_extractor'), ('observacao', 'string_extractor'),
) )
) )
class SessaoPlenariaIndex(DocumentoAcessorioIndex):
model = SessaoPlenaria
text = TextExtractField(
document=True, use_template=True,
model_attr=(
('upload_ata', 'file_extractor'),
('upload_anexo', 'file_extractor'),
('upload_pauta', 'file_extractor'),
)
)

30
sapl/base/templatetags/common_tags.py

@ -1,4 +1,3 @@
from _functools import reduce
import re import re
from django import template from django import template
@ -10,6 +9,7 @@ from sapl.base.models import AppConfig
from sapl.materia.models import DocumentoAcessorio, MateriaLegislativa, Proposicao from sapl.materia.models import DocumentoAcessorio, MateriaLegislativa, Proposicao
from sapl.norma.models import NormaJuridica from sapl.norma.models import NormaJuridica
from sapl.parlamentares.models import Filiacao from sapl.parlamentares.models import Filiacao
from sapl.sessao.models import SessaoPlenaria
from sapl.utils import filiacao_data, SEPARADOR_HASH_PROPOSICAO from sapl.utils import filiacao_data, SEPARADOR_HASH_PROPOSICAO
register = template.Library() register = template.Library()
@ -51,6 +51,13 @@ def model_verbose_name_plural(class_name):
model = get_class(class_name) model = get_class(class_name)
return model._meta.verbose_name_plural return model._meta.verbose_name_plural
@register.filter
def format_user(user):
if user.first_name:
return user.first_name + " " + user.last_name + " (" + user.username + ")"
else:
return user.username
@register.filter @register.filter
def meta_model_value(instance, attr): def meta_model_value(instance, attr):
try: try:
@ -103,6 +110,23 @@ def paginacao_limite_superior(pagina):
return int(pagina) * 10 return int(pagina) * 10
@register.filter
def resultado_votacao(materia):
ra = materia.registrovotacao_set.last()
rb = materia.retiradapauta_set.last()
rl = materia.registroleitura_set.last()
if ra:
resultado = ra.tipo_resultado_votacao.nome
elif rb:
resultado = rb.tipo_de_retirada.descricao
elif rl:
resultado = "Matéria lida"
else:
resultado = ""
return resultado
@register.filter @register.filter
def lookup(d, key): def lookup(d, key):
return d[key] if key in d else [] return d[key] if key in d else []
@ -245,6 +269,7 @@ def youtube_url(value):
r = re.findall(youtube_pattern, value) r = re.findall(youtube_pattern, value)
return True if r else False return True if r else False
@register.filter @register.filter
def facebook_url(value): def facebook_url(value):
value = value.lower() value = value.lower()
@ -252,6 +277,7 @@ def facebook_url(value):
r = re.findall(facebook_pattern, value) r = re.findall(facebook_pattern, value)
return True if r else False return True if r else False
@register.filter @register.filter
def youtube_id(value): def youtube_id(value):
from urllib.parse import urlparse, parse_qs from urllib.parse import urlparse, parse_qs
@ -289,6 +315,8 @@ def search_get_model(object):
return 'd' return 'd'
elif type(object) == NormaJuridica: elif type(object) == NormaJuridica:
return 'n' return 'n'
elif type(object) == SessaoPlenaria:
return 's'
return None return None

5
sapl/base/urls.py

@ -14,7 +14,7 @@ from sapl.settings import MEDIA_URL, LOGOUT_REDIRECT_URL
from .apps import AppConfig from .apps import AppConfig
from .forms import LoginForm from .forms import LoginForm
from .views import (AlterarSenha, AppConfigCrud, CasaLegislativaCrud, from .views import (LoginSapl, AlterarSenha, AppConfigCrud, CasaLegislativaCrud,
HelpTopicView, LogotipoView, RelatorioAtasView, HelpTopicView, LogotipoView, RelatorioAtasView,
RelatorioAudienciaView, RelatorioDataFimPrazoTramitacaoView, RelatorioHistoricoTramitacaoView, RelatorioAudienciaView, RelatorioDataFimPrazoTramitacaoView, RelatorioHistoricoTramitacaoView,
RelatorioMateriasPorAnoAutorTipoView, RelatorioMateriasPorAutorView, RelatorioMateriasPorAnoAutorTipoView, RelatorioMateriasPorAutorView,
@ -173,8 +173,7 @@ urlpatterns = [
(TemplateView.as_view(template_name='sistema.html')), (TemplateView.as_view(template_name='sistema.html')),
name='sistema'), name='sistema'),
url(r'^login/$', views.LoginView.as_view(template_name='base/login.html', authentication_form=LoginForm), url(r'^login/$', LoginSapl.as_view(), name='login'),
name='login'),
url(r'^logout/$', views.LogoutView.as_view(), url(r'^logout/$', views.LogoutView.as_view(),
{'next_page': LOGOUT_REDIRECT_URL}, name='logout'), {'next_page': LOGOUT_REDIRECT_URL}, name='logout'),

95
sapl/base/views.py

@ -4,9 +4,11 @@ import datetime
import itertools import itertools
import logging import logging
import os import os
import re
from django.apps.registry import apps
from django.contrib import messages from django.contrib import messages
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model, views
from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from django.contrib.auth.tokens import default_token_generator from django.contrib.auth.tokens import default_token_generator
@ -23,6 +25,7 @@ from django.template import TemplateDoesNotExist
from django.template.loader import get_template from django.template.loader import get_template
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.utils import timezone from django.utils import timezone
from django.utils.decorators import method_decorator
from django.utils.encoding import force_bytes from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -31,12 +34,13 @@ from django.views.generic.base import RedirectView, TemplateView
from django_filters.views import FilterView from django_filters.views import FilterView
from haystack.query import SearchQuerySet from haystack.query import SearchQuerySet
from haystack.views import SearchView from haystack.views import SearchView
from ratelimit.decorators import ratelimit
from sapl import settings from sapl import settings
from sapl.audiencia.models import AudienciaPublica, TipoAudienciaPublica from sapl.audiencia.models import AudienciaPublica, TipoAudienciaPublica
from sapl.base.forms import (AutorForm, TipoAutorForm, AutorFilterSet, RecuperarSenhaForm, from sapl.base.forms import (AutorForm, TipoAutorForm, AutorFilterSet, RecuperarSenhaForm,
NovaSenhaForm, UserAdminForm, NovaSenhaForm, UserAdminForm,
OperadorAutorForm) OperadorAutorForm, LoginForm, SaplSearchForm)
from sapl.base.models import Autor, TipoAutor, OperadorAutor from sapl.base.models import Autor, TipoAutor, OperadorAutor
from sapl.comissoes.models import Comissao, Reuniao from sapl.comissoes.models import Comissao, Reuniao
from sapl.crud.base import CrudAux, make_pagination, Crud,\ from sapl.crud.base import CrudAux, make_pagination, Crud,\
@ -62,7 +66,7 @@ from sapl.settings import EMAIL_SEND_USER
from sapl.utils import (gerar_hash_arquivo, intervalos_tem_intersecao, mail_service_configured, parlamentares_ativos, from sapl.utils import (gerar_hash_arquivo, intervalos_tem_intersecao, mail_service_configured, parlamentares_ativos,
SEPARADOR_HASH_PROPOSICAO, show_results_filter_set, num_materias_por_tipo, SEPARADOR_HASH_PROPOSICAO, show_results_filter_set, num_materias_por_tipo,
google_recaptcha_configured, sapl_as_sapn, google_recaptcha_configured, sapl_as_sapn,
groups_remove_user, groups_add_user) groups_remove_user, groups_add_user, get_client_ip)
from .forms import (AlterarSenhaForm, CasaLegislativaForm, ConfiguracoesAppForm, RelatorioAtasFilterSet, from .forms import (AlterarSenhaForm, CasaLegislativaForm, ConfiguracoesAppForm, RelatorioAtasFilterSet,
RelatorioAudienciaFilterSet, RelatorioDataFimPrazoTramitacaoFilterSet, RelatorioAudienciaFilterSet, RelatorioDataFimPrazoTramitacaoFilterSet,
@ -86,6 +90,15 @@ class IndexView(TemplateView):
return TemplateView.get(self, request, *args, **kwargs) return TemplateView.get(self, request, *args, **kwargs)
@method_decorator(ratelimit(key=lambda group, request: get_client_ip(request),
rate='20/m',
method=ratelimit.UNSAFE,
block=True), name='dispatch')
class LoginSapl(views.LoginView):
template_name = 'base/login.html'
authentication_form = LoginForm
class ConfirmarEmailView(TemplateView): class ConfirmarEmailView(TemplateView):
template_name = "email/confirma.html" template_name = "email/confirma.html"
@ -699,6 +712,12 @@ class RelatorioDataFimPrazoTramitacaoView(RelatorioMixin, FilterView):
context['data_tramitacao'] = (self.request.GET['tramitacao__data_fim_prazo_0'] + ' - ' + context['data_tramitacao'] = (self.request.GET['tramitacao__data_fim_prazo_0'] + ' - ' +
self.request.GET['tramitacao__data_fim_prazo_1']) self.request.GET['tramitacao__data_fim_prazo_1'])
if self.request.GET['ano']:
context['ano'] = self.request.GET['ano']
else:
context['ano'] = ''
if self.request.GET['tipo']: if self.request.GET['tipo']:
tipo = self.request.GET['tipo'] tipo = self.request.GET['tipo']
context['tipo'] = ( context['tipo'] = (
@ -896,7 +915,8 @@ class RelatorioMateriasTramitacaoView(RelatorioMixin, FilterView):
) )
else: else:
context['materia__autor'] = '' context['materia__autor'] = ''
if 'page' in qr:
del qr['page']
context['filter_url'] = ('&' + qr.urlencode()) if len(qr) > 0 else '' context['filter_url'] = ('&' + qr.urlencode()) if len(qr) > 0 else ''
context['show_results'] = show_results_filter_set(qr) context['show_results'] = show_results_filter_set(qr)
@ -1876,6 +1896,9 @@ class UserCrud(Crud):
'usuario', 'groups', 'is_active' 'usuario', 'groups', 'is_active'
] ]
def openapi_url(self):
return ''
def resolve_url(self, suffix, args=None): def resolve_url(self, suffix, args=None):
return reverse('sapl.base:%s' % self.url_name(suffix), return reverse('sapl.base:%s' % self.url_name(suffix),
args=args) args=args)
@ -2079,10 +2102,45 @@ class AppConfigCrud(CrudAux):
kwargs={'pk': app_config.pk})) kwargs={'pk': app_config.pk}))
class UpdateView(CrudAux.UpdateView): class UpdateView(CrudAux.UpdateView):
template_name = 'base/AppConfig.html'
form_class = ConfiguracoesAppForm form_class = ConfiguracoesAppForm
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = self.model._meta.verbose_name
return context
def get(self, request, *args, **kwargs):
if 'jsidd' in request.GET:
return self.json_simular_identificacao_de_documentos(request, *args, **kwargs)
return super().get(request, *args, **kwargs)
def json_simular_identificacao_de_documentos(self, request, *args, **kwargs):
DocumentoAdministrativo = apps.get_model(
'protocoloadm',
'DocumentoAdministrativo'
)
d = DocumentoAdministrativo.objects.order_by('-id').first()
jsidd = request.GET.get('jsidd', '')
values = {
'{sigla}': d.tipo.sigla if d else 'OF',
'{nome}': d.tipo.descricao if d else 'Ofício',
'{numero}': f'{d.numero:0>3}' if d else '001',
'{ano}': f'{d.ano}' if d else str(timezone.now().year),
'{complemento}': d.complemento if d else 'GAB',
'{assunto}': d.assunto if d else 'Simulação de Identificação de Documentos'
}
result = DocumentoAdministrativo.mask_to_str(values, jsidd)
return JsonResponse({
'jsidd': result[0],
'error': list(result[1])
})
def form_valid(self, form): def form_valid(self, form):
numeracao = AppConfig.objects.last().sequencia_numeracao_protocolo numeracao = AppConfig.objects.last().sequencia_numeracao_protocolo
numeracao_antiga = AppConfig.objects.last().inicio_numeracao_protocolo numeracao_antiga = AppConfig.objects.last().inicio_numeracao_protocolo
@ -2151,15 +2209,40 @@ class AppConfigCrud(CrudAux):
class SaplSearchView(SearchView): class SaplSearchView(SearchView):
results_per_page = 10 results_per_page = 10
def __init__(self, template=None, load_all=True, form_class=None, searchqueryset=None, results_per_page=None):
super().__init__(
template=template,
load_all=load_all,
form_class=SaplSearchForm,
searchqueryset=None,
results_per_page=results_per_page
)
def get_context(self): def get_context(self):
context = super(SaplSearchView, self).get_context() context = super(SaplSearchView, self).get_context()
data = self.request.GET or self.request.POST
data = data.copy()
if 'models' in self.request.GET: if 'models' in self.request.GET:
models = self.request.GET.getlist('models') models = self.request.GET.getlist('models')
else: else:
models = [] models = []
context['models'] = '' context['models'] = ''
context['is_paginated'] = True
page_obj = context['page']
context['page_obj'] = page_obj
paginator = context['paginator']
context['page_range'] = make_pagination(
page_obj.number, paginator.num_pages)
if 'page' in data:
del data['page']
context['filter_url'] = (
'&' + data.urlencode()) if len(data) > 0 else ''
for m in models: for m in models:
context['models'] = context['models'] + '&models=' + m context['models'] = context['models'] + '&models=' + m

23
sapl/comissoes/migrations/0028_auto_20220807_2257.py

@ -0,0 +1,23 @@
# Generated by Django 2.2.28 on 2022-08-08 01:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('comissoes', '0027_auto_20210209_1047'),
]
operations = [
migrations.AlterField(
model_name='participacao',
name='motivo_desligamento',
field=models.TextField(blank=True, verbose_name='Motivo Desligamento'),
),
migrations.AlterField(
model_name='participacao',
name='observacao',
field=models.TextField(blank=True, verbose_name='Observação'),
),
]

9
sapl/comissoes/models.py

@ -177,11 +177,10 @@ class Participacao(models.Model): # ComposicaoComissao
data_desligamento = models.DateField(blank=True, data_desligamento = models.DateField(blank=True,
null=True, null=True,
verbose_name=_('Data Desligamento')) verbose_name=_('Data Desligamento'))
motivo_desligamento = models.CharField( motivo_desligamento = models.TextField(
max_length=150, blank=True, blank=True, verbose_name=_('Motivo Desligamento'))
verbose_name=_('Motivo Desligamento')) observacao = models.TextField(
observacao = models.CharField( blank=True, verbose_name=_('Observação'))
max_length=150, blank=True, verbose_name=_('Observação'))
class Meta: class Meta:
verbose_name = _('Participação em Comissão') verbose_name = _('Participação em Comissão')

4
sapl/comissoes/views.py

@ -351,10 +351,12 @@ class AdicionaPautaView(PermissionRequiredMixin, FilterView):
context['root_pk'] = context['object'].comissao.pk context['root_pk'] = context['object'].comissao.pk
qr = self.request.GET.copy() qr = self.request.GET.copy()
materias_pauta = PautaReuniao.objects.filter(reuniao=context['object']) materias_pauta = PautaReuniao.objects.filter(reuniao=context['object'])
nao_listar = [mp.materia.pk for mp in materias_pauta] nao_listar = [mp.materia.pk for mp in materias_pauta]
if not len(qr):
context['object_list'] = []
else:
context['object_list'] = context['object_list'].filter( context['object_list'] = context['object_list'].filter(
tramitacao__unidade_tramitacao_destino__comissao=context['root_pk'] tramitacao__unidade_tramitacao_destino__comissao=context['root_pk']
).exclude(materia__pk__in=nao_listar).order_by( ).exclude(materia__pk__in=nao_listar).order_by(

44
sapl/compilacao/migrations/0019_auto_20220630_1420.py

@ -0,0 +1,44 @@
# Generated by Django 2.2.24 on 2022-06-30 17:20
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('compilacao', '0018_auto_20210227_2152'),
]
operations = [
migrations.AlterField(
model_name='dispositivo',
name='dispositivo_atualizador',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dispositivos_alterados_set', to='compilacao.Dispositivo', verbose_name='Dispositivo Atualizador'),
),
migrations.AlterField(
model_name='dispositivo',
name='dispositivo_pai',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='dispositivos_filhos_set', to='compilacao.Dispositivo', verbose_name='Dispositivo Pai'),
),
migrations.AlterField(
model_name='dispositivo',
name='dispositivo_raiz',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='nodes', to='compilacao.Dispositivo', verbose_name='Dispositivo Raiz'),
),
migrations.AlterField(
model_name='dispositivo',
name='publicacao',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='compilacao.Publicacao', verbose_name='Publicação'),
),
migrations.AlterField(
model_name='dispositivo',
name='ta_publicado',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='dispositivos_alterados_pelo_ta_set', to='compilacao.TextoArticulado', verbose_name='Texto Articulado Publicado'),
),
migrations.AlterField(
model_name='publicacao',
name='ta',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='compilacao.TextoArticulado', verbose_name='Texto Articulado'),
),
]

38
sapl/compilacao/models.py

@ -1,7 +1,7 @@
from django.contrib import messages from django.contrib import messages
from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db import models from django.db import models, transaction
from django.db.models import F, Q from django.db.models import F, Q
from django.db.models.aggregates import Max from django.db.models.aggregates import Max
from django.db.models.deletion import PROTECT from django.db.models.deletion import PROTECT
@ -475,30 +475,26 @@ class TextoArticulado(TimestampedMixin):
view_integracao = view_integracao[0] view_integracao = view_integracao[0]
ta = TextoArticulado.update_or_create(view_integracao, obj) origem = self
destino = TextoArticulado.update_or_create(view_integracao, obj)
dispositivos = Dispositivo.objects.filter(ta=self).order_by('ordem') dispositivos = Dispositivo.objects.filter(ta=origem).order_by('ordem')
with transaction.atomic():
map_ids = {} map_ids = {}
for d in dispositivos: for d in dispositivos:
id_old = d.id id_old = d.id
# TODO
# validar isso: é o suficiente para pegar apenas o texto válido?
# exemplo:
# quando uma matéria for alterada por uma emenda
# ao usar esta função para gerar uma norma deve vir apenas
# o texto válido, compilado...
if d.dispositivo_subsequente: if d.dispositivo_subsequente:
continue continue
d.id = None d.id = None
d.inicio_vigencia = ta.data d.inicio_vigencia = destino.data
d.fim_vigencia = None d.fim_vigencia = None
d.inicio_eficacia = ta.data d.inicio_eficacia = destino.data
d.fim_eficacia = None d.fim_eficacia = None
d.publicacao = None d.publicacao = None
d.ta = ta d.ta = destino
d.ta_publicado = None d.ta_publicado = None
d.dispositivo_subsequente = None d.dispositivo_subsequente = None
d.dispositivo_substituido = None d.dispositivo_substituido = None
@ -507,7 +503,8 @@ class TextoArticulado(TimestampedMixin):
d.save() d.save()
map_ids[id_old] = d.id map_ids[id_old] = d.id
dispositivos = Dispositivo.objects.filter(ta=ta).order_by('ordem') dispositivos = Dispositivo.objects.filter(
ta=destino).order_by('ordem')
for d in dispositivos: for d in dispositivos:
if not d.dispositivo_pai: if not d.dispositivo_pai:
@ -515,8 +512,7 @@ class TextoArticulado(TimestampedMixin):
d.dispositivo_pai_id = map_ids[d.dispositivo_pai_id] d.dispositivo_pai_id = map_ids[d.dispositivo_pai_id]
d.save() d.save()
return destino
return ta
def reagrupar_ordem_de_dispositivos(self): def reagrupar_ordem_de_dispositivos(self):
@ -894,7 +890,7 @@ class Publicacao(TimestampedMixin):
ta = models.ForeignKey( ta = models.ForeignKey(
TextoArticulado, TextoArticulado,
verbose_name=_('Texto Articulado'), verbose_name=_('Texto Articulado'),
on_delete=models.PROTECT on_delete=models.CASCADE
) )
veiculo_publicacao = models.ForeignKey( veiculo_publicacao = models.ForeignKey(
@ -1104,7 +1100,7 @@ class Dispositivo(BaseModel, TimestampedMixin):
null=True, null=True,
default=None, default=None,
verbose_name=_('Publicação'), verbose_name=_('Publicação'),
on_delete=models.PROTECT on_delete=models.SET_NULL,
) )
ta = models.ForeignKey( ta = models.ForeignKey(
@ -1116,7 +1112,7 @@ class Dispositivo(BaseModel, TimestampedMixin):
ta_publicado = models.ForeignKey( ta_publicado = models.ForeignKey(
TextoArticulado, TextoArticulado,
on_delete=models.PROTECT, on_delete=models.CASCADE,
blank=True, blank=True,
null=True, null=True,
default=None, default=None,
@ -1151,7 +1147,7 @@ class Dispositivo(BaseModel, TimestampedMixin):
default=None, default=None,
related_name='dispositivos_filhos_set', related_name='dispositivos_filhos_set',
verbose_name=_('Dispositivo Pai'), verbose_name=_('Dispositivo Pai'),
on_delete=models.PROTECT on_delete=models.CASCADE,
) )
dispositivo_raiz = models.ForeignKey( dispositivo_raiz = models.ForeignKey(
@ -1161,7 +1157,7 @@ class Dispositivo(BaseModel, TimestampedMixin):
default=None, default=None,
related_name='nodes', related_name='nodes',
verbose_name=_('Dispositivo Raiz'), verbose_name=_('Dispositivo Raiz'),
on_delete=models.PROTECT on_delete=models.CASCADE,
) )
dispositivo_vigencia = models.ForeignKey( dispositivo_vigencia = models.ForeignKey(
@ -1181,7 +1177,7 @@ class Dispositivo(BaseModel, TimestampedMixin):
default=None, default=None,
related_name='dispositivos_alterados_set', related_name='dispositivos_alterados_set',
verbose_name=_('Dispositivo Atualizador'), verbose_name=_('Dispositivo Atualizador'),
on_delete=models.PROTECT on_delete=models.SET_NULL,
) )
contagem_continua = models.BooleanField( contagem_continua = models.BooleanField(

80
sapl/compilacao/views.py

@ -6,6 +6,7 @@ import sys
from braces.views import FormMessagesMixin from braces.views import FormMessagesMixin
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from django import forms from django import forms
from django.apps.registry import apps
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin
@ -152,15 +153,26 @@ class IntegracaoTaView(TemplateView):
content_type=related_object_type) content_type=related_object_type)
ta_exists = bool(ta.exists()) ta_exists = bool(ta.exists())
if (ta_exists or self.object = ta = ta.first()
(request.user.has_perm(
ta_perm_edit = (
(
request.user.has_perm(
'compilacao.change_dispositivo_edicao_dinamica') and 'compilacao.change_dispositivo_edicao_dinamica') and
ta_values.get('privacidade', STATUS_TA_EDITION ta_values.get(
) != STATUS_TA_PRIVATE) or 'privacidade',
(request.user.has_perm( STATUS_TA_EDITION
) != STATUS_TA_PRIVATE
) or (
request.user.has_perm(
'compilacao.change_your_dispositivo_edicao_dinamica') and 'compilacao.change_your_dispositivo_edicao_dinamica') and
ta_values.get('privacidade', STATUS_TA_EDITION ta_values.get(
) == STATUS_TA_PRIVATE)): 'privacidade',
STATUS_TA_EDITION
) == STATUS_TA_PRIVATE
)
)
""" """
o texto articulado será criado/atualizado se: o texto articulado será criado/atualizado se:
- texto articulado foi criado. - texto articulado foi criado.
@ -173,13 +185,14 @@ class IntegracaoTaView(TemplateView):
que o texto seja privado e a permissão seja específica para que o texto seja privado e a permissão seja específica para
textos privados. textos privados.
""" """
pass
else: if not ta_exists and not ta_perm_edit:
messages.info(request, _('%s não possui %s.') % ( messages.info(request, _('%s não possui %s.') % (
item, TextoArticulado._meta.verbose_name)) item, TextoArticulado._meta.verbose_name))
return redirect('/message') return redirect('/message')
ta = TextoArticulado.update_or_create(self, item) if ta_perm_edit:
self.object = ta = TextoArticulado.update_or_create(self, item)
if not ta_exists: if not ta_exists:
if ta.editable_only_by_owners and\ if ta.editable_only_by_owners and\
@ -1098,8 +1111,45 @@ class TextEditView(CompMixin, TemplateView):
self.object = self.ta self.object = self.ta
return self.object.has_edit_permission(self.request) return self.object.has_edit_permission(self.request)
def importar_texto_materia(self, request, *args, **kwargs):
rd = redirect(to=reverse_lazy(
'sapl.compilacao:ta_text_edit', kwargs={
'ta_id': self.object.id}))
if self.object.dispositivos_set.count() > 1:
messages.error(
request,
_('Este Texto Articulado possui conteúdo, '
'para fazer a importação você deve deixar '
'apenas uma única Articulação inicial.'))
return rd
materia = self.materia_da_norma_deste_texto_articulado()
if not materia:
messages.error(
request,
_('A Norma [{}] não está vinculada a nenhuma matéria.'.format(self.object.content_object)))
return rd
self.object.dispositivos_set.all().delete()
ta_materia = materia.texto_articulado.first()
try:
ta_materia.clone_for(self.object.content_object)
#TextoArticulado.clone(ta_materia, self.object)
except Exception as e:
messages.error(
request,
_('Ocorreu erro na importação e o procedimento foi cancelado!'))
return rd
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
if 'importar_texto_materia' in request.GET:
return self.importar_texto_materia(request, *args, **kwargs)
if self.object.editing_locked: if self.object.editing_locked:
if 'unlock' not in request.GET: if 'unlock' not in request.GET:
messages.error( messages.error(
@ -1336,6 +1386,16 @@ class TextEditView(CompMixin, TemplateView):
return '' return ''
def materia_da_norma_deste_texto_articulado(self):
NormaJuridica = apps.get_model(
'norma', 'NormaJuridica')
ta = self.ta
if isinstance(ta.content_object, NormaJuridica) and\
ta.content_object.materia:
return ta.content_object.materia
return None
def runBase(self): def runBase(self):
result = Dispositivo.objects.filter(ta_id=self.kwargs['ta_id']) result = Dispositivo.objects.filter(ta_id=self.kwargs['ta_id'])

26
sapl/crispy_layout_mixin.py

@ -6,6 +6,7 @@ from crispy_forms.layout import HTML, Div, Fieldset, Layout, Submit
from django import template from django import template
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.utils import formats from django.utils import formats
from django.utils.encoding import force_text
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
import yaml import yaml
@ -38,7 +39,7 @@ def form_actions(more=[Div(css_class='clearfix')],
label=_('Salvar'), name='salvar', label=_('Salvar'), name='salvar',
css_class='float-right', disabled=True): css_class='float-right', disabled=True):
if disabled: if disabled and force_text(label) != 'Pesquisar':
doubleclick = 'this.form.submit();this.disabled=true;' doubleclick = 'this.form.submit();this.disabled=true;'
else: else:
doubleclick = 'return true;' doubleclick = 'return true;'
@ -169,7 +170,7 @@ def get_field_display(obj, fieldname):
display = '<div class="dont-break-out">{}</div>'.format(display) display = '<div class="dont-break-out">{}</div>'.format(display)
else: else:
display = str(value) display = str(value)
return verbose_name, display return verbose_name, display or '&nbsp;'
class CrispyLayoutFormMixin: class CrispyLayoutFormMixin:
@ -256,21 +257,30 @@ class CrispyLayoutFormMixin:
if '|' in fieldname: if '|' in fieldname:
fieldname, func = tuple(fieldname.split('|')) fieldname, func = tuple(fieldname.split('|'))
try:
verbose_name, field_display = get_field_display(obj, fieldname)
except:
verbose_name, field_display = '', ''
if func: if func:
verbose_name, text = getattr(self, func)(obj, fieldname) verbose_name, field_display = getattr(self, func)(obj, fieldname)
else:
hook_fieldname = 'hook_%s' % fieldname hook_fieldname = 'hook_%s' % fieldname
if hasattr(self, hook_fieldname): if hasattr(self, hook_fieldname):
verbose_name, text = getattr( try:
verbose_name, field_display = getattr(
self, hook_fieldname)(obj, verbose_name=verbose_name, field_display=field_display)
except:
verbose_name, field_display = getattr(
self, hook_fieldname)(obj) self, hook_fieldname)(obj)
else: elif not func:
verbose_name, text = get_field_display(obj, fieldname) verbose_name, field_display = get_field_display(obj, fieldname)
return { return {
'id': fieldname, 'id': fieldname,
'span': span, 'span': span,
'verbose_name': verbose_name, 'verbose_name': verbose_name,
'text': text, 'text': field_display,
} }
def fk_urlize_for_detail(self, obj, fieldname): def fk_urlize_for_detail(self, obj, fieldname):

33
sapl/crud/base.py

@ -24,11 +24,10 @@ from django.views.generic.list import MultipleObjectMixin
from sapl.crispy_layout_mixin import CrispyLayoutFormMixin, get_field_display from sapl.crispy_layout_mixin import CrispyLayoutFormMixin, get_field_display
from sapl.crispy_layout_mixin import SaplFormHelper from sapl.crispy_layout_mixin import SaplFormHelper
from sapl.rules.map_rules import (RP_ADD, RP_CHANGE, RP_DELETE, RP_DETAIL, from sapl.rules import (RP_ADD, RP_CHANGE, RP_DELETE, RP_DETAIL,
RP_LIST) RP_LIST)
from sapl.utils import normalize from sapl.utils import normalize
logger = logging.getLogger(settings.BASE_DIR.name) logger = logging.getLogger(settings.BASE_DIR.name)
ACTION_LIST, ACTION_CREATE, ACTION_DETAIL, ACTION_UPDATE, ACTION_DELETE = \ ACTION_LIST, ACTION_CREATE, ACTION_DETAIL, ACTION_UPDATE, ACTION_DELETE = \
@ -362,6 +361,13 @@ class CrudBaseMixin(CrispyLayoutFormMixin):
if self.request.user.has_perm( if self.request.user.has_perm(
self.permission(RP_DELETE)) else '' self.permission(RP_DELETE)) else ''
@property
def openapi_url(self):
obj = self.crud if hasattr(self, 'crud') else self
o = self.object
url = f'/api/{o._meta.app_label}/{o._meta.model_name}/{o.id}'
return url
def get_template_names(self): def get_template_names(self):
names = super(CrudBaseMixin, self).get_template_names() names = super(CrudBaseMixin, self).get_template_names()
names.append("crud/%s.html" % names.append("crud/%s.html" %
@ -423,14 +429,20 @@ class CrudListView(PermissionRequiredContainerCrudMixin, ListView):
m = f.related_model m = f.related_model
except: except:
f = None f = None
if f:
hook = 'hook_header_{}'.format(''.join(fn)) hook = 'hook_header_{}'.format(''.join(fn))
if hasattr(self, hook): if hasattr(self, hook):
header = getattr(self, hook)() header = getattr(self, hook)()
s.append(header) s.append(force_text(header))
elif f: else:
s.append(force_text(f.verbose_name)) s.append(force_text(f.verbose_name))
else:
hook = 'hook_header_{}'.format(''.join(fn))
if hasattr(self, hook):
header = getattr(self, hook)()
s.append(header)
s = ' / '.join(s) s = ' / '.join(filter(lambda x: x, s))
r.append(s) r.append(s)
return r return r
@ -1113,12 +1125,15 @@ class MasterDetailCrud(Crud):
root_pk = self.kwargs['pk'] if 'pkk' not in self.request.GET\ root_pk = self.kwargs['pk'] if 'pkk' not in self.request.GET\
else self.request.GET['pkk'] else self.request.GET['pkk']
kwargs.setdefault('root_pk', root_pk) kwargs.setdefault('root_pk', root_pk)
context = super(CrudBaseMixin, self).get_context_data(**kwargs)
if parent_object: title = '%s <small>(%s)</small>' % (
context['title'] = '%s <small>(%s)</small>' % ( self.object,
self.object, parent_object) parent_object
) if parent_object else ''
context = super(CrudBaseMixin, self).get_context_data(**kwargs)
if 'title' not in context and title:
context['title'] = title
return context return context
class ListView(Crud.ListView): class ListView(Crud.ListView):

40
sapl/decorators.py

@ -13,6 +13,7 @@ def vigencia_atual(decorated_method):
* A classe precisa conter os atributos 'data_inicio' e 'data_fim'. * A classe precisa conter os atributos 'data_inicio' e 'data_fim'.
* 'data_inicio' e 'data_fim' precisam ser do tipo models.DateField * 'data_inicio' e 'data_fim' precisam ser do tipo models.DateField
""" """
@wraps(decorated_method) @wraps(decorated_method)
def display_atual(self): def display_atual(self):
string_displayed = decorated_method(self) string_displayed = decorated_method(self)
@ -41,3 +42,42 @@ def vigencia_atual(decorated_method):
return string_displayed return string_displayed
return display_atual return display_atual
def receiver_multi_senders(signal, **kwargs):
"""
A decorator for connecting receivers to signals. Used by passing in the
signal (or list of signals) and keyword arguments to connect::
@receiver(post_save, senders=MyModelsList)
def signal_receiver(sender, **kwargs):
...
@receiver([post_save, post_delete], senders=MyModelsList)
def signals_receiver(sender, **kwargs):
...
"""
def _decorator(func):
senders = kwargs.get('senders', [])
if isinstance(signal, (list, tuple)):
if not senders:
for s in signal:
s.connect(func, **kwargs)
else:
senders = kwargs.pop('senders')
for sender in senders:
for s in signal:
s.connect(func, sender=sender, **kwargs)
else:
if not senders:
signal.connect(func, **kwargs)
else:
senders = kwargs.pop('senders')
for sender in senders:
signal.connect(func, sender=sender, **kwargs)
return func
return _decorator

23
sapl/lexml/OAIServer.py

@ -6,9 +6,11 @@ import oaipmh.error
import oaipmh.metadata import oaipmh.metadata
import oaipmh.server import oaipmh.server
from django.urls import reverse from django.urls import reverse
from django.utils import timezone
from lxml import etree from lxml import etree
from lxml.builder import ElementMaker from lxml.builder import ElementMaker
from sapl.base.models import AppConfig, CasaLegislativa from sapl.base.models import AppConfig, CasaLegislativa
from sapl.lexml.models import LexmlPublicador, LexmlProvedor from sapl.lexml.models import LexmlPublicador, LexmlProvedor
from sapl.norma.models import NormaJuridica from sapl.norma.models import NormaJuridica
@ -102,9 +104,12 @@ class OAIServer:
return appconfig.esfera_federacao return appconfig.esfera_federacao
def recupera_norma(self, offset, batch_size, from_, until, identifier, esfera): def recupera_norma(self, offset, batch_size, from_, until, identifier, esfera):
kwargs = {'data__lte': until} kwargs = {'data__isnull': False,
'esfera_federacao__isnull': False,
'timestamp__isnull': False,
'timestamp__lte': until}
if from_: if from_:
kwargs['data__gte'] = from_ kwargs['timestamp__gte'] = from_
if identifier: if identifier:
kwargs['numero'] = identifier kwargs['numero'] = identifier
if esfera: if esfera:
@ -232,11 +237,21 @@ class OAIServer:
return None return None
def oai_query(self, offset=0, batch_size=10, from_=None, until=None, identifier=None): def oai_query(self, offset=0, batch_size=10, from_=None, until=None, identifier=None):
from_ = timezone.make_aware(from_) # convert from naive to timezone aware datetime
esfera = self.get_esfera_federacao() esfera = self.get_esfera_federacao()
offset = 0 if offset < 0 else offset offset = 0 if offset < 0 else offset
batch_size = 10 if batch_size < 0 else batch_size batch_size = 10 if batch_size < 0 else batch_size
until = datetime.now() if not until or until > datetime.now() else until
normas = self.recupera_norma(offset, batch_size, from_, until, identifier, esfera) until = timezone.make_aware(until) \
if until and timezone.make_aware(until) < timezone.now() \
else timezone.now()
normas = self.recupera_norma(offset,
batch_size,
from_,
until,
identifier,
esfera)
for norma in normas: for norma in normas:
resultado = {} resultado = {}
identificador = self.monta_id(norma) identificador = self.monta_id(norma)

52
sapl/materia/forms.py

@ -18,8 +18,7 @@ from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
import django_filters import django_filters
import sapl from sapl.base.models import AppConfig as BaseAppConfig, Autor, TipoAutor
from sapl.base.models import AppConfig, Autor, TipoAutor
from sapl.comissoes.models import Comissao, Composicao, Participacao from sapl.comissoes.models import Comissao, Composicao, Participacao
from sapl.compilacao.models import (STATUS_TA_IMMUTABLE_PUBLIC, from sapl.compilacao.models import (STATUS_TA_IMMUTABLE_PUBLIC,
STATUS_TA_PRIVATE) STATUS_TA_PRIVATE)
@ -348,8 +347,8 @@ class DocumentoAcessorioForm(FileFieldCheckMixin, ModelForm):
if arquivo: if arquivo:
validar_arquivo(arquivo, "Texto Integral") validar_arquivo(arquivo, "Texto Integral")
else: else:
## TODO: definir arquivo no form e preservar o nome do campo # TODO: definir arquivo no form e preservar o nome do campo
## que gerou a mensagem de erro. # que gerou a mensagem de erro.
## arquivo = forms.FileField(required=True, label="Texto Integral") ## arquivo = forms.FileField(required=True, label="Texto Integral")
nome_arquivo = self.fields['arquivo'].label nome_arquivo = self.fields['arquivo'].label
raise ValidationError(f'Favor anexar arquivo em {nome_arquivo}') raise ValidationError(f'Favor anexar arquivo em {nome_arquivo}')
@ -509,7 +508,7 @@ class TramitacaoForm(ModelForm):
if not self.instance.data_tramitacao: if not self.instance.data_tramitacao:
if ultima_tramitacao: if ultima_tramitacao and BaseAppConfig.attr('tramitacao_origem_fixa'):
destino = ultima_tramitacao.unidade_tramitacao_destino destino = ultima_tramitacao.unidade_tramitacao_destino
if (destino != self.cleaned_data['unidade_tramitacao_local']): if (destino != self.cleaned_data['unidade_tramitacao_local']):
self.logger.error("A origem da nova tramitação ({}) não é igual ao " self.logger.error("A origem da nova tramitação ({}) não é igual ao "
@ -562,7 +561,7 @@ class TramitacaoForm(ModelForm):
materia.em_tramitacao = False if tramitacao.status.indicador == "F" else True materia.em_tramitacao = False if tramitacao.status.indicador == "F" else True
materia.save() materia.save()
tramitar_anexadas = sapl.base.models.AppConfig.attr( tramitar_anexadas = BaseAppConfig.attr(
'tramitacao_materia') 'tramitacao_materia')
if tramitar_anexadas: if tramitar_anexadas:
lista_tramitacao = [] lista_tramitacao = []
@ -591,10 +590,11 @@ class TramitacaoForm(ModelForm):
ip=tramitacao.ip, ip=tramitacao.ip,
ultima_edicao=tramitacao.ultima_edicao ultima_edicao=tramitacao.ultima_edicao
)) ))
## TODO: BULK UPDATE não envia Signal para Tramitacao # TODO: BULK UPDATE não envia Signal para Tramitacao
Tramitacao.objects.bulk_create(lista_tramitacao) Tramitacao.objects.bulk_create(lista_tramitacao)
# Atualiza status 'em_tramitacao' # Atualiza status 'em_tramitacao'
MateriaLegislativa.objects.bulk_update(materias_anexadas, ['em_tramitacao']) MateriaLegislativa.objects.bulk_update(
materias_anexadas, ['em_tramitacao'])
return tramitacao return tramitacao
@ -663,7 +663,7 @@ class TramitacaoUpdateForm(TramitacaoForm):
# ela não pode ter seu destino alterado. # ela não pode ter seu destino alterado.
if ultima_tramitacao != obj: if ultima_tramitacao != obj:
if cd['unidade_tramitacao_destino'] != \ if cd['unidade_tramitacao_destino'] != \
obj.unidade_tramitacao_destino: obj.unidade_tramitacao_destino and BaseAppConfig.attr('tramitacao_origem_fixa'):
self.logger.error("Você não pode mudar a Unidade de Destino desta " self.logger.error("Você não pode mudar a Unidade de Destino desta "
"tramitação para {}, pois irá conflitar com a Unidade " "tramitação para {}, pois irá conflitar com a Unidade "
"Local da tramitação seguinte ({})." "Local da tramitação seguinte ({})."
@ -700,7 +700,7 @@ class TramitacaoUpdateForm(TramitacaoForm):
materia.em_tramitacao = False if nova_tram_principal.status.indicador == "F" else True materia.em_tramitacao = False if nova_tram_principal.status.indicador == "F" else True
materia.save() materia.save()
tramitar_anexadas = sapl.base.models.AppConfig.attr( tramitar_anexadas = BaseAppConfig.attr(
'tramitacao_materia') 'tramitacao_materia')
if tramitar_anexadas: if tramitar_anexadas:
anexadas_list = lista_anexados(materia) anexadas_list = lista_anexados(materia)
@ -724,7 +724,7 @@ class TramitacaoUpdateForm(TramitacaoForm):
ma.em_tramitacao = False if nova_tram_principal.status.indicador == "F" else True ma.em_tramitacao = False if nova_tram_principal.status.indicador == "F" else True
ma.save() ma.save()
## TODO: refatorar? # TODO: refatorar?
return nova_tram_principal return nova_tram_principal
@ -1415,6 +1415,9 @@ class AnexadaEmLoteFilterSet(django_filters.FilterSet):
self.filters['tipo'].label = 'Tipo de Matéria' self.filters['tipo'].label = 'Tipo de Matéria'
self.filters['data_apresentacao'].label = 'Data (Inicial - Final)' self.filters['data_apresentacao'].label = 'Data (Inicial - Final)'
self.form.fields['tipo'].required = True
self.form.fields['data_apresentacao'].required = True
row1 = to_row([('tipo', 12)]) row1 = to_row([('tipo', 12)])
row2 = to_row([('data_apresentacao', 12)]) row2 = to_row([('data_apresentacao', 12)])
@ -1779,7 +1782,7 @@ class TramitacaoEmLoteForm(ModelForm):
ip = self.initial['ip'] if 'ip' in self.initial else '' ip = self.initial['ip'] if 'ip' in self.initial else ''
ultima_edicao = self.initial['ultima_edicao'] if 'ultima_edicao' in self.initial else '' ultima_edicao = self.initial['ultima_edicao'] if 'ultima_edicao' in self.initial else ''
tramitar_anexadas = AppConfig.attr('tramitacao_materia') tramitar_anexadas = BaseAppConfig.attr('tramitacao_materia')
for mat_id in materias: for mat_id in materias:
mat = MateriaLegislativa.objects.get(id=mat_id) mat = MateriaLegislativa.objects.get(id=mat_id)
tramitacao = Tramitacao.objects.create( tramitacao = Tramitacao.objects.create(
@ -1824,7 +1827,7 @@ class TramitacaoEmLoteForm(ModelForm):
ip=tramitacao.ip, ip=tramitacao.ip,
ultima_edicao=tramitacao.ultima_edicao ultima_edicao=tramitacao.ultima_edicao
)) ))
## TODO: BULK UPDATE não envia Signal para Tramitacao # TODO: BULK UPDATE não envia Signal para Tramitacao
Tramitacao.objects.bulk_create(lista_tramitacao) Tramitacao.objects.bulk_create(lista_tramitacao)
return tramitacao return tramitacao
@ -1898,10 +1901,10 @@ class ProposicaoForm(FileFieldCheckMixin, forms.ModelForm):
} }
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.texto_articulado_proposicao = AppConfig.attr( self.texto_articulado_proposicao = BaseAppConfig.attr(
'texto_articulado_proposicao') 'texto_articulado_proposicao')
self.receber_recibo = AppConfig.attr( self.receber_recibo = BaseAppConfig.attr(
'receber_recibo_proposicao') 'receber_recibo_proposicao')
if not self.texto_articulado_proposicao: if not self.texto_articulado_proposicao:
@ -1923,7 +1926,7 @@ class ProposicaoForm(FileFieldCheckMixin, forms.ModelForm):
] ]
if AppConfig.objects.last().escolher_numero_materia_proposicao: if BaseAppConfig.objects.last().escolher_numero_materia_proposicao:
fields.append(to_column(('numero_materia_futuro', 12)),) fields.append(to_column(('numero_materia_futuro', 12)),)
else: else:
if 'numero_materia_futuro' in self._meta.fields: if 'numero_materia_futuro' in self._meta.fields:
@ -2039,7 +2042,7 @@ class ProposicaoForm(FileFieldCheckMixin, forms.ModelForm):
def save(self, commit=True): def save(self, commit=True):
cd = self.cleaned_data cd = self.cleaned_data
inst = self.instance inst = self.instance
receber_recibo = AppConfig.objects.last().receber_recibo_proposicao receber_recibo = BaseAppConfig.objects.last().receber_recibo_proposicao
if inst.pk: if inst.pk:
if 'tipo_texto' in cd: if 'tipo_texto' in cd:
@ -2059,7 +2062,8 @@ class ProposicaoForm(FileFieldCheckMixin, forms.ModelForm):
return super().save(commit) return super().save(commit)
inst.ano = timezone.now().year inst.ano = timezone.now().year
sequencia_numeracao = AppConfig.attr('sequencia_numeracao_proposicao') sequencia_numeracao = BaseAppConfig.attr(
'sequencia_numeracao_proposicao')
if sequencia_numeracao == 'A': if sequencia_numeracao == 'A':
numero__max = Proposicao.objects.filter( numero__max = Proposicao.objects.filter(
autor=inst.autor, autor=inst.autor,
@ -2216,7 +2220,7 @@ class ConfirmarProposicaoForm(ProposicaoForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.proposicao_incorporacao_obrigatoria = \ self.proposicao_incorporacao_obrigatoria = \
AppConfig.attr('proposicao_incorporacao_obrigatoria') BaseAppConfig.attr('proposicao_incorporacao_obrigatoria')
if self.proposicao_incorporacao_obrigatoria != 'C': if self.proposicao_incorporacao_obrigatoria != 'C':
if 'gerar_protocolo' in self._meta.fields: if 'gerar_protocolo' in self._meta.fields:
@ -2268,7 +2272,7 @@ class ConfirmarProposicaoForm(ProposicaoForm):
) )
] ]
if not AppConfig.objects.last().escolher_numero_materia_proposicao or \ if not BaseAppConfig.objects.last().escolher_numero_materia_proposicao or \
not self.instance.numero_materia_futuro: not self.instance.numero_materia_futuro:
if 'numero_materia_futuro' in self._meta.fields: if 'numero_materia_futuro' in self._meta.fields:
del fields[0][0][3] del fields[0][0][3]
@ -2348,7 +2352,7 @@ class ConfirmarProposicaoForm(ProposicaoForm):
if not self.is_valid(): if not self.is_valid():
return self.cleaned_data return self.cleaned_data
numeracao = AppConfig.attr('sequencia_numeracao_proposicao') numeracao = BaseAppConfig.attr('sequencia_numeracao_proposicao')
if not numeracao: if not numeracao:
self.logger.error("A sequência de numeração (por ano ou geral)" self.logger.error("A sequência de numeração (por ano ou geral)"
@ -2431,7 +2435,7 @@ class ConfirmarProposicaoForm(ProposicaoForm):
try: try:
self.logger.debug( self.logger.debug(
"Tentando obter modelo de sequência de numeração.") "Tentando obter modelo de sequência de numeração.")
numeracao = AppConfig.objects.last( numeracao = BaseAppConfig.objects.last(
).sequencia_numeracao_protocolo ).sequencia_numeracao_protocolo
except AttributeError as e: except AttributeError as e:
self.logger.error("Erro ao obter modelo. " + str(e)) self.logger.error("Erro ao obter modelo. " + str(e))
@ -2583,7 +2587,7 @@ class ConfirmarProposicaoForm(ProposicaoForm):
GenericForeignKey GenericForeignKey
""" """
numeracao = AppConfig.attr('sequencia_numeracao_protocolo') numeracao = BaseAppConfig.attr('sequencia_numeracao_protocolo')
if numeracao == 'A': if numeracao == 'A':
nm = Protocolo.objects.filter( nm = Protocolo.objects.filter(
ano=timezone.now().year).aggregate(Max('numero')) ano=timezone.now().year).aggregate(Max('numero'))
@ -2603,6 +2607,8 @@ class ConfirmarProposicaoForm(ProposicaoForm):
protocolo.ano = timezone.now().year protocolo.ano = timezone.now().year
protocolo.tipo_protocolo = '1' protocolo.tipo_protocolo = '1'
protocolo.user = proposicao.user
protocolo.de_proposicao = True
protocolo.interessado = str(proposicao.autor)[ protocolo.interessado = str(proposicao.autor)[
:200] # tamanho máximo 200 :200] # tamanho máximo 200

18
sapl/materia/migrations/0080_auto_20211112_1106.py

@ -0,0 +1,18 @@
# Generated by Django 2.2.24 on 2021-11-12 14:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('materia', '0079_auto_20210311_1711'),
]
operations = [
migrations.AlterField(
model_name='documentoacessorio',
name='indexacao',
field=models.TextField(blank=True, verbose_name='Indexação'),
),
]

33
sapl/materia/migrations/0081_auto_20220321_0934.py

@ -0,0 +1,33 @@
# Generated by Django 2.2.24 on 2022-03-21 12:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('materia', '0080_auto_20211112_1106'),
]
operations = [
migrations.AlterField(
model_name='materialegislativa',
name='ano',
field=models.PositiveSmallIntegerField(choices=[(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=[(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=[(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=[(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'),
),
]

6
sapl/materia/models.py

@ -344,9 +344,9 @@ class MateriaLegislativa(models.Model):
numero=self.numero_protocolo).first() numero=self.numero_protocolo).first()
if protocolo: if protocolo:
if protocolo.timestamp: if protocolo.timestamp:
return protocolo.timestamp.date() return protocolo.timestamp
elif protocolo.timestamp_data_hora_manual: elif protocolo.timestamp_data_hora_manual:
return protocolo.timestamp_data_hora_manual.date() return protocolo.timestamp_data_hora_manual
elif protocolo.data: elif protocolo.data:
return protocolo.data return protocolo.data
@ -556,7 +556,7 @@ class DocumentoAcessorio(models.Model):
autor = models.CharField( autor = models.CharField(
max_length=200, blank=True, verbose_name=_('Autor')) max_length=200, blank=True, verbose_name=_('Autor'))
ementa = models.TextField(blank=True, verbose_name=_('Ementa')) ementa = models.TextField(blank=True, verbose_name=_('Ementa'))
indexacao = models.TextField(blank=True) indexacao = models.TextField(blank=True, verbose_name=_('Indexação'))
arquivo = models.FileField( arquivo = models.FileField(
blank=True, blank=True,
null=True, null=True,

58
sapl/materia/views.py

@ -394,7 +394,7 @@ class StatusTramitacaoCrud(CrudAux):
class PesquisarStatusTramitacaoView(FilterView): class PesquisarStatusTramitacaoView(FilterView):
model = StatusTramitacao model = StatusTramitacao
filterset_class = StatusTramitacaoFilterSet filterset_class = StatusTramitacaoFilterSet
paginate_by = 10 paginate_by = 20
def get_filterset_kwargs(self, filterset_class): def get_filterset_kwargs(self, filterset_class):
super(PesquisarStatusTramitacaoView, self).get_filterset_kwargs( super(PesquisarStatusTramitacaoView, self).get_filterset_kwargs(
@ -434,18 +434,23 @@ class PesquisarStatusTramitacaoView(FilterView):
if data: if data:
url = '&' + str(self.request.META["QUERY_STRING"]) url = '&' + str(self.request.META["QUERY_STRING"])
if url.startswith("&page"): if url.startswith("&page"):
ponto_comeco = url.find("descricao=") - 1 url = ''
url = url[ponto_comeco:]
context = self.get_context_data( if 'descricao' in self.request.META['QUERY_STRING'] or\
filter=self.filterset, object_list=self.object_list, 'page' in self.request.META['QUERY_STRING']:
filter_url=url, numero_res=len(self.object_list) resultados = self.object_list
) else:
resultados = []
context["show_results"] = show_results_filter_set( context = self.get_context_data(filter=self.filterset,
self.request.GET.copy() object_list=resultados,
filter_url=url,
numero_res=len(resultados)
) )
context['show_results'] = show_results_filter_set(
self.request.GET.copy())
return self.render_to_response(context) return self.render_to_response(context)
@ -583,6 +588,7 @@ class ProposicaoRecebida(PermissionRequiredMixin, ListView):
context = super(ProposicaoRecebida, self).get_context_data(**kwargs) context = super(ProposicaoRecebida, self).get_context_data(**kwargs)
paginator = context['paginator'] paginator = context['paginator']
page_obj = context['page_obj'] page_obj = context['page_obj']
context['AppConfig'] = sapl.base.models.AppConfig.objects.all().last()
context['page_range'] = make_pagination( context['page_range'] = make_pagination(
page_obj.number, paginator.num_pages) page_obj.number, paginator.num_pages)
context['NO_ENTRIES_MSG'] = 'Nenhuma proposição recebida.' context['NO_ENTRIES_MSG'] = 'Nenhuma proposição recebida.'
@ -787,14 +793,20 @@ class UnidadeTramitacaoCrud(CrudAux):
def get_headers(self): def get_headers(self):
return [_('Unidade de Tramitação')] return [_('Unidade de Tramitação')]
def is_not_empty(self, value):
if value is None:
return False
value = value.strip().replace('&nbsp;', '')
return value != ''
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
for row in context['rows']: for row in context['rows']:
if row[0][0]: # Comissão if self.is_not_empty(row[0][0]): # Comissão
pass pass
elif row[1][0]: # Órgão elif self.is_not_empty(row[1][0]): # Órgão
row[0] = (row[1][0], row[0][1]) row[0] = (row[1][0], row[0][1])
elif row[2][0]: # Parlamentar elif self.is_not_empty(row[2][0]): # Parlamentar
row[0] = (row[2][0], row[0][1]) row[0] = (row[2][0], row[0][1])
row[1], row[2] = ('', ''), ('', '') row[1], row[2] = ('', ''), ('', '')
return context return context
@ -1241,12 +1253,19 @@ class HistoricoProposicaoView(PermissionRequiredMixin, ListView):
ordering = ['-data_hora'] ordering = ['-data_hora']
paginate_by = 10 paginate_by = 10
model = HistoricoProposicao model = HistoricoProposicao
permission_required = ('materia.detail_proposicao', ) permission_required = ('materia.detail_proposicao_enviada', )
def get_queryset(self): def get_queryset(self):
qs = super().get_queryset() qs = super().get_queryset()
from sapl.rules import SAPL_GROUP_AUTOR
from django.contrib.auth.models import Group
user = self.request.user user = self.request.user
if not user.is_superuser: grupo_autor = Group.objects.get(name=SAPL_GROUP_AUTOR)
if not user.is_superuser and grupo_autor.user_set.filter(
id=user.id).exists():
autores = Autor.objects.filter(user=user) autores = Autor.objects.filter(user=user)
qs = qs.filter(proposicao__autor__in=autores) qs = qs.filter(proposicao__autor__in=autores)
return qs return qs
@ -1383,6 +1402,7 @@ class TramitacaoCrud(MasterDetailCrud):
# necessária? # necessária?
if ultima_tramitacao: if ultima_tramitacao:
if ultima_tramitacao.unidade_tramitacao_destino: if ultima_tramitacao.unidade_tramitacao_destino:
if BaseAppConfig.attr('tramitacao_origem_fixa'):
context['form'].fields[ context['form'].fields[
'unidade_tramitacao_local'].choices = [ 'unidade_tramitacao_local'].choices = [
(ultima_tramitacao.unidade_tramitacao_destino.pk, (ultima_tramitacao.unidade_tramitacao_destino.pk,
@ -1399,7 +1419,7 @@ class TramitacaoCrud(MasterDetailCrud):
# Se não for a primeira tramitação daquela matéria, o campo # Se não for a primeira tramitação daquela matéria, o campo
# não pode ser modificado # não pode ser modificado
if not primeira_tramitacao: if not primeira_tramitacao and BaseAppConfig.attr('tramitacao_origem_fixa'):
context['form'].fields[ context['form'].fields[
'unidade_tramitacao_local'].widget.attrs['readonly'] = True 'unidade_tramitacao_local'].widget.attrs['readonly'] = True
@ -2255,8 +2275,13 @@ class DocumentoAcessorioEmLoteView(PermissionRequiredMixin, FilterView):
qr = self.request.GET.copy() qr = self.request.GET.copy()
context['tipos_docs'] = TipoDocumento.objects.all() context['tipos_docs'] = TipoDocumento.objects.all()
if not len(qr):
context['object_list'] = []
else:
context['object_list'] = context['object_list'].order_by( context['object_list'] = context['object_list'].order_by(
'ano', 'numero') 'ano', 'numero')
context['filter_url'] = ('&' + qr.urlencode()) if len(qr) > 0 else '' context['filter_url'] = ('&' + qr.urlencode()) if len(qr) > 0 else ''
context['show_results'] = show_results_filter_set(qr) context['show_results'] = show_results_filter_set(qr)
@ -2377,6 +2402,9 @@ class MateriaAnexadaEmLoteView(PermissionRequiredMixin, FilterView):
return context return context
qr = self.request.GET.copy() qr = self.request.GET.copy()
if not len(qr):
context['object_list'] = []
else:
context['object_list'] = context['object_list'].order_by( context['object_list'] = context['object_list'].order_by(
'numero', '-ano') 'numero', '-ano')
principal = MateriaLegislativa.objects.get(pk=self.kwargs['pk']) principal = MateriaLegislativa.objects.get(pk=self.kwargs['pk'])

80
sapl/norma/forms.py

@ -1,24 +1,23 @@
import logging import logging
from crispy_forms.layout import Fieldset, Layout from crispy_forms.layout import (Button, Fieldset, HTML, Layout)
from django import forms from django import forms
from django.contrib.postgres.search import SearchVector
from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.db import models
from django.db.models import Q from django.db.models import Q
from django.forms import ModelChoiceField, ModelForm, widgets from django.forms import ModelChoiceField, ModelForm, widgets
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
import django_filters import django_filters
from sapl.base.models import Autor, TipoAutor from sapl.base.models import TipoAutor
from sapl.crispy_layout_mixin import form_actions, SaplFormHelper, to_row from sapl.crispy_layout_mixin import form_actions, SaplFormHelper, to_row
from sapl.materia.forms import choice_anos_com_materias
from sapl.materia.models import (MateriaLegislativa, from sapl.materia.models import (MateriaLegislativa,
TipoMateriaLegislativa, Orgao) TipoMateriaLegislativa, Orgao)
from sapl.utils import (ANO_CHOICES, choice_anos_com_normas, from sapl.parlamentares.models import Partido
from sapl.utils import (autor_label, autor_modal, ANO_CHOICES, choice_anos_com_normas,
FileFieldCheckMixin, FilterOverridesMetaMixin, FileFieldCheckMixin, FilterOverridesMetaMixin,
NormaPesquisaOrderingFilter, RangeWidgetOverride, NormaPesquisaOrderingFilter, validar_arquivo)
validar_arquivo)
from .models import (AnexoNormaJuridica, AssuntoNorma, AutoriaNorma, from .models import (AnexoNormaJuridica, AssuntoNorma, AutoriaNorma,
NormaJuridica, NormaRelacionada, TipoNormaJuridica) NormaJuridica, NormaRelacionada, TipoNormaJuridica)
@ -74,43 +73,74 @@ class NormaFilterSet(django_filters.FilterSet):
method='filter_ementa', method='filter_ementa',
label=_('Pesquisar expressões na ementa da norma')) label=_('Pesquisar expressões na ementa da norma'))
indexacao = django_filters.CharFilter(lookup_expr='icontains', indexacao = django_filters.CharFilter(method='filter_indexacao',
label=_('Indexação')) label=_('Indexação'))
assuntos = django_filters.ModelChoiceFilter( assuntos = django_filters.ModelChoiceFilter(
queryset=AssuntoNorma.objects.all()) queryset=AssuntoNorma.objects.all())
autorianorma__autor = django_filters.CharFilter(widget=forms.HiddenInput())
autorianorma__primeiro_autor = django_filters.BooleanFilter(
required=False,
label=_('Primeiro Autor'))
autorianorma__autor__parlamentar_set__filiacao__partido = django_filters.ModelChoiceFilter(
queryset=Partido.objects.all(),
label=_('Normas por Partido'))
o = NormaPesquisaOrderingFilter(help_text='') o = NormaPesquisaOrderingFilter(help_text='')
class Meta(FilterOverridesMetaMixin): class Meta(FilterOverridesMetaMixin):
model = NormaJuridica model = NormaJuridica
fields = ['orgao', 'tipo', 'numero', 'ano', 'data', 'data_vigencia', fields = ['orgao', 'tipo', 'numero', 'ano', 'data',
'data_publicacao', 'ementa', 'assuntos'] 'data_vigencia', 'data_publicacao', 'ementa', 'assuntos',
'autorianorma__autor', 'autorianorma__primeiro_autor', 'autorianorma__autor__tipo']
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(NormaFilterSet, self).__init__(*args, **kwargs) super(NormaFilterSet, self).__init__(*args, **kwargs)
self.filters['autorianorma__autor__tipo'].label = _('Tipo de Autor')
row1 = to_row([('tipo', 4), ('numero', 4), ('ano', 4)]) row1 = to_row([('tipo', 4), ('numero', 4), ('ano', 4)])
row2 = to_row([('data', 6), ('data_publicacao', 6)]) row2 = to_row([('data', 6), ('data_publicacao', 6)])
row3 = to_row([('ementa', 6), ('assuntos', 6)]) row3 = to_row([('ementa', 6), ('assuntos', 6)])
row4 = to_row([('data_vigencia', 6), ('orgao', 6), ]) row4 = to_row([('data_vigencia', 6), ('orgao', 6), ])
row5 = to_row([('o', 6), ('indexacao', 6)]) row5 = to_row([('o', 6), ('indexacao', 6)])
row6 = to_row([
('autorianorma__autor', 0),
(Button('pesquisar',
'Pesquisar Autor',
css_class='btn btn-primary btn-sm'), 2),
(Button('limpar',
'Limpar Autor',
css_class='btn btn-primary btn-sm'), 2),
('autorianorma__primeiro_autor', 2),
('autorianorma__autor__tipo', 3),
('autorianorma__autor__parlamentar_set__filiacao__partido', 3)
])
self.form.helper = SaplFormHelper() self.form.helper = SaplFormHelper()
self.form.helper.form_method = 'GET' self.form.helper.form_method = 'GET'
self.form.helper.layout = Layout( self.form.helper.layout = Layout(
Fieldset(_('Pesquisa de Norma'), Fieldset(_('Pesquisa de Norma'),
row1, row2, row3, row4, row5, row1, row2, row3, row4, row5,
Fieldset(_('Pesquisa Avançada'),
row6,
HTML(autor_label),
HTML(autor_modal)),
form_actions(label='Pesquisar')) form_actions(label='Pesquisar'))
) )
def filter_ementa(self, queryset, name, value): def filter_ementa(self, queryset, name, value):
texto = value.split() return queryset.annotate(search=SearchVector('ementa',
q = Q() config='portuguese')).filter(search=value)
for t in texto:
q &= Q(ementa__icontains=t) def filter_indexacao(self, queryset, name, value):
return queryset.annotate(search=SearchVector('indexacao',
config='portuguese')).filter(search=value)
return queryset.filter(q) def filter_autoria(self, queryset, name, value):
return queryset.filter(**{
name: value,
})
class NormaJuridicaForm(FileFieldCheckMixin, ModelForm): class NormaJuridicaForm(FileFieldCheckMixin, ModelForm):
@ -257,6 +287,9 @@ class AutoriaNormaForm(ModelForm):
data_relativa = forms.DateField( data_relativa = forms.DateField(
widget=forms.HiddenInput(), required=False) widget=forms.HiddenInput(), required=False)
legislatura_anterior = forms.BooleanField(label=_('Legislatura Anterior'),
required=False)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -269,14 +302,18 @@ class AutoriaNormaForm(ModelForm):
self.helper = SaplFormHelper() self.helper = SaplFormHelper()
self.helper.layout = Layout( self.helper.layout = Layout(
Fieldset(_('Autoria'), Fieldset(_('Autoria'),
row1, 'data_relativa', form_actions(label='Salvar'))) row1, 'data_relativa',
form_actions(label='Salvar'),
to_row([('legislatura_anterior', 12)])))
if not kwargs['instance']: if not self.instance:
self.fields['autor'].choices = [] self.fields['autor'].choices = []
class Meta: class Meta:
model = AutoriaNorma model = AutoriaNorma
fields = ['tipo_autor', 'autor', 'primeiro_autor', 'data_relativa'] fields = ['tipo_autor', 'autor',
'primeiro_autor', 'data_relativa',
'legislatura_anterior']
def clean(self): def clean(self):
cd = super(AutoriaNormaForm, self).clean() cd = super(AutoriaNormaForm, self).clean()
@ -360,7 +397,12 @@ class NormaRelacionadaForm(ModelForm):
class Meta: class Meta:
model = NormaRelacionada model = NormaRelacionada
fields = ['orgao', 'tipo', 'numero', 'ano', 'ementa', 'tipo_vinculo'] fields = ['orgao', 'tipo', 'numero', 'ano',
'resumo', 'ementa', 'tipo_vinculo']
widgets = {
'resumo': forms.Textarea(
attrs={'id': 'texto-rico'})}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(NormaRelacionadaForm, self).__init__(*args, **kwargs) super(NormaRelacionadaForm, self).__init__(*args, **kwargs)

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

Loading…
Cancel
Save