Browse Source

Merge tag '3.1.163-RC6' into migracao

migracao
Marcio Mazza 2 years ago
parent
commit
b847e295f2
  1. 17
      .drone.yml
  2. 5
      .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. 34
      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. 12
      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. 22
      sapl/api/deprecated.py
  67. 65
      sapl/api/forms.py
  68. 67
      sapl/api/pagination.py
  69. 72
      sapl/api/serializers.py
  70. 54
      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. 19
      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. 14
      sapl/comissoes/views.py
  88. 44
      sapl/compilacao/migrations/0019_auto_20220630_1420.py
  89. 84
      sapl/compilacao/models.py
  90. 100
      sapl/compilacao/views.py
  91. 30
      sapl/crispy_layout_mixin.py
  92. 45
      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. 134
      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

5
frontend/.eslintrc.js → .eslintrc.js

@ -11,7 +11,7 @@ module.exports = {
],
rules: {
'generator-star-spacing': 'off',
//'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
// 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
camelcase: 0
},
@ -20,7 +20,8 @@ module.exports = {
'vue'
],
parserOptions: {
parser: 'babel-eslint'
parser: '@babel/eslint-parser'
// requireConfigFile: false
},
globals: {

14
README.rst

@ -6,6 +6,19 @@
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.
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>`_.
* 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
ENV LANG C.UTF-8
ENV LC_ALL C.UTF-8
ENV PYTHONDONTWRITEBYTECODE 1
#ENV PYTHONFAULTHANDLER 1
ENV PYTHONUNBUFFERED=1
ENV DEBIAN_FRONTEND noninteractive
ENV BUILD_PACKAGES apt-utils apt-file libpq-dev graphviz-dev build-essential git pkg-config \
python3-dev libxml2-dev libjpeg-dev libssl-dev libffi-dev libxslt1-dev \
libcairo2-dev software-properties-common python3-setuptools python3-pip
## NAO EH PRA TIRAR O vim DA LISTA DE COMANDOS INSTALADOS!!!
ENV RUN_PACKAGES graphviz python3-lxml python3-magic postgresql-client python3-psycopg2 \
poppler-utils curl jq bash python3-venv tzdata nodejs \
poppler-utils curl jq bash vim python3-venv tzdata nodejs \
fontconfig ttf-dejavu python nginx
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 && \
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/check_solr.sh $HOME
COPY docker/solr_api.py $HOME
COPY docker/busy-wait.sh $HOME
COPY docker/solr_cli.py $HOME
COPY docker/wait-for-pg.sh $HOME
COPY docker/wait-for-solr.sh $HOME
COPY docker/create_admin.py $HOME
COPY docker/genkey.py $HOME
COPY docker/gunicorn_start.sh $HOME
@ -54,7 +54,8 @@ RUN rm -rf /var/interlegis/sapl/sapl/.env && \
rm -rf /var/interlegis/sapl/sapl.db
RUN chmod +x /var/interlegis/sapl/start.sh && \
chmod +x /var/interlegis/sapl/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/stderr /var/log/nginx/error.log && \
mkdir /var/log/sapl/ && touch /var/interlegis/sapl/sapl.log && \

6
docker/config/nginx/nginx.conf

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

34
docker/config/nginx/sapl.conf

@ -11,6 +11,31 @@ server {
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/ {
alias /var/interlegis/sapl/collected_static/;
}
@ -21,17 +46,12 @@ server {
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
if (!-f $request_filename) {
proxy_pass http://sapl_server;
break;
}
proxy_pass http://sapl_server;
}
error_page 500 502 503 504 /500.html;
location = /500.html {
root /var/interlegis/sapl/sapl/static/;

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

@ -18,7 +18,7 @@ services:
networks:
- sapl-net
saplsolr:
image: solr:8.3
image: solr:8.11
restart: always
command: bin/solr start -c -f
container_name: solr
@ -32,7 +32,7 @@ services:
networks:
- sapl-net
sapl:
image: interlegis/sapl:3.1.162-RC6
image: interlegis/sapl:3.1.163-RC6
# build:
# context: ../
# dockerfile: ./docker/Dockerfile
@ -52,7 +52,8 @@ services:
EMAIL_HOST_PASSWORD: senhasmtp
USE_SOLR: 'True'
SOLR_COLLECTION: sapl
SOLR_URL: http://saplsolr:8983
SOLR_URL: http://solr:solr@saplsolr:8983
IS_ZK_EMBEDDED: 'True'
TZ: America/Sao_Paulo
volumes:
- 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 (*)
NUM_WORKERS=3 # how many worker processes should Gunicorn spawn (*)
# NUM_WORKERS = 2 * CPUS + 1
TIMEOUT=60
TIMEOUT=300
MAX_REQUESTS=100 # number of requests before restarting worker
DJANGO_SETTINGS_MODULE=sapl.settings # which settings file should Django use (*)
DJANGO_WSGI_MODULE=sapl.wsgi # WSGI module name (*)

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 os
import requests
import logging
import re
import secrets
import subprocess
import sys
import zipfile
from base64 import b64encode, b64decode
from hashlib import sha256
from io import BytesIO
from pathlib import Path
##
## Este módulo deve ser executado na raiz do projeto
##
import requests
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()
class SolrClient:
m = sha256()
m.update(digest)
digest = m.digest()
cypher = b64encode(digest).decode('utf-8')
salt = b64encode(salt).decode('utf-8')
return cypher, salt
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"
UPLOAD_CONFIGSET = "{}/solr/admin/configs?action=UPLOAD&name={}&wt=json"
LIST_COLLECTIONS = "{}/solr/admin/collections?action=LIST&wt=json"
@ -160,6 +244,22 @@ class SolrClient:
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__':
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='?',
help='Max shards per node (default=1)', default=1)
parser.add_argument("--embedded_zk", default=False, action="store_true",
help="Embedded ZooKeeper")
try:
args = parser.parse_args()
except IOError as msg:
@ -185,10 +288,17 @@ if __name__ == '__main__':
sys.exit(-1)
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)
## 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):
print("Collection '%s' doesn't exists. Creating a new one..." % collection)
created = client.create_collection(collection,
@ -200,6 +310,7 @@ if __name__ == '__main__':
else:
print("Collection '%s' exists." % collection)
## Add --disable-index to disable auto index
num_docs = client.get_num_docs(collection)
if num_docs == 0:
print("Performing a full reindex of '%s' collection..." % collection)

30
docker/start.sh

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

4
docs/instalacao31.rst

@ -221,10 +221,10 @@ Frontend do SAPL
Preparação do ambiente::
----------------------
* **Instalação do NodeJs LTS 10.15.x**::
* **Instalação do NodeJs LTS 14.x**::
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
* **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).
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**

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

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

@ -62,8 +62,8 @@ window.DispositivoEdit = function () {
const obj_click = (event.target.classList.contains('dpt-link')
? event.target
: (event.target.parentElement.classList.contains('dpt-link')
? event.target.parentElement
: null))
? event.target.parentElement
: null))
if (obj_click && obj_click.getAttribute('href') && obj_click.getAttribute('href').length > 0) { return }
@ -245,7 +245,7 @@ window.DispositivoEdit = function () {
if (editortype !== 'construct') {
dpt_form.html(data)
if (editortype === 'tinymce') {
window.initTextRichEditor()
window.initTextRichEditor(null, false, true)
}
// OptionalCustomFrontEnd().init()
}
@ -431,9 +431,9 @@ window.DispositivoEdit = function () {
const form_data = {
csrfmiddlewaretoken: this.csrfmiddlewaretoken.value,
texto: texto,
texto_atualizador: texto_atualizador,
visibilidade: visibilidade,
texto,
texto_atualizador,
visibilidade,
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%;
}
}
.dtxt {
display: inline;
:first-child {
display: inline !important;
}
}
.ementa {
padding: 2em 0em 2em 35%;
font-weight: bold;
@ -424,6 +432,7 @@ a:link:after, a:visited:after {
float:left;
.dptt {
position: relative;
}
}
@ -490,7 +499,6 @@ a:link:after, a:visited:after {
}
}
}
.card-header {
font-size: 1.7rem;
}
@ -672,6 +680,9 @@ a:link:after, a:visited:after {
}
}
}
} /* and dpt */
.tipo-vigencias {
@ -1381,7 +1392,7 @@ a:link:after, a:visited:after {
&::before {
z-index: 20;
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:"";
top: 0;
left: 0;
@ -1403,7 +1414,7 @@ a:link:after, a:visited:after {
color: white;
}
&::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 {

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

@ -1,37 +1,28 @@
import tinymce from 'tinymce/tinymce'
import './langs/pt_BR.js'
import tinymce from 'tinymce'
import 'tinymce/themes/silver'
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/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.removeTinymce = function () {
while (window.tinymce.editors.length > 0) {
window.tinymce.remove(window.tinymce.editors[0])
}
}
window.initTextRichEditor = function (elements, readonly = false) {
window.removeTinymce()
const configTinymce = {
selector: elements === null || elements === undefined ? 'textarea' : elements,
forced_root_block: '',
min_height: 200,
language: 'pt_BR',
branding: false,
content_css: 'default',
plugins: ['lists table code visualblocks'],
menubar: 'edit view format table tools',
toolbar: 'undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent'
forced_root_block: 'p',
plugins: 'table lists advlist link code',
toolbar: 'undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link | code',
menubar: 'file edit view insert format table'
}
if (readonly) {
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 'jquery-mask-plugin'
import 'webpack-jquery-ui/dialog'
import 'webpack-jquery-ui/sortable'
import 'webpack-jquery-ui/datepicker'
import 'webpack-jquery-ui/autocomplete'
import 'jquery-ui/dist/jquery-ui'
import 'jquery-ui/ui/widgets/dialog'
import 'jquery-ui/ui/widgets/sortable'
import 'jquery-ui/ui/widgets/datepicker'
import 'jquery-ui/ui/widgets/autocomplete'
import 'jquery-ui/ui/i18n/datepicker-pt-BR'
import 'jquery-ui-themes/themes/cupertino/jquery-ui.min.css'
import 'jquery-mask-plugin'
import './scss/app.scss'
import * as moment from 'moment'
import 'moment/locale/pt-br'
import './js/tinymce'
import './js/image_cropping'
import './js/functions'
import './js/jquery.runner'
import * as moment from 'moment'
import 'moment/locale/pt-br'
// eslint-disable-next-line
require('imports-loader?window.jQuery=jquery!./js/jquery.runner.js')
import '@fortawesome/fontawesome-free/css/all.css'
import 'jquery-ui-themes/themes/cupertino/jquery-ui.min.css'
import './scss/app.scss'
window.$ = $
window.jQuery = $

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

@ -2,42 +2,8 @@
$blue: #02baf2 !default;
$red: #f84545 !default;
@import "~bootstrap/scss/functions";
@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";
@import "~bootstrap/scss/bootstrap";
@each $color, $value in $theme-colors {
.btn-outline-#{$color} {

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

@ -1,3 +1,5 @@
@import "~bootstrap/scss/variables";
.navbar {
padding: 0;
}
@ -26,7 +28,7 @@
border-radius: 0;
}
a {
padding: 0 $grid-gutter-width / 2;
padding: 0 calc($grid-gutter-width / 2);
line-height: 2.3rem;
display: block;
text-decoration: none;
@ -45,7 +47,7 @@
}
}
.search-form {
padding: $grid-gutter-width / 3;
padding: calc($grid-gutter-width / 3);
min-width: 20%;;
}
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"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^5.13.0",
"axios": "^0.21.1",
"axios-progress-bar": "^1.2.0",
"bootstrap": "^4.4.1",
"bootstrap-vue": "^2.12.0",
"diff": "^4.0.1",
"dotenv": "^6.2.0",
"exports-loader": "^0.7.0",
"imports-loader": "^0.8.0",
"jquery": "^3.5.1",
"@fortawesome/fontawesome-free": "^6.1.2",
"axios": "^0.27.2",
"bootstrap": "^4.6.2",
"bootstrap-vue": "^2.22.0",
"diff": "^5.1.0",
"jquery": "^3.6.0",
"jquery-mask-plugin": "^1.14.16",
"jquery-ui": "^1.13.2",
"jquery-ui-themes": "^1.12.0",
"lodash": "^4.17.19",
"moment": "^2.24.0",
"moment-locales-webpack-plugin": "^1.1.2",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"moment-locales-webpack-plugin": "^1.2.0",
"popper.js": "^1.16.1",
"serialize-javascript": "^3.1.0",
"terser": "^4.6.11",
"tinymce": "^5.6.2",
"vue": "^2.6.11",
"webpack": "^4.43.0",
"webpack-jquery-ui": "^2.0.1",
"websocket-extensions": "^0.1.4"
"tinymce": "^6.1.2",
"vue": "^2.7.9"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^4.3.1",
"@vue/cli-plugin-eslint": "^4.3.1",
"@vue/cli-service": "^4.3.1",
"babel-eslint": "^10.1.0",
"compression-webpack-plugin": "^3.1.0",
"css-loader": "^3.5.2",
"eslint": "^6.8.0",
"eslint-config-standard": "^14.1.1",
"@babel/core": "^7.18.13",
"@babel/eslint-parser": "^7.18.9",
"@vue/cli-plugin-babel": "^5.0.8",
"@vue/cli-service": "^5.0.8",
"compression-webpack-plugin": "^10.0.0",
"copy-webpack-plugin": "^11.0.0",
"css-loader": "^6.7.1",
"dotenv": "^16.0.1",
"eslint": "^8.22.0",
"eslint-config-standard": "^17.0.0",
"eslint-friendly-formatter": "^4.0.1",
"eslint-loader": "^4.0.0",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-n": "^15.2.5",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.1",
"eslint-plugin-vue": "^6.2.2",
"node-sass": "^4.13.1",
"sass-loader": "^8.0.2",
"shelljs": "^0.8.4",
"vue-template-compiler": "^2.6.11",
"webpack-bundle-tracker": "^0.4.3"
"eslint-plugin-promise": "^6.0.0",
"eslint-plugin-vue": "^9.3.0",
"eslint-webpack-plugin": "^3.2.0",
"html-webpack-plugin": "^5.5.0",
"imports-loader": "^4.0.1",
"mini-css-extract-plugin": "^2.6.1",
"sass": "^1.54.5",
"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 {
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
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 {
@ -61,11 +61,14 @@ function set_rc_version {
fi
FINAL_VERSION=$NEXT_RC_VERSION
echo "OLD_VERSION: $OLD_VERSION"
echo "FINAL_VERSION: $FINAL_VERSION"
}
function commit_and_push {
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 tag $FINAL_VERSION

2
requirements/dev-requirements.txt

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

19
requirements/requirements.txt

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

22
sapl/api/deprecated.py

@ -1,5 +1,4 @@
import logging
import logging
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.utils import timezone
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.rest_framework.backends import DjangoFilterBackend
from django_filters.rest_framework.filterset import FilterSet
from rest_framework import serializers
from rest_framework import serializers
from rest_framework.generics import ListAPIView
from rest_framework.mixins import ListModelMixin, RetrieveModelMixin
from rest_framework.permissions import (IsAuthenticated,
IsAuthenticatedOrReadOnly, AllowAny)
from rest_framework.viewsets import GenericViewSet
from sapl.api.serializers import ModelChoiceSerializer, AutorSerializer,\
ChoiceSerializer
from sapl.api.core.serializers import ModelChoiceSerializer, ChoiceSerializer
from sapl.api.serializers import AutorSerializer
from sapl.base.models import TipoAutor, Autor, CasaLegislativa
from sapl.materia.models import MateriaLegislativa
from sapl.parlamentares.models import Legislatura
@ -210,7 +207,16 @@ class AutoresPossiveisFilterSet(FilterSet):
if legislatura_relativa.atual():
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):
return queryset.filter(
@ -505,7 +511,7 @@ class AutorListView(ListAPIView):
model = Autor
filter_class = AutorChoiceFilterSet
filter_backends = (DjangoFilterBackend, )
filter_backends = (DjangoFilterBackend,)
serializer_class = AutorChoiceSerializer
@property
@ -651,7 +657,7 @@ class MateriaLegislativaViewSet(ListModelMixin,
serializer_class = MateriaLegislativaOldSerializer
queryset = MateriaLegislativa.objects.all()
filter_backends = (DjangoFilterBackend,)
filter_fields = ('numero', 'ano', 'tipo', )
filter_fields = ('numero', 'ano', 'tipo',)
class SessaoPlenariaViewSet(ListModelMixin,

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):
page_size = 10
page_size_query_param = 'page_size'
max_page_size = 50
max_page_size = 100
def paginate_queryset(self, queryset, request, view=None):
if request.query_params.get('get_all', '').lower() == 'true':
return None
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):
try:
previous_page_number = self.page.previous_page_number()
@ -26,6 +87,10 @@ class StandardPagination(pagination.PageNumberPagination):
return Response({
'pagination': {
'links': {
'next': self.get_next_link(),
'previous': self.get_previous_link(),
},
'previous_page': previous_page_number,
'next_page': next_page_number,
'start_index': self.page.start_index(),

72
sapl/api/serializers.py

@ -1,44 +1,14 @@
import logging
from django.conf import settings
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
from django.db.models import F, 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 django.db.models import Q
from image_cropping.utils import get_backend
from rest_framework import serializers
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
from sapl.api.core.serializers import ModelChoiceObjectRelatedField
from sapl.base.models import Autor
from sapl.parlamentares.models import Parlamentar, Mandato, Legislatura
class AutorSerializer(serializers.ModelSerializer):
@ -52,18 +22,7 @@ class AutorSerializer(serializers.ModelSerializer):
fields = '__all__'
class CasaLegislativaSerializer(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 ParlamentarSerializerPublic(serializers.ModelSerializer):
class Meta:
model = Parlamentar
@ -73,20 +32,13 @@ class ParlamentarSerializer(serializers.ModelSerializer):
"telefone_residencia", "titulo_eleitor", "fax_residencia"]
class ParlamentarEditSerializer(serializers.ModelSerializer):
class Meta:
model = Parlamentar
fields = '__all__'
class ParlamentarResumeSerializer(serializers.ModelSerializer):
class ParlamentarSerializerVerbose(serializers.ModelSerializer):
titular = serializers.SerializerMethodField('check_titular')
partido = serializers.SerializerMethodField('check_partido')
fotografia_cropped = serializers.SerializerMethodField('crop_fotografia')
logger = logging.getLogger(__name__)
def crop_fotografia(self,obj):
def crop_fotografia(self, obj):
thumbnail_url = ""
try:
import os
@ -107,7 +59,7 @@ class ParlamentarResumeSerializer(serializers.ModelSerializer):
return thumbnail_url
def check_titular(self,obj):
def check_titular(self, obj):
is_titular = None
if not Legislatura.objects.exists():
self.logger.error("Não há legislaturas cadastradas.")
@ -128,14 +80,14 @@ class ParlamentarResumeSerializer(serializers.ModelSerializer):
is_titular = '-'
return is_titular
def check_partido(self,obj):
def check_partido(self, obj):
# Coloca a filiação atual ao invés da última
# As condições para mostrar a filiação são:
# A data de filiacao deve ser menor que a data de fim
# da legislatura e data de desfiliação deve nula, ou maior,
# ou igual a data de fim da legislatura
username = self.context['request'].user.username
username = self.context['request'].user.username
if not Legislatura.objects.exists():
self.logger.error("Não há legislaturas cadastradas.")
return ""

54
sapl/api/urls.py

@ -1,55 +1,41 @@
from django.conf import settings
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 sapl.api.deprecated import MateriaLegislativaViewSet, SessaoPlenariaViewSet,\
AutoresProvaveisListView, AutoresPossiveisListView, AutorListView,\
from sapl.api.deprecated import MateriaLegislativaViewSet, SessaoPlenariaViewSet, \
AutoresProvaveisListView, AutoresPossiveisListView, AutorListView, \
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
app_name = AppConfig.name
router = DefaultRouter()
router.register(r'materia$', MateriaLegislativaViewSet)
router.register(r'sessao-plenaria', SessaoPlenariaViewSet)
for app, built_sets in SaplApiViewSetConstrutor._built_sets.items():
for view_prefix, viewset in built_sets.items():
router.register(app.label + '/' +
view_prefix._meta.model_name, viewset)
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 = [
url(r'^docs/swagger(?P<format>\.json|\.yaml)$',
schema_view.without_ui(cache_timeout=0), name='schema-json'),
url(r'^docs/swagger/$',
schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
url(r'^docs/redoc/$',
schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
]
urlpatterns_api_doc = [
# Optional UI:
url('^schema/swagger-ui/',
SpectacularSwaggerView.as_view(url_name='sapl.api:schema_api'), name='swagger_ui_schema_api'),
url('^schema/redoc/',
SpectacularRedocView.as_view(url_name='sapl.api:schema_api'), name='redoc_schema_api'),
# YOUR PATTERNS
url('^schema/', SpectacularAPIView.as_view(), name='schema_api'),
]
# TODO: refatorar para customização da api automática
deprecated_urlpatterns_api = [
@ -57,13 +43,9 @@ deprecated_urlpatterns_api = [
AutoresProvaveisListView.as_view(), name='autores_provaveis_list'),
url(r'^autor/possiveis',
AutoresPossiveisListView.as_view(), name='autores_possiveis_list'),
url(r'^autor', AutorListView.as_view(), name='autor_list'),
url(r'^model/(?P<content_type>\d+)/(?P<pk>\d*)$',
ModelChoiceView.as_view(), name='model_list'),
]
urlpatterns = [
@ -73,6 +55,8 @@ urlpatterns = [
url(r'^api/version', AppVersionView.as_view()),
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
# https://www.django-rest-framework.org/tutorial/4-authentication-and-permissions/
# url(r'^api/auth/', include('rest_framework.urls', namespace='rest_framework')),

668
sapl/api/views.py

@ -1,39 +1,14 @@
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.decorators import api_view, permission_classes
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.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)
@ -51,647 +26,6 @@ def recria_token(request, pk):
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):
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.
if coluna_materia[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})
else:
url_materia = None

55
sapl/base/forms.py

@ -2,6 +2,7 @@ import logging
import os
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 django import forms
from django.conf import settings
@ -17,6 +18,7 @@ from django.utils import timezone
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
import django_filters
from haystack.forms import ModelSearchForm
from sapl.audiencia.models import AudienciaPublica
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,
FilterOverridesMetaMixin, FileFieldCheckMixin,
ImageThumbnailFileInput, qs_override_django_filter,
RANGE_ANOS, YES_NO_CHOICES,
RANGE_ANOS, YES_NO_CHOICES, choice_tipos_normas,
GoogleRecapthaMixin, parlamentares_ativos)
from .models import AppConfig, CasaLegislativa
@ -879,6 +881,11 @@ class RelatorioNormasMesFilterSet(django_filters.FilterSet):
choices=choice_anos_com_normas,
initial=ultimo_ano_com_norma)
tipo = django_filters.ChoiceFilter(required=False,
label='Tipo Norma',
choices=choice_tipos_normas,
initial=0)
class Meta:
model = NormaJuridica
fields = ['ano']
@ -890,7 +897,7 @@ class RelatorioNormasMesFilterSet(django_filters.FilterSet):
self.filters['ano'].label = 'Ano'
self.form.fields['ano'].required = True
row1 = to_row([('ano', 12)])
row1 = to_row([('ano', 6), ('tipo', 6)])
buttons = FormActions(
*[
@ -969,6 +976,11 @@ class RelatorioNormasVigenciaFilterSet(django_filters.FilterSet):
choices=choice_anos_com_normas,
initial=ultimo_ano_com_norma)
tipo = django_filters.ChoiceFilter(required=False,
label='Tipo Norma',
choices=choice_tipos_normas,
initial=0)
vigencia = forms.ChoiceField(
label=_('Vigência'),
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['vigencia'] = self.vigencia
row1 = to_row([('ano', 12)])
row1 = to_row([('ano', 6), ('tipo', 6)])
row2 = to_row([('vigencia', 12)])
buttons = FormActions(
@ -1150,6 +1162,10 @@ class RelatorioHistoricoTramitacaoFilterSet(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
def qs(self):
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__status'].label = 'Status de tramitação'
row1 = to_row([('tramitacao__data_fim_prazo', 12)])
row2 = to_row([('tramitacao__unidade_tramitacao_local', 6),
row1 = to_row([('ano', 12)])
row2 = to_row([('tramitacao__data_fim_prazo', 12)])
row3 = to_row([('tramitacao__unidade_tramitacao_local', 6),
('tramitacao__unidade_tramitacao_destino', 6)])
row3 = to_row(
row4 = to_row(
[('tipo', 6),
('tramitacao__status', 6)])
@ -1196,7 +1213,7 @@ class RelatorioDataFimPrazoTramitacaoFilterSet(django_filters.FilterSet):
self.form.helper.form_method = 'GET'
self.form.helper.layout = Layout(
Fieldset(_('Tramitações'),
row1, row2, row3,
row1, row2, row3, row4,
buttons, )
)
@ -1561,11 +1578,13 @@ class ConfiguracoesAppForm(ModelForm):
'assinatura_ata',
'estatisticas_acesso_normas',
'escolher_numero_materia_proposicao',
'tramitacao_origem_fixa',
'tramitacao_materia',
'tramitacao_documento',
'google_recaptcha_site_key',
'google_recaptcha_secret_key',
'sapl_as_sapn']
'sapl_as_sapn',
'identificacao_de_documentos']
def __init__(self, *args, **kwargs):
super(ConfiguracoesAppForm, self).__init__(*args, **kwargs)
@ -1888,3 +1907,23 @@ class RelatorioNormasPorAutorFilterSet(django_filters.FilterSet):
row3,
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.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.models.deletion import CASCADE
from django.db.models.signals import post_migrate
@ -91,63 +94,124 @@ class AppConfig(models.Model):
('N', _('Nunca Protocolar ao incorporar uma proposição')),
)
documentos_administrativos = models.CharField(
max_length=1,
verbose_name=_('Visibilidade dos Documentos Administrativos'),
choices=TIPO_DOCUMENTO_ADMINISTRATIVO, default='O')
# MANTENHA A SEQUÊNCIA EQUIVALENTE COM /sapl/templates/base/layout.yaml
# AppConfig:
estatisticas_acesso_normas = models.CharField(
# CONFIGURAÇÕES GERAIS
# Linha 1 ------------
esfera_federacao = models.CharField(
max_length=1,
verbose_name=_('Estatísticas de acesso a normas'),
choices=RELATORIO_ATOS_ACESSADOS, default='N')
blank=True,
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(
max_length=1,
verbose_name=_('Sequência de numeração de proposições'),
choices=SEQUENCIA_NUMERACAO_PROPOSICAO, default='A')
# MÓDULO PARLAMENTARES
# 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(
max_length=1,
verbose_name=_('Sequência de numeração de protocolos'),
choices=SEQUENCIA_NUMERACAO_PROTOCOLO, default='A')
inicio_numeracao_protocolo = models.PositiveIntegerField(
verbose_name=_('Início da numeração de protocolo'),
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,
blank=True,
default="",
verbose_name=_('Esfera Federação'),
choices=ESFERA_FEDERACAO_CHOICES)
verbose_name=_('Sequência de numeração de proposições'),
choices=SEQUENCIA_NUMERACAO_PROPOSICAO, default='A')
receber_recibo_proposicao = models.BooleanField(
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
# painel_aberto = models.BooleanField(
# verbose_name=_('Painel aberto para usuário anônimo'),
# choices=YES_NO_CHOICES, default=False)
# MÓDULO MATÉRIA LEGISLATIVA
# Linha 1 ------------------
tramitacao_origem_fixa = models.BooleanField(
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(
verbose_name=_('Usar Textos Articulados para Proposições'),
choices=YES_NO_CHOICES, default=False)
texto_articulado_materia = models.BooleanField(
verbose_name=_('Usar Textos Articulados para Matérias'),
choices=YES_NO_CHOICES, default=False)
texto_articulado_norma = models.BooleanField(
verbose_name=_('Usar Textos Articulados para Normas'),
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')
# MÓDULO SESSÃO PLENÁRIA
assinatura_ata = models.CharField(
verbose_name=_('Quem deve assinar a ata'),
max_length=1, choices=ASSINATURA_ATA_CHOICES, default='T')
# MÓDULO PAINEL
cronometro_discurso = models.DurationField(
verbose_name=_('Cronômetro do Discurso'),
blank=True,
@ -172,28 +236,20 @@ class AppConfig(models.Model):
default=False,
verbose_name=_('Mostrar brasão da Casa no painel?'))
receber_recibo_proposicao = models.BooleanField(
verbose_name=_('Protocolar proposição somente com recibo?'),
choices=YES_NO_CHOICES, default=True)
protocolo_manual = models.BooleanField(
verbose_name=_('Informar data e hora de protocolo?'),
choices=YES_NO_CHOICES, default=False)
# MÓDULO ESTATÍSTICAS DE ACESSO
estatisticas_acesso_normas = models.CharField(
max_length=1,
verbose_name=_('Estatísticas de acesso a normas'),
choices=RELATORIO_ATOS_ACESSADOS, default='N')
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)
# MÓDULO SEGURANÇA
tramitacao_materia = models.BooleanField(
verbose_name=_(
'Tramitar matérias anexadas junto com as matérias principais?'),
choices=YES_NO_CHOICES, default=True)
# MÓDULO LEXML
tramitacao_documento = models.BooleanField(
verbose_name=_(
'Tramitar documentos anexados junto com os documentos principais?'),
choices=YES_NO_CHOICES, default=True)
# TODO: a ser implementado na versão 3.2
# painel_aberto = models.BooleanField(
# verbose_name=_('Painel aberto para usuário anônimo'),
# choices=YES_NO_CHOICES, default=False)
google_recaptcha_site_key = models.CharField(
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'),
max_length=256, default='')
sapl_as_sapn = models.BooleanField(
verbose_name=_(
'Utilizar SAPL como SAPN?'),
choices=YES_NO_CHOICES, default=False)
class Meta:
verbose_name = _('Configurações da Aplicação')
verbose_name_plural = _('Configurações da Aplicação')
@ -216,15 +267,31 @@ class AppConfig(models.Model):
)
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
def attr(cls, attr):
value = cache.get(f'sapl_{attr}')
if not value is None:
return value
config = AppConfig.objects.first()
if not config:
config = AppConfig()
config.save()
return getattr(config, attr)
value = getattr(config, attr)
cache.set(f'sapl_{attr}', value, 600)
return value
def __str__(self):
return _('Configurações da Aplicação - %(id)s') % {
@ -376,3 +443,29 @@ class AuditLog(models.Model):
self.model_name,
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}'

19
sapl/base/receivers.py

@ -4,7 +4,7 @@ import logging
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.core import serializers
from django.db.models.signals import post_delete, post_save,\
from django.db.models.signals import post_delete, post_save, \
post_migrate
from django.db.utils import DEFAULT_DB_ALIAS
from django.dispatch import receiver
@ -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.models import AuditLog, TipoAutor, Autor
from sapl.decorators import receiver_multi_senders
from sapl.materia.models import Tramitacao
from sapl.protocoloadm.models import TramitacaoAdministrativo
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=TramitacaoAdministrativo)
@ -143,11 +156,9 @@ def audit_log_post_save(sender, **kwargs):
def cria_models_tipo_autor(app_config=None, verbosity=2, interactive=True,
using=DEFAULT_DB_ALIAS, **kwargs):
models = models_with_gr_for_model(Autor)
print("\n\033[93m\033[1m{}\033[0m".format(
_('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)
tipo_autor = TipoAutor.objects.filter(
content_type=content_type.id).exists()

26
sapl/base/search_indexes.py

@ -1,5 +1,5 @@
import os.path
import logging
import os.path
from django.db.models import F, Q, Value
from django.db.models.fields import TextField
@ -15,6 +15,7 @@ from sapl.compilacao.models import (STATUS_TA_IMMUTABLE_PUBLIC,
STATUS_TA_PUBLIC, Dispositivo)
from sapl.materia.models import DocumentoAcessorio, MateriaLegislativa
from sapl.norma.models import NormaJuridica
from sapl.sessao.models import SessaoPlenaria
from sapl.settings import SOLR_URL
from sapl.utils import RemoveTag
@ -37,9 +38,14 @@ class TextExtractField(CharField):
try:
with open(arquivo.path, 'rb') as f:
content = self.backend.extract_file_contents(f)
if not content or not content['contents']:
return ''
data = content['contents']
data = ''
if content:
# 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:
print('erro processando arquivo: ' % arquivo.path)
self.logger.error(arquivo.path)
@ -168,3 +174,15 @@ class MateriaLegislativaIndex(DocumentoAcessorioIndex):
('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
from django import template
@ -10,6 +9,7 @@ from sapl.base.models import AppConfig
from sapl.materia.models import DocumentoAcessorio, MateriaLegislativa, Proposicao
from sapl.norma.models import NormaJuridica
from sapl.parlamentares.models import Filiacao
from sapl.sessao.models import SessaoPlenaria
from sapl.utils import filiacao_data, SEPARADOR_HASH_PROPOSICAO
register = template.Library()
@ -51,6 +51,13 @@ def model_verbose_name_plural(class_name):
model = get_class(class_name)
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
def meta_model_value(instance, attr):
try:
@ -103,6 +110,23 @@ def paginacao_limite_superior(pagina):
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
def lookup(d, key):
return d[key] if key in d else []
@ -245,6 +269,7 @@ def youtube_url(value):
r = re.findall(youtube_pattern, value)
return True if r else False
@register.filter
def facebook_url(value):
value = value.lower()
@ -252,6 +277,7 @@ def facebook_url(value):
r = re.findall(facebook_pattern, value)
return True if r else False
@register.filter
def youtube_id(value):
from urllib.parse import urlparse, parse_qs
@ -289,6 +315,8 @@ def search_get_model(object):
return 'd'
elif type(object) == NormaJuridica:
return 'n'
elif type(object) == SessaoPlenaria:
return 's'
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 .forms import LoginForm
from .views import (AlterarSenha, AppConfigCrud, CasaLegislativaCrud,
from .views import (LoginSapl, AlterarSenha, AppConfigCrud, CasaLegislativaCrud,
HelpTopicView, LogotipoView, RelatorioAtasView,
RelatorioAudienciaView, RelatorioDataFimPrazoTramitacaoView, RelatorioHistoricoTramitacaoView,
RelatorioMateriasPorAnoAutorTipoView, RelatorioMateriasPorAutorView,
@ -173,8 +173,7 @@ urlpatterns = [
(TemplateView.as_view(template_name='sistema.html')),
name='sistema'),
url(r'^login/$', views.LoginView.as_view(template_name='base/login.html', authentication_form=LoginForm),
name='login'),
url(r'^login/$', LoginSapl.as_view(), name='login'),
url(r'^logout/$', views.LogoutView.as_view(),
{'next_page': LOGOUT_REDIRECT_URL}, name='logout'),

95
sapl/base/views.py

@ -4,9 +4,11 @@ import datetime
import itertools
import logging
import os
import re
from django.apps.registry import apps
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.models import Group
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.urls import reverse, reverse_lazy
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
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 haystack.query import SearchQuerySet
from haystack.views import SearchView
from ratelimit.decorators import ratelimit
from sapl import settings
from sapl.audiencia.models import AudienciaPublica, TipoAudienciaPublica
from sapl.base.forms import (AutorForm, TipoAutorForm, AutorFilterSet, RecuperarSenhaForm,
NovaSenhaForm, UserAdminForm,
OperadorAutorForm)
OperadorAutorForm, LoginForm, SaplSearchForm)
from sapl.base.models import Autor, TipoAutor, OperadorAutor
from sapl.comissoes.models import Comissao, Reuniao
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,
SEPARADOR_HASH_PROPOSICAO, show_results_filter_set, num_materias_por_tipo,
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,
RelatorioAudienciaFilterSet, RelatorioDataFimPrazoTramitacaoFilterSet,
@ -86,6 +90,15 @@ class IndexView(TemplateView):
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):
template_name = "email/confirma.html"
@ -699,6 +712,12 @@ class RelatorioDataFimPrazoTramitacaoView(RelatorioMixin, FilterView):
context['data_tramitacao'] = (self.request.GET['tramitacao__data_fim_prazo_0'] + ' - ' +
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']:
tipo = self.request.GET['tipo']
context['tipo'] = (
@ -896,7 +915,8 @@ class RelatorioMateriasTramitacaoView(RelatorioMixin, FilterView):
)
else:
context['materia__autor'] = ''
if 'page' in qr:
del qr['page']
context['filter_url'] = ('&' + qr.urlencode()) if len(qr) > 0 else ''
context['show_results'] = show_results_filter_set(qr)
@ -1876,6 +1896,9 @@ class UserCrud(Crud):
'usuario', 'groups', 'is_active'
]
def openapi_url(self):
return ''
def resolve_url(self, suffix, args=None):
return reverse('sapl.base:%s' % self.url_name(suffix),
args=args)
@ -2079,10 +2102,45 @@ class AppConfigCrud(CrudAux):
kwargs={'pk': app_config.pk}))
class UpdateView(CrudAux.UpdateView):
template_name = 'base/AppConfig.html'
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):
numeracao = AppConfig.objects.last().sequencia_numeracao_protocolo
numeracao_antiga = AppConfig.objects.last().inicio_numeracao_protocolo
@ -2151,15 +2209,40 @@ class AppConfigCrud(CrudAux):
class SaplSearchView(SearchView):
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):
context = super(SaplSearchView, self).get_context()
data = self.request.GET or self.request.POST
data = data.copy()
if 'models' in self.request.GET:
models = self.request.GET.getlist('models')
else:
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:
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,
null=True,
verbose_name=_('Data Desligamento'))
motivo_desligamento = models.CharField(
max_length=150, blank=True,
verbose_name=_('Motivo Desligamento'))
observacao = models.CharField(
max_length=150, blank=True, verbose_name=_('Observação'))
motivo_desligamento = models.TextField(
blank=True, verbose_name=_('Motivo Desligamento'))
observacao = models.TextField(
blank=True, verbose_name=_('Observação'))
class Meta:
verbose_name = _('Participação em Comissão')

14
sapl/comissoes/views.py

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

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'),
),
]

84
sapl/compilacao/models.py

@ -1,7 +1,7 @@
from django.contrib import messages
from django.contrib.contenttypes.fields import GenericForeignKey
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.aggregates import Max
from django.db.models.deletion import PROTECT
@ -475,48 +475,44 @@ class TextoArticulado(TimestampedMixin):
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')
map_ids = {}
for d in dispositivos:
id_old = d.id
with transaction.atomic():
map_ids = {}
for d in dispositivos:
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:
continue
if d.dispositivo_subsequente:
continue
d.id = None
d.inicio_vigencia = ta.data
d.fim_vigencia = None
d.inicio_eficacia = ta.data
d.fim_eficacia = None
d.publicacao = None
d.ta = ta
d.ta_publicado = None
d.dispositivo_subsequente = None
d.dispositivo_substituido = None
d.dispositivo_vigencia = None
d.dispositivo_atualizador = None
d.save()
map_ids[id_old] = d.id
dispositivos = Dispositivo.objects.filter(ta=ta).order_by('ordem')
for d in dispositivos:
if not d.dispositivo_pai:
continue
d.id = None
d.inicio_vigencia = destino.data
d.fim_vigencia = None
d.inicio_eficacia = destino.data
d.fim_eficacia = None
d.publicacao = None
d.ta = destino
d.ta_publicado = None
d.dispositivo_subsequente = None
d.dispositivo_substituido = None
d.dispositivo_vigencia = None
d.dispositivo_atualizador = None
d.save()
map_ids[id_old] = d.id
dispositivos = Dispositivo.objects.filter(
ta=destino).order_by('ordem')
for d in dispositivos:
if not d.dispositivo_pai:
continue
d.dispositivo_pai_id = map_ids[d.dispositivo_pai_id]
d.save()
return ta
d.dispositivo_pai_id = map_ids[d.dispositivo_pai_id]
d.save()
return destino
def reagrupar_ordem_de_dispositivos(self):
@ -894,7 +890,7 @@ class Publicacao(TimestampedMixin):
ta = models.ForeignKey(
TextoArticulado,
verbose_name=_('Texto Articulado'),
on_delete=models.PROTECT
on_delete=models.CASCADE
)
veiculo_publicacao = models.ForeignKey(
@ -1104,7 +1100,7 @@ class Dispositivo(BaseModel, TimestampedMixin):
null=True,
default=None,
verbose_name=_('Publicação'),
on_delete=models.PROTECT
on_delete=models.SET_NULL,
)
ta = models.ForeignKey(
@ -1116,7 +1112,7 @@ class Dispositivo(BaseModel, TimestampedMixin):
ta_publicado = models.ForeignKey(
TextoArticulado,
on_delete=models.PROTECT,
on_delete=models.CASCADE,
blank=True,
null=True,
default=None,
@ -1151,7 +1147,7 @@ class Dispositivo(BaseModel, TimestampedMixin):
default=None,
related_name='dispositivos_filhos_set',
verbose_name=_('Dispositivo Pai'),
on_delete=models.PROTECT
on_delete=models.CASCADE,
)
dispositivo_raiz = models.ForeignKey(
@ -1161,7 +1157,7 @@ class Dispositivo(BaseModel, TimestampedMixin):
default=None,
related_name='nodes',
verbose_name=_('Dispositivo Raiz'),
on_delete=models.PROTECT
on_delete=models.CASCADE,
)
dispositivo_vigencia = models.ForeignKey(
@ -1181,7 +1177,7 @@ class Dispositivo(BaseModel, TimestampedMixin):
default=None,
related_name='dispositivos_alterados_set',
verbose_name=_('Dispositivo Atualizador'),
on_delete=models.PROTECT
on_delete=models.SET_NULL,
)
contagem_continua = models.BooleanField(

100
sapl/compilacao/views.py

@ -6,6 +6,7 @@ import sys
from braces.views import FormMessagesMixin
from bs4 import BeautifulSoup
from django import forms
from django.apps.registry import apps
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.mixins import PermissionRequiredMixin
@ -152,34 +153,46 @@ class IntegracaoTaView(TemplateView):
content_type=related_object_type)
ta_exists = bool(ta.exists())
if (ta_exists or
(request.user.has_perm(
self.object = ta = ta.first()
ta_perm_edit = (
(
request.user.has_perm(
'compilacao.change_dispositivo_edicao_dinamica') and
ta_values.get('privacidade', STATUS_TA_EDITION
) != STATUS_TA_PRIVATE) or
(request.user.has_perm(
ta_values.get(
'privacidade',
STATUS_TA_EDITION
) != STATUS_TA_PRIVATE
) or (
request.user.has_perm(
'compilacao.change_your_dispositivo_edicao_dinamica') and
ta_values.get('privacidade', STATUS_TA_EDITION
) == STATUS_TA_PRIVATE)):
"""
o texto articulado será criado/atualizado se:
- texto articulado foi criado.
ta_values.get(
'privacidade',
STATUS_TA_EDITION
) == STATUS_TA_PRIVATE
)
)
- não foi criado e o usuário possui permissão para criar
desde que o texto não seja um texto privado pois a permissão
para criar textos privados é diferente.
"""
o texto articulado será criado/atualizado se:
- texto articulado foi criado.
- não foi criado e o usuário possui permissão para criar desde
que o texto seja privado e a permissão seja específica para
textos privados.
"""
pass
else:
- não foi criado e o usuário possui permissão para criar
desde que o texto não seja um texto privado pois a permissão
para criar textos privados é diferente.
- não foi criado e o usuário possui permissão para criar desde
que o texto seja privado e a permissão seja específica para
textos privados.
"""
if not ta_exists and not ta_perm_edit:
messages.info(request, _('%s não possui %s.') % (
item, TextoArticulado._meta.verbose_name))
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 ta.editable_only_by_owners and\
@ -1098,8 +1111,45 @@ class TextEditView(CompMixin, TemplateView):
self.object = self.ta
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):
if 'importar_texto_materia' in request.GET:
return self.importar_texto_materia(request, *args, **kwargs)
if self.object.editing_locked:
if 'unlock' not in request.GET:
messages.error(
@ -1336,6 +1386,16 @@ class TextEditView(CompMixin, TemplateView):
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):
result = Dispositivo.objects.filter(ta_id=self.kwargs['ta_id'])

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

45
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 SaplFormHelper
from sapl.rules.map_rules import (RP_ADD, RP_CHANGE, RP_DELETE, RP_DETAIL,
RP_LIST)
from sapl.rules import (RP_ADD, RP_CHANGE, RP_DELETE, RP_DETAIL,
RP_LIST)
from sapl.utils import normalize
logger = logging.getLogger(settings.BASE_DIR.name)
ACTION_LIST, ACTION_CREATE, ACTION_DETAIL, ACTION_UPDATE, ACTION_DELETE = \
@ -362,6 +361,13 @@ class CrudBaseMixin(CrispyLayoutFormMixin):
if self.request.user.has_perm(
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):
names = super(CrudBaseMixin, self).get_template_names()
names.append("crud/%s.html" %
@ -423,14 +429,20 @@ class CrudListView(PermissionRequiredContainerCrudMixin, ListView):
m = f.related_model
except:
f = None
hook = 'hook_header_{}'.format(''.join(fn))
if hasattr(self, hook):
header = getattr(self, hook)()
s.append(header)
elif f:
s.append(force_text(f.verbose_name))
s = ' / '.join(s)
if f:
hook = 'hook_header_{}'.format(''.join(fn))
if hasattr(self, hook):
header = getattr(self, hook)()
s.append(force_text(header))
else:
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(filter(lambda x: x, s))
r.append(s)
return r
@ -1113,12 +1125,15 @@ class MasterDetailCrud(Crud):
root_pk = self.kwargs['pk'] if 'pkk' not in self.request.GET\
else self.request.GET['pkk']
kwargs.setdefault('root_pk', root_pk)
context = super(CrudBaseMixin, self).get_context_data(**kwargs)
if parent_object:
context['title'] = '%s <small>(%s)</small>' % (
self.object, parent_object)
title = '%s <small>(%s)</small>' % (
self.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
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'.
* 'data_inicio' e 'data_fim' precisam ser do tipo models.DateField
"""
@wraps(decorated_method)
def display_atual(self):
string_displayed = decorated_method(self)
@ -41,3 +42,42 @@ def vigencia_atual(decorated_method):
return string_displayed
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.server
from django.urls import reverse
from django.utils import timezone
from lxml import etree
from lxml.builder import ElementMaker
from sapl.base.models import AppConfig, CasaLegislativa
from sapl.lexml.models import LexmlPublicador, LexmlProvedor
from sapl.norma.models import NormaJuridica
@ -102,9 +104,12 @@ class OAIServer:
return appconfig.esfera_federacao
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_:
kwargs['data__gte'] = from_
kwargs['timestamp__gte'] = from_
if identifier:
kwargs['numero'] = identifier
if esfera:
@ -232,11 +237,21 @@ class OAIServer:
return 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()
offset = 0 if offset < 0 else offset
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:
resultado = {}
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 _
import django_filters
import sapl
from sapl.base.models import AppConfig, Autor, TipoAutor
from sapl.base.models import AppConfig as BaseAppConfig, Autor, TipoAutor
from sapl.comissoes.models import Comissao, Composicao, Participacao
from sapl.compilacao.models import (STATUS_TA_IMMUTABLE_PUBLIC,
STATUS_TA_PRIVATE)
@ -348,8 +347,8 @@ class DocumentoAcessorioForm(FileFieldCheckMixin, ModelForm):
if arquivo:
validar_arquivo(arquivo, "Texto Integral")
else:
## TODO: definir arquivo no form e preservar o nome do campo
## que gerou a mensagem de erro.
# TODO: definir arquivo no form e preservar o nome do campo
# que gerou a mensagem de erro.
## arquivo = forms.FileField(required=True, label="Texto Integral")
nome_arquivo = self.fields['arquivo'].label
raise ValidationError(f'Favor anexar arquivo em {nome_arquivo}')
@ -509,7 +508,7 @@ class TramitacaoForm(ModelForm):
if not self.instance.data_tramitacao:
if ultima_tramitacao:
if ultima_tramitacao and BaseAppConfig.attr('tramitacao_origem_fixa'):
destino = ultima_tramitacao.unidade_tramitacao_destino
if (destino != self.cleaned_data['unidade_tramitacao_local']):
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.save()
tramitar_anexadas = sapl.base.models.AppConfig.attr(
tramitar_anexadas = BaseAppConfig.attr(
'tramitacao_materia')
if tramitar_anexadas:
lista_tramitacao = []
@ -591,10 +590,11 @@ class TramitacaoForm(ModelForm):
ip=tramitacao.ip,
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)
# Atualiza status 'em_tramitacao'
MateriaLegislativa.objects.bulk_update(materias_anexadas, ['em_tramitacao'])
MateriaLegislativa.objects.bulk_update(
materias_anexadas, ['em_tramitacao'])
return tramitacao
@ -663,7 +663,7 @@ class TramitacaoUpdateForm(TramitacaoForm):
# ela não pode ter seu destino alterado.
if ultima_tramitacao != obj:
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 "
"tramitação para {}, pois irá conflitar com a Unidade "
"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.save()
tramitar_anexadas = sapl.base.models.AppConfig.attr(
tramitar_anexadas = BaseAppConfig.attr(
'tramitacao_materia')
if tramitar_anexadas:
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.save()
## TODO: refatorar?
# TODO: refatorar?
return nova_tram_principal
@ -1415,6 +1415,9 @@ class AnexadaEmLoteFilterSet(django_filters.FilterSet):
self.filters['tipo'].label = 'Tipo de Matéria'
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)])
row2 = to_row([('data_apresentacao', 12)])
@ -1779,7 +1782,7 @@ class TramitacaoEmLoteForm(ModelForm):
ip = self.initial['ip'] if 'ip' 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:
mat = MateriaLegislativa.objects.get(id=mat_id)
tramitacao = Tramitacao.objects.create(
@ -1824,7 +1827,7 @@ class TramitacaoEmLoteForm(ModelForm):
ip=tramitacao.ip,
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)
return tramitacao
@ -1898,10 +1901,10 @@ class ProposicaoForm(FileFieldCheckMixin, forms.ModelForm):
}
def __init__(self, *args, **kwargs):
self.texto_articulado_proposicao = AppConfig.attr(
self.texto_articulado_proposicao = BaseAppConfig.attr(
'texto_articulado_proposicao')
self.receber_recibo = AppConfig.attr(
self.receber_recibo = BaseAppConfig.attr(
'receber_recibo_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)),)
else:
if 'numero_materia_futuro' in self._meta.fields:
@ -2039,7 +2042,7 @@ class ProposicaoForm(FileFieldCheckMixin, forms.ModelForm):
def save(self, commit=True):
cd = self.cleaned_data
inst = self.instance
receber_recibo = AppConfig.objects.last().receber_recibo_proposicao
receber_recibo = BaseAppConfig.objects.last().receber_recibo_proposicao
if inst.pk:
if 'tipo_texto' in cd:
@ -2059,7 +2062,8 @@ class ProposicaoForm(FileFieldCheckMixin, forms.ModelForm):
return super().save(commit)
inst.ano = timezone.now().year
sequencia_numeracao = AppConfig.attr('sequencia_numeracao_proposicao')
sequencia_numeracao = BaseAppConfig.attr(
'sequencia_numeracao_proposicao')
if sequencia_numeracao == 'A':
numero__max = Proposicao.objects.filter(
autor=inst.autor,
@ -2216,7 +2220,7 @@ class ConfirmarProposicaoForm(ProposicaoForm):
def __init__(self, *args, **kwargs):
self.proposicao_incorporacao_obrigatoria = \
AppConfig.attr('proposicao_incorporacao_obrigatoria')
BaseAppConfig.attr('proposicao_incorporacao_obrigatoria')
if self.proposicao_incorporacao_obrigatoria != 'C':
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:
if 'numero_materia_futuro' in self._meta.fields:
del fields[0][0][3]
@ -2348,7 +2352,7 @@ class ConfirmarProposicaoForm(ProposicaoForm):
if not self.is_valid():
return self.cleaned_data
numeracao = AppConfig.attr('sequencia_numeracao_proposicao')
numeracao = BaseAppConfig.attr('sequencia_numeracao_proposicao')
if not numeracao:
self.logger.error("A sequência de numeração (por ano ou geral)"
@ -2431,7 +2435,7 @@ class ConfirmarProposicaoForm(ProposicaoForm):
try:
self.logger.debug(
"Tentando obter modelo de sequência de numeração.")
numeracao = AppConfig.objects.last(
numeracao = BaseAppConfig.objects.last(
).sequencia_numeracao_protocolo
except AttributeError as e:
self.logger.error("Erro ao obter modelo. " + str(e))
@ -2583,7 +2587,7 @@ class ConfirmarProposicaoForm(ProposicaoForm):
GenericForeignKey
"""
numeracao = AppConfig.attr('sequencia_numeracao_protocolo')
numeracao = BaseAppConfig.attr('sequencia_numeracao_protocolo')
if numeracao == 'A':
nm = Protocolo.objects.filter(
ano=timezone.now().year).aggregate(Max('numero'))
@ -2603,6 +2607,8 @@ class ConfirmarProposicaoForm(ProposicaoForm):
protocolo.ano = timezone.now().year
protocolo.tipo_protocolo = '1'
protocolo.user = proposicao.user
protocolo.de_proposicao = True
protocolo.interessado = str(proposicao.autor)[
: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()
if protocolo:
if protocolo.timestamp:
return protocolo.timestamp.date()
return protocolo.timestamp
elif protocolo.timestamp_data_hora_manual:
return protocolo.timestamp_data_hora_manual.date()
return protocolo.timestamp_data_hora_manual
elif protocolo.data:
return protocolo.data
@ -556,7 +556,7 @@ class DocumentoAcessorio(models.Model):
autor = models.CharField(
max_length=200, blank=True, verbose_name=_('Autor'))
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(
blank=True,
null=True,

134
sapl/materia/views.py

@ -104,9 +104,9 @@ def proposicao_texto(request, pk):
if proposicao.texto_original:
if (not proposicao.data_recebimento and
not proposicao.autor.operadores.filter(
id=request.user.id
).exists()
not proposicao.autor.operadores.filter(
id=request.user.id
).exists()
):
logger.error("user=" + username + ". Usuário ({}) não tem permissão para acessar o texto original."
.format(request.user.id))
@ -394,7 +394,7 @@ class StatusTramitacaoCrud(CrudAux):
class PesquisarStatusTramitacaoView(FilterView):
model = StatusTramitacao
filterset_class = StatusTramitacaoFilterSet
paginate_by = 10
paginate_by = 20
def get_filterset_kwargs(self, filterset_class):
super(PesquisarStatusTramitacaoView, self).get_filterset_kwargs(
@ -434,17 +434,22 @@ class PesquisarStatusTramitacaoView(FilterView):
if data:
url = '&' + str(self.request.META["QUERY_STRING"])
if url.startswith("&page"):
ponto_comeco = url.find("descricao=") - 1
url = url[ponto_comeco:]
url = ''
context = self.get_context_data(
filter=self.filterset, object_list=self.object_list,
filter_url=url, numero_res=len(self.object_list)
)
if 'descricao' in self.request.META['QUERY_STRING'] or\
'page' in self.request.META['QUERY_STRING']:
resultados = self.object_list
else:
resultados = []
context["show_results"] = show_results_filter_set(
self.request.GET.copy()
)
context = self.get_context_data(filter=self.filterset,
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)
@ -583,6 +588,7 @@ class ProposicaoRecebida(PermissionRequiredMixin, ListView):
context = super(ProposicaoRecebida, self).get_context_data(**kwargs)
paginator = context['paginator']
page_obj = context['page_obj']
context['AppConfig'] = sapl.base.models.AppConfig.objects.all().last()
context['page_range'] = make_pagination(
page_obj.number, paginator.num_pages)
context['NO_ENTRIES_MSG'] = 'Nenhuma proposição recebida.'
@ -787,14 +793,20 @@ class UnidadeTramitacaoCrud(CrudAux):
def get_headers(self):
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):
context = super().get_context_data(**kwargs)
for row in context['rows']:
if row[0][0]: # Comissão
if self.is_not_empty(row[0][0]): # Comissão
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])
elif row[2][0]: # Parlamentar
elif self.is_not_empty(row[2][0]): # Parlamentar
row[0] = (row[2][0], row[0][1])
row[1], row[2] = ('', ''), ('', '')
return context
@ -1241,12 +1253,19 @@ class HistoricoProposicaoView(PermissionRequiredMixin, ListView):
ordering = ['-data_hora']
paginate_by = 10
model = HistoricoProposicao
permission_required = ('materia.detail_proposicao', )
permission_required = ('materia.detail_proposicao_enviada', )
def get_queryset(self):
qs = super().get_queryset()
from sapl.rules import SAPL_GROUP_AUTOR
from django.contrib.auth.models import Group
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)
qs = qs.filter(proposicao__autor__in=autores)
return qs
@ -1383,10 +1402,11 @@ class TramitacaoCrud(MasterDetailCrud):
# necessária?
if ultima_tramitacao:
if ultima_tramitacao.unidade_tramitacao_destino:
context['form'].fields[
'unidade_tramitacao_local'].choices = [
(ultima_tramitacao.unidade_tramitacao_destino.pk,
ultima_tramitacao.unidade_tramitacao_destino)]
if BaseAppConfig.attr('tramitacao_origem_fixa'):
context['form'].fields[
'unidade_tramitacao_local'].choices = [
(ultima_tramitacao.unidade_tramitacao_destino.pk,
ultima_tramitacao.unidade_tramitacao_destino)]
else:
self.logger.error('user=' + username + '. Unidade de tramitação destino '
'da última tramitação não pode ser vazia!')
@ -1399,7 +1419,7 @@ class TramitacaoCrud(MasterDetailCrud):
# Se não for a primeira tramitação daquela matéria, o campo
# não pode ser modificado
if not primeira_tramitacao:
if not primeira_tramitacao and BaseAppConfig.attr('tramitacao_origem_fixa'):
context['form'].fields[
'unidade_tramitacao_local'].widget.attrs['readonly'] = True
@ -2255,8 +2275,13 @@ class DocumentoAcessorioEmLoteView(PermissionRequiredMixin, FilterView):
qr = self.request.GET.copy()
context['tipos_docs'] = TipoDocumento.objects.all()
context['object_list'] = context['object_list'].order_by(
'ano', 'numero')
if not len(qr):
context['object_list'] = []
else:
context['object_list'] = context['object_list'].order_by(
'ano', 'numero')
context['filter_url'] = ('&' + qr.urlencode()) if len(qr) > 0 else ''
context['show_results'] = show_results_filter_set(qr)
@ -2377,39 +2402,42 @@ class MateriaAnexadaEmLoteView(PermissionRequiredMixin, FilterView):
return context
qr = self.request.GET.copy()
context['object_list'] = context['object_list'].order_by(
'numero', '-ano')
principal = MateriaLegislativa.objects.get(pk=self.kwargs['pk'])
not_list = [self.kwargs['pk']] + \
[m for m in principal.materia_principal_set.all(
).values_list('materia_anexada_id', flat=True)]
context['object_list'] = context['object_list'].exclude(
pk__in=not_list)
context['temp_object_list'] = context['object_list']
context['object_list'] = []
for obj in context['temp_object_list']:
materia_anexada = obj
ciclico = False
anexadas_anexada = Anexada.objects.filter(
materia_principal=materia_anexada
)
if not len(qr):
context['object_list'] = []
else:
context['object_list'] = context['object_list'].order_by(
'numero', '-ano')
principal = MateriaLegislativa.objects.get(pk=self.kwargs['pk'])
not_list = [self.kwargs['pk']] + \
[m for m in principal.materia_principal_set.all(
).values_list('materia_anexada_id', flat=True)]
context['object_list'] = context['object_list'].exclude(
pk__in=not_list)
context['temp_object_list'] = context['object_list']
context['object_list'] = []
for obj in context['temp_object_list']:
materia_anexada = obj
ciclico = False
anexadas_anexada = Anexada.objects.filter(
materia_principal=materia_anexada
)
while anexadas_anexada and not ciclico:
anexadas = []
while anexadas_anexada and not ciclico:
anexadas = []
for anexa in anexadas_anexada:
for anexa in anexadas_anexada:
if principal == anexa.materia_anexada:
ciclico = True
else:
for a in Anexada.objects.filter(materia_principal=anexa.materia_anexada):
anexadas.append(a)
if principal == anexa.materia_anexada:
ciclico = True
else:
for a in Anexada.objects.filter(materia_principal=anexa.materia_anexada):
anexadas.append(a)
anexadas_anexada = anexadas
anexadas_anexada = anexadas
if not ciclico:
context['object_list'].append(obj)
if not ciclico:
context['object_list'].append(obj)
context['numero_res'] = len(context['object_list'])

80
sapl/norma/forms.py

@ -1,24 +1,23 @@
import logging
from crispy_forms.layout import Fieldset, Layout
from crispy_forms.layout import (Button, Fieldset, HTML, Layout)
from django import forms
from django.contrib.postgres.search import SearchVector
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.db import models
from django.db.models import Q
from django.forms import ModelChoiceField, ModelForm, widgets
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
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.materia.forms import choice_anos_com_materias
from sapl.materia.models import (MateriaLegislativa,
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,
NormaPesquisaOrderingFilter, RangeWidgetOverride,
validar_arquivo)
NormaPesquisaOrderingFilter, validar_arquivo)
from .models import (AnexoNormaJuridica, AssuntoNorma, AutoriaNorma,
NormaJuridica, NormaRelacionada, TipoNormaJuridica)
@ -74,43 +73,74 @@ class NormaFilterSet(django_filters.FilterSet):
method='filter_ementa',
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'))
assuntos = django_filters.ModelChoiceFilter(
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='')
class Meta(FilterOverridesMetaMixin):
model = NormaJuridica
fields = ['orgao', 'tipo', 'numero', 'ano', 'data', 'data_vigencia',
'data_publicacao', 'ementa', 'assuntos']
fields = ['orgao', 'tipo', 'numero', 'ano', 'data',
'data_vigencia', 'data_publicacao', 'ementa', 'assuntos',
'autorianorma__autor', 'autorianorma__primeiro_autor', 'autorianorma__autor__tipo']
def __init__(self, *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)])
row2 = to_row([('data', 6), ('data_publicacao', 6)])
row3 = to_row([('ementa', 6), ('assuntos', 6)])
row4 = to_row([('data_vigencia', 6), ('orgao', 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.form_method = 'GET'
self.form.helper.layout = Layout(
Fieldset(_('Pesquisa de Norma'),
row1, row2, row3, row4, row5,
Fieldset(_('Pesquisa Avançada'),
row6,
HTML(autor_label),
HTML(autor_modal)),
form_actions(label='Pesquisar'))
)
def filter_ementa(self, queryset, name, value):
texto = value.split()
q = Q()
for t in texto:
q &= Q(ementa__icontains=t)
return queryset.annotate(search=SearchVector('ementa',
config='portuguese')).filter(search=value)
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):
@ -257,6 +287,9 @@ class AutoriaNormaForm(ModelForm):
data_relativa = forms.DateField(
widget=forms.HiddenInput(), required=False)
legislatura_anterior = forms.BooleanField(label=_('Legislatura Anterior'),
required=False)
logger = logging.getLogger(__name__)
def __init__(self, *args, **kwargs):
@ -269,14 +302,18 @@ class AutoriaNormaForm(ModelForm):
self.helper = SaplFormHelper()
self.helper.layout = Layout(
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 = []
class Meta:
model = AutoriaNorma
fields = ['tipo_autor', 'autor', 'primeiro_autor', 'data_relativa']
fields = ['tipo_autor', 'autor',
'primeiro_autor', 'data_relativa',
'legislatura_anterior']
def clean(self):
cd = super(AutoriaNormaForm, self).clean()
@ -360,7 +397,12 @@ class NormaRelacionadaForm(ModelForm):
class Meta:
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):
super(NormaRelacionadaForm, self).__init__(*args, **kwargs)

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

Loading…
Cancel
Save