Browse Source

Merge branch '3.1.x' into discutir_materia

pull/3557/head
cristian-longhi 1 year ago
committed by GitHub
parent
commit
fc9d562d15
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      .eslintrc.js
  2. 50
      CHANGES.md
  3. 16
      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. 18
      docker/Dockerfile
  9. 9
      docker/docker-compose.yaml
  10. 127
      docker/solr_cli.py
  11. 33
      docker/start.sh
  12. 0
      docker/wait-for-pg.sh
  13. 8
      docker/wait-for-solr.sh
  14. 2
      docs/solr.rst
  15. 1
      drfautoapi/__init__.py
  16. 409
      drfautoapi/drfautoapi.py
  17. BIN
      frontend/public/img/arrow.png
  18. BIN
      frontend/public/img/authenticated.png
  19. BIN
      frontend/public/img/avatar.png
  20. BIN
      frontend/public/img/beta.png
  21. BIN
      frontend/public/img/brasao_transp.gif
  22. BIN
      frontend/public/img/down_arrow_select.jpg
  23. BIN
      frontend/public/img/etiqueta.png
  24. BIN
      frontend/public/img/favicon.ico
  25. BIN
      frontend/public/img/file.png
  26. BIN
      frontend/public/img/hand-note.png
  27. BIN
      frontend/public/img/icon_comissoes.png
  28. BIN
      frontend/public/img/icon_delete_white.png
  29. BIN
      frontend/public/img/icon_materia_legislativa.png
  30. BIN
      frontend/public/img/icon_mesa_diretora.png
  31. BIN
      frontend/public/img/icon_normas_juridicas.png
  32. BIN
      frontend/public/img/icon_parlamentares.png
  33. BIN
      frontend/public/img/icon_pautas.png
  34. BIN
      frontend/public/img/icon_plenarias.png
  35. BIN
      frontend/public/img/icon_relatorios.png
  36. BIN
      frontend/public/img/icon_save_white.png
  37. BIN
      frontend/public/img/lexml.gif
  38. BIN
      frontend/public/img/logo.png
  39. BIN
      frontend/public/img/logo_cc.png
  40. BIN
      frontend/public/img/logo_interlegis.png
  41. BIN
      frontend/public/img/manual.png
  42. BIN
      frontend/public/img/pdflogo.png
  43. BIN
      frontend/public/img/perfil.png
  44. BIN
      frontend/public/img/search-gray.png
  45. BIN
      frontend/public/img/search.png
  46. BIN
      frontend/public/img/user.png
  47. 8
      frontend/src/__apps/compilacao/js/old/compilacao_edit.js
  48. 25
      frontend/src/__apps/compilacao/scss/compilacao.scss
  49. 27
      frontend/src/__global/js/functions.js
  50. 36
      frontend/src/__global/js/tinymce/index.js
  51. 32
      frontend/src/__global/main.js
  52. 38
      frontend/src/__global/scss/_header.scss
  53. 6
      frontend/src/__global/scss/libs/bootstrap/_nav_navbar.scss
  54. 0
      frontend/src/assets/audio/ring.mp3
  55. 808
      frontend/webpack-stats.json
  56. 74
      package.json
  57. 87
      release.sh
  58. 1
      requirements/dev-requirements.txt
  59. 17
      requirements/requirements.txt
  60. 3
      sapl/api/apps.py
  61. 292
      sapl/api/core/__init__.py
  62. 110
      sapl/api/core/filters.py
  63. 25
      sapl/api/core/forms.py
  64. 50
      sapl/api/core/serializers.py
  65. 669
      sapl/api/deprecated.py
  66. 182
      sapl/api/forms.py
  67. 0
      sapl/api/schema.py
  68. 205
      sapl/api/serializers.py
  69. 10
      sapl/api/signals.py
  70. 51
      sapl/api/urls.py
  71. 91
      sapl/api/views.py
  72. 11
      sapl/api/views_audiencia.py
  73. 180
      sapl/api/views_base.py
  74. 12
      sapl/api/views_comissoes.py
  75. 12
      sapl/api/views_compilacao.py
  76. 129
      sapl/api/views_materia.py
  77. 14
      sapl/api/views_norma.py
  78. 11
      sapl/api/views_painel.py
  79. 119
      sapl/api/views_parlamentares.py
  80. 102
      sapl/api/views_protocoloadm.py
  81. 47
      sapl/api/views_sessao.py
  82. 413
      sapl/api/viewset.py
  83. 31
      sapl/audiencia/forms.py
  84. 27
      sapl/audiencia/migrations/0017_audienciapublica_ano.py
  85. 17
      sapl/audiencia/migrations/0018_auto_20230529_1641.py
  86. 12
      sapl/audiencia/models.py
  87. 12
      sapl/audiencia/views.py
  88. 13
      sapl/base/admin.py
  89. 888
      sapl/base/forms.py
  90. 7
      sapl/base/legacy.yaml
  91. 38
      sapl/base/management/commands/backfill_auditlog.py
  92. 19
      sapl/base/migrations/0048_appconfig_tramitacao_origem_fixa.py
  93. 18
      sapl/base/migrations/0049_auto_20220728_2029.py
  94. 31
      sapl/base/migrations/0050_metadata.py
  95. 23
      sapl/base/migrations/0051_auto_20220814_2138.py
  96. 15
      sapl/base/migrations/0052_auto_20220914_1125.py
  97. 18
      sapl/base/migrations/0053_auto_20220919_1705.py
  98. 14
      sapl/base/migrations/0054_auto_20220921_1217.py
  99. 18
      sapl/base/migrations/0055_appconfig_mostrar_voto.py
  100. 23
      sapl/base/migrations/0056_auto_20221118_1330.py

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

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

50
CHANGES.md

@ -0,0 +1,50 @@
3.1.163-RC16 / 2023-09-13
=========================
* Conserta bug em relatórios com emenda longa. (#3674)
* fix: restring acesso ao prometheus metrics para apenas ips locais/invalidos (#3668)
* feat: adiciona filtro de autor no relatorio de tramitacao com data fim de prazo (#3671)
* fix: verifica se existe dispositivo atualizador ao tentar montar nota alteracao (#3669)
* Revert "Remove redirect de URLs (#3652)"
3.1.163-RC15 / 2023-08-11
=========================
* HOT-FIX: conserta geração de CHANGES.md
* fix: Cria novos campos para o model proposicao para salvar o usuario responsavel por cada acao (#3660)
* Simplificação da tela de pesquisa de Matéria Legislativa (#3662)
* bump pyyaml
* Adiciona controle de visibilidade no módulo de relatorios
* Adiciona coluna de justificativa de ausência (#3657)
* Adiciona coluna de justificativa de ausência
* Conserta lógica para embutir SAPL em iframe (#3653)
* Move relatorios para app de relatorios (#3656)
* Remove redirect de URLs (#3652)
* Hot-fix: endpoint do prometheus endpoint URL
* feat: Adiciona funcionalidade de baixar lista de documentos acessorios de um documento administrativo (#3650)
3.1.163-RC14 / 2023-06-28
=========================
* HOT-FIX: conserta changelog
* feat: Torna o campo Data Nascimento de Parlamentares sensivel (#3648)
* hot-fix: desconecta signal pre_save no migrate
3.1.163-RC13 / 2023-06-18
=========================
* Add options to abort release generation
* Adiciona documentação automática de mudanças
* Adiciona link para texto original em Sessão Plenária (#3644)
* Altera nome completo para nome parlamentar em ata (#3645)
* refactor: altera título do link e descrição de relatório
* feat: Script to find and extract codified images pasted into text fields using the tinyMCE editor (#3643)
* feat: adiciona a coluna assunto na list de correspondencias do expediente do dia (#3640)
* fix: força periodo de busca no relatorio audit log (#3639)
* add migrate de ano novo
* impl: add campo para script do google analytics
* refactor: corrige relatório alinhando a proposta da nomenclatura
* hot-fix: corrige inicialização de variável
* fix: ativa filtro que estava comentado para debug
* impl: captura de assinaturas eletrônicas em matérias

16
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.
@ -58,6 +71,9 @@ Orientações gerais sobre o GitHub
===================================
`Instruções para GitHub <https://github.com/interlegis/sapl/blob/3.1.x/docs/howtogit.rst>`_
Suporte ao utilizadores
===================================
`Sala do Discord "Somos Interlegis" sobre SAPL <https://discord.gg/fzXSbhZbcy>`_
Perguntas Frequentes

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"}
}}

18
docker/Dockerfile

@ -1,12 +1,10 @@
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 \
@ -34,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
@ -55,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 && \

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

@ -18,21 +18,21 @@ services:
networks:
- sapl-net
saplsolr:
image: solr:8.9
image: solr:8.11
restart: always
command: bin/solr start -c -f
container_name: solr
labels:
NAME: "solr"
volumes:
- solr_data:/opt/solr/server/solr
- solr_data:/var/solr
- solr_configsets:/opt/solr/server/solr/configsets
ports:
- "8983:8983"
networks:
- sapl-net
sapl:
image: interlegis/sapl:3.1.162
image: interlegis/sapl:3.1.163-RC16
# 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

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()
m = sha256()
m.update(digest)
digest = m.digest()
cypher = b64encode(digest).decode('utf-8')
salt = b64encode(salt).decode('utf-8')
return cypher, salt
class SolrClient:
def create_security_file(username, password):
print("Creating security.json file...")
with open("security.json", "w") as f:
cypher, salt = solr_hash_password(password)
f.write(SECURITY_FILE_TEMPLATE % (username, cypher, salt, username))
print("file created!")
def upload_security_file(zk_host):
zk_port = 9983 # embedded ZK port
print(f"Uploading security file to Solr, ZK server={zk_host}:{zk_port}...")
try:
with open('security.json', 'r') as f:
data = f.read()
zk = KazooClient(hosts=f"{zk_host}:{zk_port}")
zk.start()
print("Uploading security.json file...")
if zk.exists('/security.json'):
zk.set("/security.json", str.encode(data))
else:
zk.create("/security.json", str.encode(data))
data, stat = zk.get('/security.json')
print("file uploaded!")
print(data.decode('utf-8'))
zk.stop()
except Exception as e:
print(e)
sys.exit(-1)
class SolrClient:
LIST_CONFIGSETS = "{}/solr/admin/configs?action=LIST&omitHeader=true&wt=json"
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)

33
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)
@ -108,6 +114,9 @@ if [ $lack_pwd -eq 0 ]; then
# return -1
fi
# Backfilling AuditLog's JSON field
time ./manage.py backfill_auditlog &
echo "-------------------------------------"
echo "| ███████╗ █████╗ ██████╗ ██╗ |"
echo "| ██╔════╝██╔══██╗██╔══██╗██║ |"

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."

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**

1
drfautoapi/__init__.py

@ -0,0 +1 @@
# Transformar em projeto externo instalável para uso geral

409
drfautoapi/drfautoapi.py

@ -0,0 +1,409 @@
from collections import OrderedDict
import importlib
import inspect
import logging
import re
from django.apps.config import AppConfig
from django.apps.registry import apps
from django.conf import settings
from django.contrib.postgres.fields.jsonb import JSONField
from django.db.models.base import ModelBase
from django.db.models.fields import TextField, CharField
from django.db.models.fields.files import FileField
from django.template.defaultfilters import capfirst
from django.utils.translation import ugettext_lazy as _
import django_filters
from django_filters.constants import ALL_FIELDS, EMPTY_VALUES
from django_filters.filters import CharFilter
from django_filters.filterset import FilterSet
from django_filters.rest_framework.backends import DjangoFilterBackend
from django_filters.utils import resolve_field, get_all_model_fields
from rest_framework import serializers as rest_serializers
from rest_framework.response import Response
from rest_framework.routers import DefaultRouter
from rest_framework.viewsets import ModelViewSet
logger = logging.getLogger(__name__)
class SplitStringCharFilter(django_filters.CharFilter):
_re = re.compile(r'("[^"]+"| +|[^"]+)')
def filter(self, qs, value):
if value in EMPTY_VALUES:
return qs
if self.distinct:
qs = qs.distinct()
lookup = '%s__%s' % (self.field_name, self.lookup_expr)
values = [value]
if self.lookup_expr == 'icontains':
if not '"' in value:
values = value.split(' ')
else:
values = list(
filter(
lambda x: x and x != ' ' and x[0] != '"',
self._re.findall(value)
)
) + list(
map(
lambda x: x[1:-1],
filter(
lambda x: x and x[0] == '"',
self._re.findall(value)
)
)
)
if not isinstance(values, list):
values = [values]
for v in values:
qs = self.get_method(qs)(**{lookup: v})
return qs
class ApiFilterSetMixin(FilterSet):
o = CharFilter(method='filter_o')
class Meta:
fields = '__all__'
filter_overrides = {
FileField: {
'filter_class': django_filters.CharFilter,
'extra': lambda f: {
'lookup_expr': 'exact',
},
},
CharField: {
'filter_class': SplitStringCharFilter,
},
TextField: {
'filter_class': SplitStringCharFilter,
},
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 in ('contained_by', 'trigram_similar', 'unaccent', 'search'):
continue
sflk = f'{sub_f}{"__" if sub_f else ""}{lk}'
r.append(sflk)
if hasattr(lv, 'get_lookups'):
r += get_keys_lookups(lv.get_lookups(), sflk)
if hasattr(lv, 'output_field') and hasattr(lv, 'output_field.get_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.get_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
class BusinessRulesNotImplementedMixin:
http_method_names = ['get', 'head', 'options', 'trace']
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 ApiViewSetConstrutor():
_built_sets = {}
class ApiViewSet(ModelViewSet):
filter_backends = (DjangoFilterBackend,)
@classmethod
def get_viewset_for_model(cls, model):
return cls._built_sets[model._meta.app_config][model]
@classmethod
def update(cls, other):
cls._built_sets.update(other._built_sets)
@classmethod
def import_modules(cls, modules):
for m in modules:
importlib.import_module(m)
@classmethod
def router(cls, router_class=DefaultRouter):
router = router_class()
for app, built_sets in cls._built_sets.items():
for model, viewset in built_sets.items():
router.register(
f'{app.label}/{model._meta.model_name}', viewset)
return router
@classmethod
def build_class(cls, apps_or_models):
DRFAUTOAPI = settings.DRFAUTOAPI
serializers_classes = {}
filters_classes = {}
global_serializer_mixin = rest_serializers.ModelSerializer
global_filter_class = ApiFilterSetMixin
try:
if DRFAUTOAPI:
if 'DEFAULT_SERIALIZER_MODULE' in DRFAUTOAPI:
serializers = importlib.import_module(
DRFAUTOAPI['DEFAULT_SERIALIZER_MODULE']
)
serializers_classes = inspect.getmembers(serializers)
serializers_classes = {i[0]: i[1] for i in filter(
lambda x: x[0].endswith('Serializer'),
serializers_classes
)}
if 'DEFAULT_FILTER_MODULE' in DRFAUTOAPI:
filters = importlib.import_module(
DRFAUTOAPI['DEFAULT_FILTER_MODULE']
)
filters_classes = inspect.getmembers(filters)
filters_classes = {i[0]: i[1] for i in filter(
lambda x: x[0].endswith('FilterSet'),
filters_classes
)}
if 'GLOBAL_SERIALIZER_MIXIN' in DRFAUTOAPI:
cs = DRFAUTOAPI['GLOBAL_SERIALIZER_MIXIN'].split('.')
module = importlib.import_module(
'.'.join(cs[0:-1]))
global_serializer_mixin = getattr(module, cs[-1])
if 'GLOBAL_FILTERSET_MIXIN' in DRFAUTOAPI:
cs = DRFAUTOAPI['GLOBAL_FILTERSET_MIXIN'].split('.')
m = importlib.import_module('.'.join(cs[0:-1]))
global_filter_class = getattr(m, cs[-1])
except Exception as e:
logger.error(e)
built_sets = {}
def build(_model):
object_name = _model._meta.object_name
serializer_name = f'{object_name}Serializer'
_serializer_class = serializers_classes.get(
serializer_name, global_serializer_mixin)
filter_name = f'{object_name}FilterSet'
_filterset_class = filters_classes.get(
filter_name, global_filter_class)
def create_class():
_meta_serializer = object if not hasattr(
_serializer_class, 'Meta') else _serializer_class.Meta
class ApiSerializer(_serializer_class):
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)
else:
fields = _meta_serializer.fields
_meta_filterset = object if not hasattr(
_filterset_class, 'Meta') else _filterset_class.Meta
class ApiFilterSet(_filterset_class):
class Meta(_meta_filterset, ):
if not hasattr(_meta_filterset, 'model'):
model = _model
class ModelApiViewSet(ApiViewSetConstrutor.ApiViewSet):
queryset = _model.objects.all()
filterset_class = ApiFilterSet
serializer_class = ApiSerializer
return ModelApiViewSet
viewset = create_class()
viewset.__name__ = '%sModelViewSet' % _model.__name__
return viewset
for am in apps_or_models:
if isinstance(am, ModelBase):
app = am._meta.app_config
else:
app = am
if app not in cls._built_sets:
cls._built_sets[app] = {}
if am != app:
cls._built_sets[app][am] = build(am)
continue
for model in app.get_models():
cls._built_sets[app][model] = build(model)
return cls
# 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 = ApiViewSetConstrutor._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 _ApiViewSet(
cls,
ApiViewSetConstrutor._built_sets[
self.model._meta.app_config][self.model]
):
pass
if hasattr(_ApiViewSet, 'build'):
_ApiViewSet = _ApiViewSet.build()
ApiViewSetConstrutor._built_sets[
self.model._meta.app_config][self.model] = _ApiViewSet
return _ApiViewSet

BIN
frontend/public/img/arrow.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 B

BIN
frontend/public/img/authenticated.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

BIN
frontend/public/img/avatar.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

BIN
frontend/public/img/beta.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

BIN
frontend/public/img/brasao_transp.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

BIN
frontend/public/img/down_arrow_select.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 682 B

BIN
frontend/public/img/etiqueta.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 694 B

BIN
frontend/public/img/favicon.ico

Binary file not shown.

Before

Width:  |  Height:  |  Size: 975 B

BIN
frontend/public/img/file.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1021 B

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 502 B

BIN
frontend/public/img/icon_comissoes.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

BIN
frontend/public/img/icon_delete_white.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

BIN
frontend/public/img/icon_materia_legislativa.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

BIN
frontend/public/img/icon_mesa_diretora.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

BIN
frontend/public/img/icon_normas_juridicas.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

BIN
frontend/public/img/icon_parlamentares.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

BIN
frontend/public/img/icon_pautas.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

BIN
frontend/public/img/icon_plenarias.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

BIN
frontend/public/img/icon_relatorios.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

BIN
frontend/public/img/icon_save_white.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

BIN
frontend/public/img/lexml.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 568 B

BIN
frontend/public/img/logo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

BIN
frontend/public/img/logo_cc.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

BIN
frontend/public/img/logo_interlegis.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

BIN
frontend/public/img/manual.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 343 B

BIN
frontend/public/img/pdflogo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 238 KiB

BIN
frontend/public/img/perfil.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

BIN
frontend/public/img/search.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 367 B

BIN
frontend/public/img/user.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 290 B

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

@ -245,7 +245,7 @@ window.DispositivoEdit = function () {
if (editortype !== 'construct') {
dpt_form.html(data)
if (editortype === 'tinymce') {
window.initTextRichEditor()
window.initTextRichEditor(null, false, false)
}
// 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'
}

25
frontend/src/__apps/compilacao/scss/compilacao.scss

@ -367,6 +367,14 @@ a:link:after, a:visited:after {
max-width: 100%;
}
}
.dtxt {
display: inline;
& > :not(table):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;
}
}
@ -435,6 +444,14 @@ a:link:after, a:visited:after {
.texto_n_estruturado{
margin-top: 0.3333em;
font-size: 1.15em;
.dtxt {
display: inline;
& > :not(table):first-child {
display: inline !important;
}
}
}
@ -490,7 +507,6 @@ a:link:after, a:visited:after {
}
}
}
.card-header {
font-size: 1.7rem;
}
@ -672,6 +688,9 @@ a:link:after, a:visited:after {
}
}
}
} /* and dpt */
.tipo-vigencias {
@ -1381,7 +1400,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 +1422,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 {

27
frontend/src/__global/js/functions.js

@ -10,11 +10,11 @@ window.refreshDatePicker = function () {
}
window.getCookie = function (name) {
var cookieValue = null
let cookieValue = null
if (document.cookie && document.cookie !== '') {
var cookies = document.cookie.split(';')
for (var i = 0; i < cookies.length; i++) {
var cookie = $.trim(cookies[i])
const cookies = document.cookie.split(';')
for (let i = 0; i < cookies.length; i++) {
const cookie = $.trim(cookies[i])
if (cookie.substring(0, name.length + 1) === name + '=') {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1))
break
@ -26,7 +26,7 @@ window.getCookie = function (name) {
window.autorModal = function () {
$(function () {
var dialog = $('#modal_autor').dialog({
const dialog = $('#modal_autor').dialog({
autoOpen: false,
modal: true,
width: 500,
@ -65,12 +65,11 @@ window.autorModal = function () {
})
$('#pesquisar').click(function () {
var name_in_query = $('#q').val()
// var q_0 = "q_0=nome__icontains"
// var q_1 = name_in_query
// query = q_1
$.get('/api/autor?q=' + name_in_query, function (data) {
const json_data = {
q: $('#q').val()
// get_all: true
}
$.get('/api/base/autor', json_data, function (data) {
$('#div-resultado')
.children()
.remove()
@ -82,15 +81,15 @@ window.autorModal = function () {
return
}
var select = $(
const select = $(
'<select id="resultados" style="min-width: 90%; max-width:90%;" size="5"/>'
)
data.results.forEach(function (item) {
select.append(
$('<option>')
.attr('value', item.value)
.text(item.text)
.attr('value', item.id)
.text(item.nome)
)
})

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

@ -1,37 +1,29 @@
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()
window.initTextRichEditor = function (elements, readonly = false, paste_as_text = false) {
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',
paste_as_text,
plugins: 'table lists advlist link code',
toolbar: 'undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link | code',
menubar: 'file edit view insert format table'
}
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.da853c1c.css","publicPath":"/static/sapl/frontend/css/chunk-vendors.da853c1c.css","path":"sapl/static/sapl/frontend/css/chunk-vendors.da853c1c.css"},{"name":"js/chunk-vendors.926da1dd.js","publicPath":"/static/sapl/frontend/js/chunk-vendors.926da1dd.js","path":"sapl/static/sapl/frontend/js/chunk-vendors.926da1dd.js"},{"name":"css/chunk-vendors.da853c1c.css.map","publicPath":"/static/sapl/frontend/css/chunk-vendors.da853c1c.css.map","path":"sapl/static/sapl/frontend/css/chunk-vendors.da853c1c.css.map"},{"name":"js/chunk-vendors.926da1dd.js.map","publicPath":"/static/sapl/frontend/js/chunk-vendors.926da1dd.js.map","path":"sapl/static/sapl/frontend/js/chunk-vendors.926da1dd.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.ae866d2d.js","publicPath":"/static/sapl/frontend/js/compilacao.ae866d2d.js","path":"sapl/static/sapl/frontend/js/compilacao.ae866d2d.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.ae866d2d.js.map","publicPath":"/static/sapl/frontend/js/compilacao.ae866d2d.js.map","path":"sapl/static/sapl/frontend/js/compilacao.ae866d2d.js.map"}],"global":[{"name":"css/global.cfffff0f.css","publicPath":"/static/sapl/frontend/css/global.cfffff0f.css","path":"sapl/static/sapl/frontend/css/global.cfffff0f.css"},{"name":"js/global.8d7024d2.js","publicPath":"/static/sapl/frontend/js/global.8d7024d2.js","path":"sapl/static/sapl/frontend/js/global.8d7024d2.js"},{"name":"css/global.cfffff0f.css.map","publicPath":"/static/sapl/frontend/css/global.cfffff0f.css.map","path":"sapl/static/sapl/frontend/css/global.cfffff0f.css.map"},{"name":"js/global.8d7024d2.js.map","publicPath":"/static/sapl/frontend/js/global.8d7024d2.js.map","path":"sapl/static/sapl/frontend/js/global.8d7024d2.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.22053ae6.js","publicPath":"/static/sapl/frontend/js/painel.22053ae6.js","path":"sapl/static/sapl/frontend/js/painel.22053ae6.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.22053ae6.js.map","publicPath":"/static/sapl/frontend/js/painel.22053ae6.js.map","path":"sapl/static/sapl/frontend/js/painel.22053ae6.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.e6a288dc.js","publicPath":"/static/sapl/frontend/js/parlamentar.e6a288dc.js","path":"sapl/static/sapl/frontend/js/parlamentar.e6a288dc.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.e6a288dc.js.map","publicPath":"/static/sapl/frontend/js/parlamentar.e6a288dc.js.map","path":"sapl/static/sapl/frontend/js/parlamentar.e6a288dc.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.f01dd32a.js": {
"name": "js/global.f01dd32a.js",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/global.f01dd32a.js",
"publicPath": "/static/sapl/frontend/js/global.f01dd32a.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.f4baf459.css": {
"name": "css/compilacao.f4baf459.css",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/css/compilacao.f4baf459.css",
"publicPath": "/static/sapl/frontend/css/compilacao.f4baf459.css"
},
"js/compilacao.d68d2b28.js": {
"name": "js/compilacao.d68d2b28.js",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/compilacao.d68d2b28.js",
"publicPath": "/static/sapl/frontend/js/compilacao.d68d2b28.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.f01dd32a.js.LICENSE.txt": {
"name": "js/global.f01dd32a.js.LICENSE.txt",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/global.f01dd32a.js.LICENSE.txt",
"publicPath": "/static/sapl/frontend/js/global.f01dd32a.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"
},
"js/compilacao.d68d2b28.js.gz": {
"name": "js/compilacao.d68d2b28.js.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/compilacao.d68d2b28.js.gz",
"publicPath": "/static/sapl/frontend/js/compilacao.d68d2b28.js.gz"
},
"js/global.f01dd32a.js.gz": {
"name": "js/global.f01dd32a.js.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/js/global.f01dd32a.js.gz",
"publicPath": "/static/sapl/frontend/js/global.f01dd32a.js.gz"
},
"css/compilacao.f4baf459.css.gz": {
"name": "css/compilacao.f4baf459.css.gz",
"path": "/home/leandro/desenvolvimento/envs/sapl/sapl/static/sapl/frontend/css/compilacao.f4baf459.css.gz",
"publicPath": "/static/sapl/frontend/css/compilacao.f4baf459.css.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.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/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/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/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/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.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.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/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/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/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.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/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/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.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/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.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.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.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/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/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/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.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/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/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.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/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-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/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/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/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"
},
"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"
},
"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"
},
"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"
},
"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"
},
"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.f01dd32a.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.f4baf459.css",
"js/compilacao.d68d2b28.js"
]
},
"publicPath": "/static/sapl/frontend/"
}

74
package.json

@ -8,49 +8,51 @@
"lint": "vue-cli-service lint"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^5.13.0",
"axios": "^0.21.4",
"axios-progress-bar": "^1.2.0",
"bootstrap": "^4.6.0",
"bootstrap-vue": "^2.21.2",
"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.21",
"moment": "^2.24.0",
"moment-locales-webpack-plugin": "^1.1.2",
"moment": "^2.29.4",
"moment-locales-webpack-plugin": "^1.2.0",
"popper.js": "^1.16.1",
"terser": "^4.6.11",
"tinymce": "^5.10.0",
"vue": "^2.6.11",
"webpack": "^4.46.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.5.13",
"@vue/cli-service": "^4.5.13",
"babel-eslint": "^10.1.0",
"compression-webpack-plugin": "^6.1.1",
"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",
"glob-parent": "^5.1.2",
"node-sass": "^6.0.1",
"sass-loader": "^10.2.0",
"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"
}
}

87
release.sh

@ -4,6 +4,10 @@
## Versioning info: [major].[minor].[patch][-RC[num]], example: 3.1.159, 3.1.159-RC1
##
## IMPORTANT: requires gh and git-extras commands installed
## Currently only runs on MacOS because of sed issue on lines 41 to 47 (see double quotes after -i)
##
# TODO: verificar porque só pega versões superiores (3.1.200 ao invés de 3.1.200-RC9)
# VERSION=`git describe --tags --abbrev=0`
@ -11,7 +15,12 @@ VERSION_PATTERN='([0-9]+)\.([0-9]+)\.([0-9]+)(-RC[0-9]+)?'
SED_AWKWARD_PATTERN="[0-9]+\.[0-9]+\.[0-9]+(-RC[0-9]+){0,1}"
LATEST_VERSION=$(git tag | egrep $VERSION_PATTERN | sort --version-sort | tail -1)
# Define colors
green_color='\033[0;32m'
red_color='\033[0;31m'
reset_color='\033[0m'
LATEST_VERSION=$(git tag | egrep $VERSION_PATTERN | sort --version-sort -r | head -1)
MAJOR_VERSION=$(echo $LATEST_VERSION | cut -d"-" -f1)
MAJOR_TAG_CREATED=$(git tag | egrep $MAJOR_VERSION"$")
@ -30,17 +39,19 @@ FINAL_VERSION=
function change_files {
OLD_VERSION=$(grep -E 'interlegis/sapl:'$VERSION_PATTERN docker/docker-compose.yml | cut -d':' -f3)
# TODO: figure out better way of getting latest version
OLD_VERSION=$(grep -E 'interlegis/sapl:'$VERSION_PATTERN docker/docker-compose.yaml | cut -d':' -f3)
echo "Atualizando de "$OLD_VERSION" para "$FINAL_VERSION
echo "Updating from "$OLD_VERSION" to "$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,34 +72,68 @@ function set_rc_version {
fi
FINAL_VERSION=$NEXT_RC_VERSION
## DEBUG
# echo "OLD_VERSION: $OLD_VERSION"
# echo "FINAL_VERSION: $FINAL_VERSION"
}
# Function to display Yes/No prompt with colored message
prompt_yes_no() {
while true; do
echo -e "${green_color}$1 (y/n): ${reset_color}\c"
read answer
case $answer in
[Yy]* ) return 0;;
[Nn]* ) return 1;;
* ) echo -e "${red_color}Please answer 'yes' or 'no'.${reset_color}";;
esac
done
}
function commit_and_push {
echo "committing..."
git add docker/docker-compose.yml setup.py sapl/settings.py sapl/templates/base.html
echo -e "${green_color}Committing new release $FINAL_VERSION...${color_reset}"
git add docker/docker-compose.yaml setup.py sapl/settings.py sapl/templates/base.html
git changelog --tag $FINAL_VERSION --prune-old -x > latest_changes.md
cat latest_changes.md CHANGES.md > CHANGES.tmp
mv CHANGES.tmp CHANGES.md
git add CHANGES.md
rm latest_changes.md
if prompt_yes_no "${green_color}Do you want to commit SAPL $FINAL_VERSION release locally?${reset_color}"; then
git commit -m "Release: $FINAL_VERSION"
git tag $FINAL_VERSION
echo -e "${green_color}Commit and tag created locally!${color_reset}"
else
git reset --hard HEAD
echo -e "${red_color}Aborting release creation!${color_reset}"
return
fi
echo "================================================================================"
echo " Versão criada e gerada localmente."
echo "Para enviar pro github execute..."
echo "git push origin 3.1.x"
echo "git push origin "$FINAL_VERSION
echo "================================================================================"
echo "done."
echo -e "${red_color}### BEFORE PROCEEDING, MAKE SURE THE NEW VERSION NUMBER AND CHANGES ARE CORRECT!${color_reset}"
echo -e "${green_color}Release: $FINAL_VERSION${reset_color}"
if prompt_yes_no "${green_color}Do you want to publish SAPL $FINAL_VERSION release on Github?${reset_color}"; then
echo -e "${green_color}Publishing $FINAL_VERSION on Github...${reset_color}"
current_date=$(date +%Y-%m-%d)
git push origin 3.1.x
gh release create $FINAL_VERSION --repo interlegis/sapl --title "Release: $FINAL_VERSION" --notes "Release notes for $FINAL_VERSION in CHANGES.md file. Release date: $current_date"
echo -e "${green_color}Done.${reset_color}"
else
echo -e "${red_color}Publishing aborted.${reset_color}"
fi
echo "${green_color}Done.${green_color}"
echo -e "${green_color}================================================================================${color_reset}"
}
case "$1" in
--latest)
git fetch
echo $LATEST_VERSION
echo -e "${green_color}$LATEST_VERSION${reset_color}"
exit 0
;;
--major)
git fetch
set_major_version
echo "generating major release: "$FINAL_VERSION
echo -e "${green_color}Creating MAJOR release: "$FINAL_VERSION"${reset_color}"
# git tag $FINAL_VERSION
change_files
commit_and_push
@ -97,14 +142,14 @@ case "$1" in
--rc)
git fetch
set_rc_version
echo "generating release candidate: "$FINAL_VERSION
echo -e "${green_color}Creating RELEASE CANDIDATE (RC): "$FINAL_VERSION"${reset_color}"
# git tag $FINAL_VERSION
change_files
commit_and_push
exit 0
;;
--top)
git tag | sort --version-sort | tail "-$2"
git tag | sort --version-sort -r | head "-$2"
exit 0
;;

1
requirements/dev-requirements.txt

@ -7,4 +7,3 @@ ipdb==0.13.3
pdbpp==0.9.2
pip-review==0.4
pipdeptree==0.10.1
pygraphviz==1.3.1

17
requirements/requirements.txt

@ -5,33 +5,36 @@ djangorestframework==3.12.4
dj-database-url==0.5.0
django-braces==1.14.0
django-crispy-forms==1.7.2
django-contrib-postgres==0.0.1
django-floppyforms==1.8.0
django-extra-views==0.12.0
django-model-utils==3.1.2
django-reversion==3.0.2
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
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.4
pyyaml==6.0.1
pytz==2019.3
python-magic==0.4.15
unipath==1.1
WeasyPrint==51
Pillow==9.0.1
Pillow==9.3.0
gunicorn==19.9.0
more-itertools==8.2.0
pysolr==3.6.0
PyPDF4==1.27.0
pyoai==2.5.0
#pyoai==2.5.1
git+https://github.com/infrae/pyoai@5ff2f15e869869e70d8139e4c37b7832854d7049
Unidecode==1.1.1
whitenoise==5.1.0
kazoo==2.8.0
django-prometheus==2.2.0
asn1crypto==1.5.1
git+https://github.com/interlegis/trml2pdf
git+https://github.com/interlegis/django-admin-bootstrapped

3
sapl/api/apps.py

@ -6,3 +6,6 @@ class AppConfig(apps.AppConfig):
name = 'sapl.api'
label = 'api'
verbose_name = _('API Rest')
def ready(self):
from . import signals

292
sapl/api/core/__init__.py

@ -1,292 +0,0 @@
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
# 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()
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__', ]
else:
fields = _meta_serializer.fields
def get___str__(self, obj) -> str:
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.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

110
sapl/api/core/filters.py

@ -1,110 +0,0 @@
from collections import OrderedDict
from django.db.models.fields.files import FileField
from django.template.defaultfilters import capfirst
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
import django_filters
# 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',
},
},
}
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'):
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

@ -1,25 +0,0 @@
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

50
sapl/api/core/serializers.py

@ -1,50 +0,0 @@
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__'

669
sapl/api/deprecated.py

@ -1,676 +1,27 @@
import logging
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
from django.db.models import Q
from django.forms.fields import CharField, MultiValueField
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_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.generics import ListAPIView
from rest_framework.mixins import ListModelMixin, RetrieveModelMixin
from rest_framework.permissions import (IsAuthenticated,
IsAuthenticatedOrReadOnly, AllowAny)
from rest_framework.permissions import AllowAny
from rest_framework.viewsets import GenericViewSet
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
from sapl.sessao.models import SessaoPlenaria, OrdemDia
from sapl.utils import SaplGenericRelation
from sapl.utils import generic_relations_for_model
class SaplGenericRelationSearchFilterSet(FilterSet):
q = CharFilter(method='filter_q')
def filter_q(self, queryset, name, value):
query = value.split(' ')
if query:
q = Q()
for qtext in query:
if not qtext:
continue
q_fs = Q(nome__icontains=qtext)
order_by = []
for gr in generic_relations_for_model(self._meta.model):
sgr = gr[1]
for item in sgr:
if item.related_model != self._meta.model:
continue
flag_order_by = True
for field in item.fields_search:
if flag_order_by:
flag_order_by = False
order_by.append('%s__%s' % (
item.related_query_name(),
field[0])
)
# if len(field) == 3 and field[2](qtext) is not
# None:
q_fs = q_fs | Q(**{'%s__%s%s' % (
item.related_query_name(),
field[0],
field[1]): qtext if len(field) == 2
else field[2](qtext)})
q = q & q_fs
if q:
queryset = queryset.filter(q).order_by(*order_by)
return queryset
class SearchForFieldWidget(MultiWidget):
def decompress(self, value):
if value is None:
return [None, None]
return value
def __init__(self, attrs=None):
widgets = (TextInput, TextInput)
MultiWidget.__init__(self, widgets, attrs)
class SearchForFieldField(MultiValueField):
widget = SearchForFieldWidget
def __init__(self, *args, **kwargs):
fields = (
CharField(),
CharField())
super(SearchForFieldField, self).__init__(fields, *args, **kwargs)
def compress(self, parameters):
if parameters:
return parameters
return None
class SearchForFieldFilter(CharFilter):
field_class = SearchForFieldField
class AutorChoiceFilterSet(SaplGenericRelationSearchFilterSet):
q = CharFilter(method='filter_q')
tipo = ModelChoiceFilter(queryset=TipoAutor.objects.all())
class Meta:
model = Autor
fields = ['q',
'tipo',
'nome', ]
def filter_q(self, queryset, name, value):
return super().filter_q(
queryset, name, value).distinct('nome').order_by('nome')
class AutorSearchForFieldFilterSet(AutorChoiceFilterSet):
q = SearchForFieldFilter(method='filter_q')
class Meta(AutorChoiceFilterSet.Meta):
pass
def filter_q(self, queryset, name, value):
value[0] = value[0].split(',')
value[1] = value[1].split(',')
params = {}
for key, v in list(zip(value[0], value[1])):
if v in ['True', 'False']:
v = '1' if v == 'True' else '0'
params[key] = v
return queryset.filter(**params).distinct('nome').order_by('nome')
class AutoresPossiveisFilterSet(FilterSet):
logger = logging.getLogger(__name__)
data_relativa = DateFilter(method='filter_data_relativa')
tipo = CharFilter(method='filter_tipo')
class Meta:
model = Autor
fields = ['data_relativa', 'tipo', ]
def filter_data_relativa(self, queryset, name, value):
return queryset
def filter_tipo(self, queryset, name, value):
try:
self.logger.debug(
"Tentando obter TipoAutor correspondente à pk {}.".format(value))
tipo = TipoAutor.objects.get(pk=value)
except:
self.logger.error("TipoAutor(pk={}) inexistente.".format(value))
raise serializers.ValidationError(_('Tipo de Autor inexistente.'))
qs = queryset.filter(tipo=tipo)
return qs
@property
def qs(self):
qs = super().qs
data_relativa = self.form.cleaned_data['data_relativa'] \
if 'data_relativa' in self.form.cleaned_data else None
tipo = self.form.cleaned_data['tipo'] \
if 'tipo' in self.form.cleaned_data else None
if not tipo:
return qs
tipo = TipoAutor.objects.get(pk=tipo)
if not tipo.content_type:
return qs
filter_for_model = 'filter_%s' % tipo.content_type.model
if not hasattr(self, filter_for_model):
return qs
if not data_relativa:
data_relativa = timezone.now()
return getattr(self, filter_for_model)(qs, data_relativa).distinct()
def filter_parlamentar(self, queryset, data_relativa):
# não leva em conta afastamentos
legislatura_relativa = Legislatura.objects.filter(
data_inicio__lte=data_relativa,
data_fim__gte=data_relativa).first()
q = Q(
parlamentar_set__mandato__data_inicio_mandato__lte=data_relativa,
parlamentar_set__mandato__data_fim_mandato__isnull=True) | Q(
parlamentar_set__mandato__data_inicio_mandato__lte=data_relativa,
parlamentar_set__mandato__data_fim_mandato__gte=data_relativa)
if legislatura_relativa.atual():
q = q & Q(parlamentar_set__ativo=True)
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(
Q(comissao_set__data_extincao__isnull=True,
comissao_set__data_fim_comissao__isnull=True) |
Q(comissao_set__data_extincao__gte=data_relativa,
comissao_set__data_fim_comissao__isnull=True) |
Q(comissao_set__data_extincao__gte=data_relativa,
comissao_set__data_fim_comissao__isnull=True) |
Q(comissao_set__data_extincao__isnull=True,
comissao_set__data_fim_comissao__gte=data_relativa) |
Q(comissao_set__data_extincao__gte=data_relativa,
comissao_set__data_fim_comissao__gte=data_relativa),
comissao_set__data_criacao__lte=data_relativa)
def filter_frente(self, queryset, data_relativa):
return queryset.filter(
Q(frente_set__data_extincao__isnull=True) |
Q(frente_set__data_extincao__gte=data_relativa),
frente_set__data_criacao__lte=data_relativa)
def filter_bancada(self, queryset, data_relativa):
return queryset.filter(
Q(bancada_set__data_extincao__isnull=True) |
Q(bancada_set__data_extincao__gte=data_relativa),
bancada_set__data_criacao__lte=data_relativa)
def filter_bloco(self, queryset, data_relativa):
return queryset.filter(
Q(bloco_set__data_extincao__isnull=True) |
Q(bloco_set__data_extincao__gte=data_relativa),
bloco_set__data_criacao__lte=data_relativa)
def filter_orgao(self, queryset, data_relativa):
# na implementação, não havia regras a implementar para orgao
return queryset
class AutorChoiceSerializer(ModelChoiceSerializer):
def get_text(self, obj):
return obj.nome
class Meta:
model = Autor
fields = ['id', 'nome']
class MateriaLegislativaOldSerializer(serializers.ModelSerializer):
class Meta:
model = MateriaLegislativa
fields = '__all__'
class SessaoPlenariaOldSerializer(serializers.ModelSerializer):
codReuniao = serializers.SerializerMethodField('get_pk_sessao')
codReuniaoPrincipal = serializers.SerializerMethodField('get_pk_sessao')
txtTituloReuniao = serializers.SerializerMethodField('get_name')
txtSiglaOrgao = serializers.SerializerMethodField('get_sigla_orgao')
txtApelido = serializers.SerializerMethodField('get_name')
txtNomeOrgao = serializers.SerializerMethodField('get_nome_orgao')
codEstadoReuniao = serializers.SerializerMethodField(
'get_estadoSessaoPlenaria')
txtTipoReuniao = serializers.SerializerMethodField('get_tipo_sessao')
txtObjeto = serializers.SerializerMethodField('get_assunto_sessao')
txtLocal = serializers.SerializerMethodField('get_endereco_orgao')
bolReuniaoConjunta = serializers.SerializerMethodField(
'get_reuniao_conjunta')
bolHabilitarEventoInterativo = serializers.SerializerMethodField(
'get_iterativo')
idYoutube = serializers.SerializerMethodField('get_url')
codEstadoTransmissaoYoutube = serializers.SerializerMethodField(
'get_estadoTransmissaoYoutube')
datReuniaoString = serializers.SerializerMethodField('get_date')
# Constantes SessaoPlenaria (de 1-9) (apenas 3 serão usados)
SESSAO_FINALIZADA = 4
SESSAO_EM_ANDAMENTO = 3
SESSAO_CONVOCADA = 2
# Constantes EstadoTranmissaoYoutube (de 0 a 2)
TRANSMISSAO_ENCERRADA = 2
TRANSMISSAO_EM_ANDAMENTO = 1
SEM_TRANSMISSAO = 0
class Meta:
model = SessaoPlenaria
fields = (
'codReuniao',
'codReuniaoPrincipal',
'txtTituloReuniao',
'txtSiglaOrgao',
'txtApelido',
'txtNomeOrgao',
'codEstadoReuniao',
'txtTipoReuniao',
'txtObjeto',
'txtLocal',
'bolReuniaoConjunta',
'bolHabilitarEventoInterativo',
'idYoutube',
'codEstadoTransmissaoYoutube',
'datReuniaoString'
)
def __init__(self, *args, **kwargs):
super(SessaoPlenariaOldSerializer, self).__init__(args, kwargs)
def get_pk_sessao(self, obj):
return obj.pk
def get_name(self, obj):
return obj.__str__()
def get_estadoSessaoPlenaria(self, obj):
if obj.finalizada:
return self.SESSAO_FINALIZADA
elif obj.iniciada:
return self.SESSAO_EM_ANDAMENTO
else:
return self.SESSAO_CONVOCADA
def get_tipo_sessao(self, obj):
return obj.tipo.__str__()
def get_url(self, obj):
return obj.url_video if obj.url_video else None
def get_iterativo(self, obj):
return obj.interativa if obj.interativa else False
def get_date(self, obj):
return "{} {}{}".format(
obj.data_inicio.strftime("%d/%m/%Y"),
obj.hora_inicio,
":00"
)
def get_estadoTransmissaoYoutube(self, obj):
if obj.url_video:
if obj.finalizada:
return self.TRANSMISSAO_ENCERRADA
else:
return self.TRANSMISSAO_EM_ANDAMENTO
else:
return self.SEM_TRANSMISSAO
def get_assunto_sessao(self, obj):
pauta_sessao = ''
ordem_dia = OrdemDia.objects.filter(sessao_plenaria=obj.pk)
pauta_sessao = ', '.join([i.materia.__str__() for i in ordem_dia])
return str(pauta_sessao)
def get_endereco_orgao(self, obj):
return self.casa().endereco
def get_reuniao_conjunta(self, obj):
return False
def get_sigla_orgao(self, obj):
return self.casa().sigla
def get_nome_orgao(self, obj):
return self.casa().nome
def casa(self):
casa = CasaLegislativa.objects.first()
return casa
class ModelChoiceView(ListAPIView):
"""
Deprecated
TODO Migrar para customização na api automática
"""
# FIXME aplicar permissão correta de usuário
permission_classes = (IsAuthenticated,)
serializer_class = ModelChoiceSerializer
def get(self, request, *args, **kwargs):
self.model = ContentType.objects.get_for_id(
self.kwargs['content_type']).model_class()
pagination = request.GET.get('pagination', '')
if pagination == 'False':
self.pagination_class = None
return ListAPIView.get(self, request, *args, **kwargs)
def get_queryset(self):
return self.model.objects.all()
class AutorListView(ListAPIView):
"""
Deprecated
TODO Migrar para customização na api automática
Listagem de Autores com filtro para autores cadastrados
e/ou possíveis autores.
- tr - tipo do resultado
Prepera Lista de Autores para 2 cenários distintos
- default = 1
= 1 -> para (value, text) usados geralmente
em combobox, radiobox, checkbox, etc com pesquisa básica
de Autores feita pelo django-filter
-> processo usado nas pesquisas, o mais usado.
= 3 -> Devolve instancias da classe Autor filtradas pelo
django-filter
- tipo - chave primária do Tipo de Autor a ser filtrado
- q - busca textual no nome do Autor ou em fields_search
declarados no field SaplGenericRelation das GenericFks
A busca textual acontece via django-filter com a
variável `tr` igual 1 ou 3. Em caso contrário,
o django-filter é desativado e a busca é feita
no model do ContentType associado ao tipo.
- q_0 / q_1 - q_0 é opcional e quando usado, faz o código ignorar "q"...
q_0 -> campos lookup a serem filtrados em qualquer Model
que implemente SaplGenericRelation
q_1 -> o valor que será pesquisado no lookup de q_0
q_0 e q_1 podem ser separados por ","... isso dará a
possibilidade de filtrar mais de um campo.
http://localhost:8000
/api/autor?tr=1&q_0=parlamentar_set__ativo&q_1=False
/api/autor?tr=1&q_0=parlamentar_set__ativo&q_1=True
/api/autor?tr=3&q_0=parlamentar_set__ativo&q_1=False
/api/autor?tr=3&q_0=parlamentar_set__ativo&q_1=True
http://localhost:8000
/api/autor?tr=1
&q_0=parlamentar_set__nome_parlamentar__icontains,
parlamentar_set__ativo
&q_1=Carvalho,False
/api/autor?tr=1
&q_0=parlamentar_set__nome_parlamentar__icontains,
parlamentar_set__ativo
&q_1=Carvalho,True
/api/autor?tr=3
&q_0=parlamentar_set__nome_parlamentar__icontains,
parlamentar_set__ativo
&q_1=Carvalho,False
/api/autor?tr=3
&q_0=parlamentar_set__nome_parlamentar__icontains,
parlamentar_set__ativo
&q_1=Carvalho,True
não importa o campo que vc passe de qualquer dos Models
ligados... é possível ver que models são esses,
na ocasião do commit deste texto, executando:
In [6]: from sapl.utils import models_with_gr_for_model
In [7]: models_with_gr_for_model(Autor)
Out[7]:
[sapl.parlamentares.models.Parlamentar,
sapl.parlamentares.models.Frente,
sapl.comissoes.models.Comissao,
sapl.materia.models.Orgao,
sapl.sessao.models.Bancada,
sapl.sessao.models.Bloco]
qualquer atributo destes models podem ser passados
para busca
"""
logger = logging.getLogger(__name__)
TR_AUTOR_CHOICE_SERIALIZER = 1
TR_AUTOR_SERIALIZER = 3
permission_classes = (IsAuthenticatedOrReadOnly,)
queryset = Autor.objects.all()
model = Autor
filter_class = AutorChoiceFilterSet
filter_backends = (DjangoFilterBackend,)
serializer_class = AutorChoiceSerializer
@property
def tr(self):
username = self.request.user.username
try:
tr = int(self.request.GET.get
('tr', AutorListView.TR_AUTOR_CHOICE_SERIALIZER))
if tr not in (AutorListView.TR_AUTOR_CHOICE_SERIALIZER,
AutorListView.TR_AUTOR_SERIALIZER):
return AutorListView.TR_AUTOR_CHOICE_SERIALIZER
except Exception as e:
self.logger.error('user=' + username + '. ' + str(e))
return AutorListView.TR_AUTOR_CHOICE_SERIALIZER
return tr
def get(self, request, *args, **kwargs):
if self.tr == AutorListView.TR_AUTOR_SERIALIZER:
self.serializer_class = AutorSerializer
self.permission_classes = (IsAuthenticated,)
if self.filter_class and 'q_0' in request.GET:
self.filter_class = AutorSearchForFieldFilterSet
return ListAPIView.get(self, request, *args, **kwargs)
class AutoresProvaveisListView(ListAPIView):
"""
Deprecated
TODO Migrar para customização na api automática
"""
logger = logging.getLogger(__name__)
permission_classes = (IsAuthenticatedOrReadOnly,)
queryset = Autor.objects.all()
model = Autor
filter_class = None
filter_backends = []
serializer_class = ChoiceSerializer
def get_queryset(self):
params = {'content_type__isnull': False}
username = self.request.user.username
tipo = ''
try:
tipo = int(self.request.GET.get('tipo', ''))
if tipo:
params['id'] = tipo
except Exception as e:
self.logger.error('user= ' + username + '. ' + str(e))
pass
tipos = TipoAutor.objects.filter(**params)
if not tipos.exists() and tipo:
raise Http404()
r = []
for tipo in tipos:
q = self.request.GET.get('q', '').strip()
model_class = tipo.content_type.model_class()
fields = list(filter(
lambda field: isinstance(field, SaplGenericRelation) and
field.related_model == Autor,
model_class._meta.get_fields(include_hidden=True)))
"""
fields - é um array de SaplGenericRelation que deve possuir o
atributo fields_search. Verifique na documentação da classe
a estrutura de fields_search.
"""
assert len(fields) >= 1, (_(
'Não foi encontrado em %(model)s um atributo do tipo '
'SaplGenericRelation que use o model %(model_autor)s') % {
'model': model_class._meta.verbose_name,
'model_autor': Autor._meta.verbose_name})
qs = model_class.objects.all()
q_filter = Q()
if q:
for item in fields:
if item.related_model != Autor:
continue
q_fs = Q()
for field in item.fields_search:
q_fs = q_fs | Q(**{'%s%s' % (
field[0],
field[1]): q})
q_filter = q_filter & q_fs
qs = qs.filter(q_filter).distinct(
fields[0].fields_search[0][0]).order_by(
fields[0].fields_search[0][0])
else:
qs = qs.order_by(fields[0].fields_search[0][0])
qs = qs.values_list(
'id', fields[0].fields_search[0][0])
r += list(qs)
if tipos.count() > 1:
r.sort(key=lambda x: x[1].upper())
return r
class AutoresPossiveisListView(ListAPIView):
"""
Deprecated
TODO Migrar para customização na api automática
"""
permission_classes = (IsAuthenticatedOrReadOnly,)
queryset = Autor.objects.all()
model = Autor
pagination_class = None
filter_class = AutoresPossiveisFilterSet
serializer_class = AutorChoiceSerializer
class MateriaLegislativaViewSet(ListModelMixin,
RetrieveModelMixin,
GenericViewSet):
"""
Deprecated
TODO Migrar para customização na api automática
"""
permission_classes = (IsAuthenticated,)
serializer_class = MateriaLegislativaOldSerializer
queryset = MateriaLegislativa.objects.all()
filter_backends = (DjangoFilterBackend,)
filter_fields = ('numero', 'ano', 'tipo',)
from sapl.api.serializers import SessaoPlenariaECidadaniaSerializer
from sapl.sessao.models import SessaoPlenaria
class SessaoPlenariaViewSet(ListModelMixin,
RetrieveModelMixin,
GenericViewSet):
"""
Deprecated
Deprecated - Será eliminado na versão 3.2
TODO Migrar para customização na api automática
* TODO:
* eliminar endpoint, transferido para SaplApiViewSetConstrutor
* /api/sessao-planaria -> /api/sessao/sessaoplenaria/ecidadania
* /api/sessao-planaria/{pk} -> /api/sessao/sessaoplenaria/{pk}/ecidadania
* verificar se ainda permanece necessidade desses endpoint's
"""
permission_classes = (AllowAny,)
serializer_class = SessaoPlenariaOldSerializer
serializer_class = SessaoPlenariaECidadaniaSerializer
queryset = SessaoPlenaria.objects.all()
filter_backends = (DjangoFilterBackend,)
filter_fields = ('data_inicio', 'data_fim', 'interativa')

182
sapl/api/forms.py

@ -0,0 +1,182 @@
import logging
from django.db.models import Q
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from django_filters.filters import CharFilter, DateFilter, ModelChoiceFilter
from django_filters.filterset import FilterSet
from rest_framework import serializers
from drfautoapi.drfautoapi import ApiFilterSetMixin
from sapl.base.models import TipoAutor, Autor
from sapl.parlamentares.models import Legislatura
from sapl.utils import generic_relations_for_model
logger = logging.getLogger(__name__)
class SaplFilterSetMixin(ApiFilterSetMixin):
pass
class AutorFilterSet(SaplFilterSetMixin):
q = CharFilter(method='filter_q')
tipo = ModelChoiceFilter(queryset=TipoAutor.objects.all())
def filter_q(self, queryset, name, value):
query = value.split(' ')
if query:
q = Q()
for qtext in query:
if not qtext:
continue
q_fs = Q(nome__icontains=qtext) | Q(
tipo__descricao__icontains=qtext)
order_by = []
for gr in generic_relations_for_model(self._meta.model):
sgr = gr[1]
for item in sgr:
if item.related_model != self._meta.model:
continue
flag_order_by = True
for field in item.fields_search:
if flag_order_by:
flag_order_by = False
order_by.append('%s__%s' % (
item.related_query_name(),
field[0])
)
# if len(field) == 3 and field[2](qtext) is not
# None:
q_fs = q_fs | Q(**{'%s__%s%s' % (
item.related_query_name(),
field[0],
field[1]): qtext if len(field) == 2
else field[2](qtext)})
q = q & q_fs
if q:
queryset = queryset.filter(q).order_by(*order_by)
return queryset.distinct()
class AutoresPossiveisFilterSet(SaplFilterSetMixin):
data_relativa = DateFilter(method='filter_data_relativa')
tipo = CharFilter(method='filter_tipo')
class Meta:
model = Autor
fields = ['data_relativa', 'tipo', ]
def filter_data_relativa(self, queryset, name, value):
return queryset
def filter_tipo(self, queryset, name, value):
try:
logger.debug(
"Tentando obter TipoAutor correspondente à pk {}.".format(value))
tipo = TipoAutor.objects.get(pk=value)
except:
logger.error("TipoAutor(pk={}) inexistente.".format(value))
raise serializers.ValidationError(_('Tipo de Autor inexistente.'))
qs = queryset.filter(tipo=tipo)
return qs
@property
def qs(self):
qs = super().qs
data_relativa = self.form.cleaned_data['data_relativa'] \
if 'data_relativa' in self.form.cleaned_data else None
tipo = self.form.cleaned_data['tipo'] \
if 'tipo' in self.form.cleaned_data else None
if not tipo:
return qs
tipo = TipoAutor.objects.get(pk=tipo)
if not tipo.content_type:
return qs
filter_for_model = 'filter_%s' % tipo.content_type.model
if not hasattr(self, filter_for_model):
return qs
if not data_relativa:
data_relativa = timezone.now()
return getattr(self, filter_for_model)(qs, data_relativa).distinct()
def filter_parlamentar(self, queryset, data_relativa):
# não leva em conta afastamentos
legislatura_relativa = Legislatura.objects.filter(
data_inicio__lte=data_relativa,
data_fim__gte=data_relativa).first()
q = Q(
parlamentar_set__mandato__data_inicio_mandato__lte=data_relativa,
parlamentar_set__mandato__data_fim_mandato__isnull=True) | Q(
parlamentar_set__mandato__data_inicio_mandato__lte=data_relativa,
parlamentar_set__mandato__data_fim_mandato__gte=data_relativa)
if legislatura_relativa.atual():
q = q & Q(parlamentar_set__ativo=True)
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(
Q(comissao_set__data_extincao__isnull=True,
comissao_set__data_fim_comissao__isnull=True) |
Q(comissao_set__data_extincao__gte=data_relativa,
comissao_set__data_fim_comissao__isnull=True) |
Q(comissao_set__data_extincao__gte=data_relativa,
comissao_set__data_fim_comissao__isnull=True) |
Q(comissao_set__data_extincao__isnull=True,
comissao_set__data_fim_comissao__gte=data_relativa) |
Q(comissao_set__data_extincao__gte=data_relativa,
comissao_set__data_fim_comissao__gte=data_relativa),
comissao_set__data_criacao__lte=data_relativa)
def filter_frente(self, queryset, data_relativa):
return queryset.filter(
Q(frente_set__data_extincao__isnull=True) |
Q(frente_set__data_extincao__gte=data_relativa),
frente_set__data_criacao__lte=data_relativa)
def filter_bancada(self, queryset, data_relativa):
return queryset.filter(
Q(bancada_set__data_extincao__isnull=True) |
Q(bancada_set__data_extincao__gte=data_relativa),
bancada_set__data_criacao__lte=data_relativa)
def filter_bloco(self, queryset, data_relativa):
return queryset.filter(
Q(bloco_set__data_extincao__isnull=True) |
Q(bloco_set__data_extincao__gte=data_relativa),
bloco_set__data_criacao__lte=data_relativa)
def filter_orgao(self, queryset, data_relativa):
# na implementação, não havia regras a implementar para orgao
return queryset

0
sapl/api/core/schema.py → sapl/api/schema.py

205
sapl/api/serializers.py

@ -1,19 +1,68 @@
import logging
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
from django.db.models import Q
from image_cropping.utils import get_backend
from rest_framework import serializers
from rest_framework.fields import SerializerMethodField
from sapl.api.core.serializers import ModelChoiceObjectRelatedField
from sapl.base.models import Autor
from sapl.base.models import Autor, CasaLegislativa, Metadata
from sapl.parlamentares.models import Parlamentar, Mandato, Legislatura
from sapl.sessao.models import OrdemDia, SessaoPlenaria
class AutorSerializer(serializers.ModelSerializer):
# AutorSerializer sendo utilizado pelo gerador automático da api devidos aos
# critérios anotados em views.py
class SaplSerializerMixin(serializers.ModelSerializer):
__str__ = SerializerMethodField()
metadata = SerializerMethodField()
class Meta:
fields = '__all__'
def get___str__(self, obj) -> str:
return str(obj)
def get_metadata(self, obj) -> dict:
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
class ChoiceSerializer(serializers.Serializer):
value = serializers.SerializerMethodField()
text = serializers.SerializerMethodField()
def get_text(self, obj):
return obj[1]
def get_value(self, obj):
return obj[0]
class ModelChoiceSerializer(ChoiceSerializer):
def get_text(self, obj):
return str(obj)
def get_value(self, obj):
return obj.id
class ModelChoiceObjectRelatedField(serializers.RelatedField):
def to_representation(self, value):
return ModelChoiceSerializer(value).data
class AutorSerializer(SaplSerializerMixin):
autor_related = ModelChoiceObjectRelatedField(read_only=True)
@ -22,17 +71,28 @@ class AutorSerializer(serializers.ModelSerializer):
fields = '__all__'
class ParlamentarSerializerPublic(serializers.ModelSerializer):
class CasaLegislativaSerializer(SaplSerializerMixin):
version = serializers.SerializerMethodField()
def get_version(self, obj):
return settings.SAPL_VERSION
class Meta:
model = CasaLegislativa
fields = '__all__'
class ParlamentarSerializerPublic(SaplSerializerMixin):
class Meta:
model = Parlamentar
exclude = ["cpf", "rg", "fax",
exclude = ["cpf", "rg", "fax", "data_nascimento",
"endereco_residencia", "municipio_residencia",
"uf_residencia", "cep_residencia", "situacao_militar",
"telefone_residencia", "titulo_eleitor", "fax_residencia"]
class ParlamentarSerializerVerbose(serializers.ModelSerializer):
class ParlamentarSerializerVerbose(SaplSerializerMixin):
titular = serializers.SerializerMethodField('check_titular')
partido = serializers.SerializerMethodField('check_partido')
fotografia_cropped = serializers.SerializerMethodField('crop_fotografia')
@ -44,6 +104,7 @@ class ParlamentarSerializerVerbose(serializers.ModelSerializer):
import os
if not obj.fotografia or not os.path.exists(obj.fotografia.path):
return thumbnail_url
self.logger.warning(f"Iniciando cropping da imagem {obj.fotografia}")
thumbnail_url = get_backend().get_thumbnail_url(
obj.fotografia,
{
@ -53,9 +114,11 @@ class ParlamentarSerializerVerbose(serializers.ModelSerializer):
'detail': True,
}
)
self.logger.warning(f"Cropping da imagem {obj.fotografia} realizado com sucesso")
except Exception as e:
self.logger.error(e)
self.logger.error('erro processando arquivo: %s' % obj.fotografia.path)
self.logger.error('erro processando arquivo: %s' %
obj.fotografia.path)
return thumbnail_url
@ -66,7 +129,8 @@ class ParlamentarSerializerVerbose(serializers.ModelSerializer):
return ""
try:
legislatura = Legislatura.objects.get(id=self.context.get('legislatura'))
legislatura = Legislatura.objects.get(
id=self.context.get('legislatura'))
except ObjectDoesNotExist:
legislatura = Legislatura.objects.first()
mandato = Mandato.objects.filter(
@ -92,7 +156,8 @@ class ParlamentarSerializerVerbose(serializers.ModelSerializer):
self.logger.error("Não há legislaturas cadastradas.")
return ""
try:
legislatura = Legislatura.objects.get(id=self.context.get('legislatura'))
legislatura = Legislatura.objects.get(
id=self.context.get('legislatura'))
except ObjectDoesNotExist:
legislatura = Legislatura.objects.first()
@ -131,4 +196,120 @@ class ParlamentarSerializerVerbose(serializers.ModelSerializer):
class Meta:
model = Parlamentar
fields = ['id', 'nome_parlamentar', 'fotografia_cropped', 'fotografia', 'ativo', 'partido', 'titular']
fields = ['id', 'nome_parlamentar', 'fotografia_cropped',
'fotografia', 'ativo', 'partido', 'titular', ]
class SessaoPlenariaECidadaniaSerializer(serializers.ModelSerializer):
codReuniao = serializers.SerializerMethodField('get_pk_sessao')
codReuniaoPrincipal = serializers.SerializerMethodField('get_pk_sessao')
txtTituloReuniao = serializers.SerializerMethodField('get_name')
txtSiglaOrgao = serializers.SerializerMethodField('get_sigla_orgao')
txtApelido = serializers.SerializerMethodField('get_name')
txtNomeOrgao = serializers.SerializerMethodField('get_nome_orgao')
codEstadoReuniao = serializers.SerializerMethodField(
'get_estadoSessaoPlenaria')
txtTipoReuniao = serializers.SerializerMethodField('get_tipo_sessao')
txtObjeto = serializers.SerializerMethodField('get_assunto_sessao')
txtLocal = serializers.SerializerMethodField('get_endereco_orgao')
bolReuniaoConjunta = serializers.SerializerMethodField(
'get_reuniao_conjunta')
bolHabilitarEventoInterativo = serializers.SerializerMethodField(
'get_iterativo')
idYoutube = serializers.SerializerMethodField('get_url')
codEstadoTransmissaoYoutube = serializers.SerializerMethodField(
'get_estadoTransmissaoYoutube')
datReuniaoString = serializers.SerializerMethodField('get_date')
# Constantes SessaoPlenaria (de 1-9) (apenas 3 serão usados)
SESSAO_FINALIZADA = 4
SESSAO_EM_ANDAMENTO = 3
SESSAO_CONVOCADA = 2
# Constantes EstadoTranmissaoYoutube (de 0 a 2)
TRANSMISSAO_ENCERRADA = 2
TRANSMISSAO_EM_ANDAMENTO = 1
SEM_TRANSMISSAO = 0
class Meta:
model = SessaoPlenaria
fields = (
'codReuniao',
'codReuniaoPrincipal',
'txtTituloReuniao',
'txtSiglaOrgao',
'txtApelido',
'txtNomeOrgao',
'codEstadoReuniao',
'txtTipoReuniao',
'txtObjeto',
'txtLocal',
'bolReuniaoConjunta',
'bolHabilitarEventoInterativo',
'idYoutube',
'codEstadoTransmissaoYoutube',
'datReuniaoString'
)
def get_pk_sessao(self, obj):
return obj.pk
def get_name(self, obj):
return obj.__str__()
def get_estadoSessaoPlenaria(self, obj):
if obj.finalizada:
return self.SESSAO_FINALIZADA
elif obj.iniciada:
return self.SESSAO_EM_ANDAMENTO
else:
return self.SESSAO_CONVOCADA
def get_tipo_sessao(self, obj):
return obj.tipo.__str__()
def get_url(self, obj):
return obj.url_video if obj.url_video else None
def get_iterativo(self, obj):
return obj.interativa if obj.interativa else False
def get_date(self, obj):
return "{} {}{}".format(
obj.data_inicio.strftime("%d/%m/%Y"),
obj.hora_inicio,
":00"
)
def get_estadoTransmissaoYoutube(self, obj):
if obj.url_video:
if obj.finalizada:
return self.TRANSMISSAO_ENCERRADA
else:
return self.TRANSMISSAO_EM_ANDAMENTO
else:
return self.SEM_TRANSMISSAO
def get_assunto_sessao(self, obj):
pauta_sessao = ''
ordem_dia = OrdemDia.objects.filter(sessao_plenaria=obj.pk)
pauta_sessao = ', '.join([i.materia.__str__() for i in ordem_dia])
return str(pauta_sessao)
def get_endereco_orgao(self, obj):
return self.casa().endereco
def get_reuniao_conjunta(self, obj):
return False
def get_sigla_orgao(self, obj):
return self.casa().sigla
def get_nome_orgao(self, obj):
return self.casa().nome
def casa(self):
casa = CasaLegislativa.objects.first()
return casa

10
sapl/api/signals.py

@ -0,0 +1,10 @@
from django.conf import settings
from django.db.models.signals import post_save
from django.dispatch.dispatcher import receiver
from rest_framework.authtoken.models import Token
@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_auth_token(sender, instance=None, created=False, **kwargs):
if created:
Token.objects.create(user=instance)

51
sapl/api/urls.py

@ -3,61 +3,42 @@ from django.conf.urls import include, url
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, \
ModelChoiceView
from sapl.api.views import AppVersionView, recria_token
from sapl.api.viewset import SaplApiViewSetConstrutor
from sapl.api.deprecated import SessaoPlenariaViewSet
from sapl.api.views import AppVersionView, recria_token,\
SaplApiViewSetConstrutor
from .apps import AppConfig
app_name = AppConfig.name
router = DefaultRouter()
router.register(r'materia$', MateriaLegislativaViewSet)
router.register(r'sessao-plenaria', SessaoPlenariaViewSet)
router = SaplApiViewSetConstrutor.router()
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)
# TODO: eliminar endpoint, transferido para SaplApiViewSetConstrutor
# verificar se ainda permanece necessidade desses endpoint's
# /api/sessao-planaria -> /api/sessao/sessaoplenaria/ecidadania
# /api/sessao-planaria/{pk} -> /api/sessao/sessaoplenaria/{pk}/ecidadania
router.register(r'sessao-plenaria', SessaoPlenariaViewSet,
basename='sessao_plenaria_old')
urlpatterns_router = router.urls
urlpatterns_api_doc = [
# Optional UI:
url('^schema/swagger-ui/',
SpectacularSwaggerView.as_view(url_name='sapl.api:schema_api'), name='swagger_ui_schema_api'),
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
SpectacularRedocView.as_view(url_name='sapl.api:schema_api'),
name='redoc_schema_api'),
url('^schema/', SpectacularAPIView.as_view(), name='schema_api'),
]
# TODO: refatorar para customização da api automática
deprecated_urlpatterns_api = [
url(r'^autor/provaveis',
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 = [
url(r'^api/', include(deprecated_urlpatterns_api)),
url(r'^api/', include(urlpatterns_api_doc)),
url(r'^api/', include(urlpatterns_router)),
url(r'^api/version', AppVersionView.as_view()),
url(r'^api/recriar-token/(?P<pk>\d*)$', recria_token, name="recria_token"),
url(r'^api/version', AppVersionView.as_view()),
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')),
url(r'^api/recriar-token/(?P<pk>\d*)$', recria_token, name="recria_token"),
]

91
sapl/api/views.py

@ -1,26 +1,21 @@
import logging
from django.conf import settings
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.utils.translation import ugettext_lazy as _
from rest_framework.authtoken.models import Token
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 drfautoapi.drfautoapi import ApiViewSetConstrutor
@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_auth_token(sender, instance=None, created=False, **kwargs):
if created:
Token.objects.create(user=instance)
logger = logging.getLogger(__name__)
@api_view(['POST'])
@permission_classes([IsAdminUser])
def recria_token(request, pk):
Token.objects.get(user_id=pk).delete()
Token.objects.filter(user_id=pk).delete()
token = Token.objects.create(user_id=pk)
return Response({"message": "Token recriado com sucesso!", "token": token.key})
@ -38,3 +33,83 @@ class AppVersionView(APIView):
'is_authenticated': request.user.is_authenticated,
}
return Response(content)
SaplApiViewSetConstrutor = ApiViewSetConstrutor
SaplApiViewSetConstrutor.import_modules([
'sapl.api.views_audiencia',
'sapl.api.views_base',
'sapl.api.views_comissoes',
'sapl.api.views_compilacao',
'sapl.api.views_materia',
'sapl.api.views_norma',
'sapl.api.views_painel',
'sapl.api.views_parlamentares',
'sapl.api.views_protocoloadm',
'sapl.api.views_sessao',
])
"""
1. ApiViewSetConstrutor constroi uma rest_framework.viewsets.ModelViewSet
para todos os models de todas as app_configs passadas no list
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.1.1 - se existir um DEFAULT_SERIALIZER_MODULE em settings,
recupera Serializer customizados no módulo DEFAULT_SERIALIZER_MODULE
3.2 - Para todo model é opcional a existência de {model}Serializer.
Caso não seja definido um Serializer customizado, utiliza-se o genérico
3.3 - Caso exista GLOBAL_SERIALIZER_MIXIN definido,
utiliza este Serializer para construir o genérico de 3.1
4. Define um FilterSet como a seguir:
4.1 - Define um FilterSet genérico para cada módel
4.1.1 - se existir um DEFAULT_FILTER_MODULE em settings,
recupera o FilterSet customizado no módulo DEFAULT_FILTER_MODULE
4.2 - Para todo model é opcional a existência de {model}FilterSet.
Caso não seja definido um FilterSet customizado, utiliza-se o genérico
4.3 - Caso exista GLOBAL_FILTERSET_MIXIN definido,
utiliza este FilterSet para construir o genérico de 4.1
4.4 - Caso não exista GLOBAL_FILTERSET_MIXIN, será aplicado
drfautoapi.drjautoapi.ApiFilterSetMixin que inclui parametro para:
- order_by: através do parâmetro "o"
- amplia os lookups aceitos pelo FilterSet default
para os aceitos pelo django sem a necessidade de criar
fields específicos em um FilterSet customizado.
5. ApiViewSetConstrutor 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
**ApiViewSetConstrutor._built_sets** é um dict de dicts de models conforme:
{
...
'audiencia': {
'tipoaudienciapublica': TipoAudienciaPublicaViewSet,
'audienciapublica': AudienciaPublicaViewSet,
'anexoaudienciapublica': AnexoAudienciaPublicaViewSet
...
},
...
'base': {
'casalegislativa': CasaLegislativaViewSet,
'appconfig': AppConfigViewSet,
...
}
...
}
"""

11
sapl/api/views_audiencia.py

@ -0,0 +1,11 @@
from django.apps.registry import apps
from drfautoapi.drfautoapi import ApiViewSetConstrutor, \
customize, wrapper_queryset_response_for_drf_action
AudienciaApiViewSetConstrutor = ApiViewSetConstrutor.build_class(
[
apps.get_app_config('audiencia')
]
)

180
sapl/api/views_base.py

@ -0,0 +1,180 @@
import logging
from django.apps.registry import apps
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
from django.http.response import Http404
from rest_framework.decorators import action
from rest_framework.response import Response
from drfautoapi.drfautoapi import ApiViewSetConstrutor, customize
from sapl.api.forms import AutoresPossiveisFilterSet
from sapl.api.serializers import ChoiceSerializer
from sapl.base.models import Autor, TipoAutor
from sapl.utils import models_with_gr_for_model, SaplGenericRelation
logger = logging.getLogger(__name__)
ApiViewSetConstrutor.build_class(
[
apps.get_app_config('contenttypes'),
apps.get_app_config('base')
]
)
@customize(ContentType)
class _ContentTypeSet:
http_method_names = ['get', 'head', 'options', 'trace']
@customize(Autor)
class _AutorViewSet:
"""
Nesta customização do que foi criado em
ApiViewSetConstrutor 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 local:
* /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)
@classmethod
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
@action(detail=False)
def possiveis(self, request, *args, **kwargs):
self.filterset_class = AutoresPossiveisFilterSet
return self.list(request, *args, **kwargs)
@action(detail=False)
def provaveis(self, request, *args, **kwargs):
self.get_queryset = self.provaveis__get_queryset
self.filter_backends = []
self.filterset_class = None
self.serializer_class = ChoiceSerializer
return self.list(request, *args, **kwargs)
def provaveis__get_queryset(self):
params = {'content_type__isnull': False}
username = self.request.user.username
tipo = ''
try:
tipo = int(self.request.GET.get('tipo', ''))
if tipo:
params['id'] = tipo
except Exception as e:
logger.error('user= ' + username + '. ' + str(e))
pass
tipos = TipoAutor.objects.filter(**params)
if not tipos.exists() and tipo:
raise Http404()
r = []
for tipo in tipos:
q = self.request.GET.get('q', '').strip()
model_class = tipo.content_type.model_class()
fields = list(filter(
lambda field: isinstance(field, SaplGenericRelation) and
field.related_model == Autor,
model_class._meta.get_fields(include_hidden=True)))
"""
fields - é um array de SaplGenericRelation que deve possuir o
atributo fields_search. Verifique na documentação da classe
a estrutura de fields_search.
"""
assert len(fields) >= 1, (_(
'Não foi encontrado em %(model)s um atributo do tipo '
'SaplGenericRelation que use o model %(model_autor)s') % {
'model': model_class._meta.verbose_name,
'model_autor': Autor._meta.verbose_name})
qs = model_class.objects.all()
q_filter = Q()
if q:
for item in fields:
if item.related_model != Autor:
continue
q_fs = Q()
for field in item.fields_search:
q_fs = q_fs | Q(**{'%s%s' % (
field[0],
field[1]): q})
q_filter = q_filter & q_fs
qs = qs.filter(q_filter).distinct(
fields[0].fields_search[0][0]).order_by(
fields[0].fields_search[0][0])
else:
qs = qs.order_by(fields[0].fields_search[0][0])
qs = qs.values_list(
'id', fields[0].fields_search[0][0])
r += list(qs)
if tipos.count() > 1:
r.sort(key=lambda x: x[1].upper())
return r

12
sapl/api/views_comissoes.py

@ -0,0 +1,12 @@
from django.apps.registry import apps
from drfautoapi.drfautoapi import ApiViewSetConstrutor, \
customize, wrapper_queryset_response_for_drf_action
ApiViewSetConstrutor.build_class(
[
apps.get_app_config('comissoes')
]
)

12
sapl/api/views_compilacao.py

@ -0,0 +1,12 @@
from django.apps.registry import apps
from drfautoapi.drfautoapi import ApiViewSetConstrutor, \
customize, wrapper_queryset_response_for_drf_action
ApiViewSetConstrutor.build_class(
[
apps.get_app_config('compilacao')
]
)

129
sapl/api/views_materia.py

@ -0,0 +1,129 @@
from django.apps.registry import apps
from django.db.models import Q
from rest_framework.decorators import action
from rest_framework.response import Response
from drfautoapi.drfautoapi import ApiViewSetConstrutor, \
customize, wrapper_queryset_response_for_drf_action
from sapl.api.permissions import SaplModelPermissions
from sapl.materia.models import TipoMateriaLegislativa, Tramitacao,\
MateriaLegislativa, Proposicao
ApiViewSetConstrutor.build_class(
[
apps.get_app_config('materia')
]
)
@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 = ApiViewSetConstrutor.get_viewset_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)

14
sapl/api/views_norma.py

@ -0,0 +1,14 @@
from django.apps.registry import apps
from rest_framework.decorators import action
from drfautoapi.drfautoapi import ApiViewSetConstrutor, \
customize, wrapper_queryset_response_for_drf_action
from sapl.norma.models import NormaJuridica
ApiViewSetConstrutor.build_class(
[
apps.get_app_config('norma')
]
)

11
sapl/api/views_painel.py

@ -0,0 +1,11 @@
from django.apps.registry import apps
from drfautoapi.drfautoapi import ApiViewSetConstrutor, \
customize, wrapper_queryset_response_for_drf_action
ApiViewSetConstrutor.build_class(
[
apps.get_app_config('painel')
]
)

119
sapl/api/views_parlamentares.py

@ -0,0 +1,119 @@
from django.apps.registry import apps
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
from rest_framework.decorators import action
from rest_framework.response import Response
from drfautoapi.drfautoapi import customize, ApiViewSetConstrutor, \
wrapper_queryset_response_for_drf_action
from sapl.api.permissions import SaplModelPermissions
from sapl.api.serializers import ParlamentarSerializerVerbose, \
ParlamentarSerializerPublic
from sapl.materia.models import Proposicao
from sapl.parlamentares.models import Mandato, Legislatura
from sapl.parlamentares.models import Parlamentar
ApiViewSetConstrutor.build_class(
[
apps.get_app_config('parlamentares')
]
)
@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

102
sapl/api/views_protocoloadm.py

@ -0,0 +1,102 @@
from django.apps.registry import apps
from drfautoapi.drfautoapi import ApiViewSetConstrutor, \
customize, wrapper_queryset_response_for_drf_action
from sapl.api.permissions import SaplModelPermissions
from sapl.base.models import AppConfig, DOC_ADM_OSTENSIVO
from sapl.protocoloadm.models import DocumentoAdministrativo, \
DocumentoAcessorioAdministrativo, TramitacaoAdministrativo, Anexado
ApiViewSetConstrutor.build_class(
[
apps.get_app_config('protocoloadm')
]
)
@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:
# TODO: Implementar regras de manutenção das post, put, patch
# tramitacação de adm possui regras previstas de limitação de origem
# destino
http_method_names = ['get', 'head', 'options', 'trace']
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:
# TODO: Implementar regras de manutenção post, put, patch
# anexado deve possuir controle que impeça anexação cíclica
http_method_names = ['get', 'head', 'options', 'trace']
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

47
sapl/api/views_sessao.py

@ -0,0 +1,47 @@
from django.apps.registry import apps
from rest_framework.decorators import action
from rest_framework.response import Response
from drfautoapi.drfautoapi import ApiViewSetConstrutor, \
customize, wrapper_queryset_response_for_drf_action
from sapl.api.serializers import ChoiceSerializer,\
SessaoPlenariaECidadaniaSerializer
from sapl.sessao.models import SessaoPlenaria, ExpedienteSessao
from sapl.utils import choice_anos_com_sessaoplenaria
ApiViewSetConstrutor.build_class(
[
apps.get_app_config('sessao')
]
)
@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'])
@action(detail=True)
def ecidadania(self, request, *args, **kwargs):
self.serializer_class = SessaoPlenariaECidadaniaSerializer
return self.retrieve(request, *args, **kwargs)
@action(detail=False, url_path='ecidadania')
def ecidadania_list(self, request, *args, **kwargs):
self.serializer_class = SessaoPlenariaECidadaniaSerializer
return self.list(request, *args, **kwargs)

413
sapl/api/viewset.py

@ -1,413 +0,0 @@
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)

31
sapl/audiencia/forms.py

@ -1,5 +1,7 @@
import logging
from datetime import datetime
from django import forms
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.db import transaction
@ -13,6 +15,7 @@ from sapl.materia.models import MateriaLegislativa, TipoMateriaLegislativa
from sapl.parlamentares.models import Parlamentar
from sapl.utils import timezone, FileFieldCheckMixin, validar_arquivo
class AudienciaForm(FileFieldCheckMixin, forms.ModelForm):
logger = logging.getLogger(__name__)
data_atual = timezone.now()
@ -53,7 +56,7 @@ class AudienciaForm(FileFieldCheckMixin, forms.ModelForm):
class Meta:
model = AudienciaPublica
fields = ['tipo', 'numero', 'nome',
fields = ['tipo', 'numero', 'ano', 'nome',
'tema', 'data', 'hora_inicio', 'hora_fim',
'observacao', 'audiencia_cancelada', 'parlamentar_autor', 'requerimento', 'url_audio',
'url_video', 'upload_pauta', 'upload_ata',
@ -85,6 +88,26 @@ class AudienciaForm(FileFieldCheckMixin, forms.ModelForm):
parlamentar_autor = cleaned_data["parlamentar_autor"]
requerimento = cleaned_data["requerimento"]
if cleaned_data["ano"] != cleaned_data["data"].year:
raise ValidationError(f"Ano da audiência ({cleaned_data['ano']}) difere "
f"do ano no campo data ({cleaned_data['data'].year})")
#
# TODO: converter hora_inicio e hora_fim para TimeField
#
# valida hora inicio
try:
datetime.strptime(cleaned_data["hora_inicio"], '%H:%M').time()
except ValueError:
raise ValidationError(f"Formato de horário de início inválido: {cleaned_data['hora_inicio']}")
# valida hora fim
if cleaned_data["hora_fim"]:
try:
datetime.strptime(cleaned_data["hora_fim"], '%H:%M').time()
except ValueError:
raise ValidationError(f"Formato de horário de fim inválido: {cleaned_data['hora_fim']}")
if materia and ano_materia and tipo_materia:
try:
self.logger.debug("Tentando obter MateriaLegislativa %s%s/%s." % (tipo_materia, materia, ano_materia))
@ -115,14 +138,14 @@ class AudienciaForm(FileFieldCheckMixin, forms.ModelForm):
raise ValidationError(msg)
if not cleaned_data['numero']:
ultima_audiencia = AudienciaPublica.objects.all().order_by('numero').last()
ultima_audiencia = AudienciaPublica.objects.all().order_by('ano', 'numero').last()
if ultima_audiencia:
cleaned_data['numero'] = ultima_audiencia.numero + 1
else:
cleaned_data['numero'] = 1
else:
if AudienciaPublica.objects.filter(numero=cleaned_data['numero']).exclude(pk=self.instance.pk).exists():
raise ValidationError(f"Já existe uma audiência com a numeração {cleaned_data['numero']}.")
if AudienciaPublica.objects.filter(numero=cleaned_data['numero'], ano=cleaned_data['ano']).exclude(pk=self.instance.pk).exists():
raise ValidationError(f"Já existe uma audiência pública com a numeração {str(cleaned_data['numero']).rjust(3, '0')}/{cleaned_data['ano']}.")
if self.cleaned_data['hora_inicio'] and self.cleaned_data['hora_fim']:
if self.cleaned_data['hora_fim'] < self.cleaned_data['hora_inicio']:

27
sapl/audiencia/migrations/0017_audienciapublica_ano.py

@ -0,0 +1,27 @@
# Generated by Django 2.2.28 on 2023-01-31 03:01
from django.db import migrations, models
def preencher_ano(apps, schema_editor):
AudienciaPublica = apps.get_model('audiencia', 'AudienciaPublica')
for audiencia in AudienciaPublica.objects.all():
audiencia.ano = audiencia.data.year
audiencia.save()
class Migration(migrations.Migration):
dependencies = [
('audiencia', '0016_auto_20201013_1126'),
]
operations = [
migrations.AddField(
model_name='audienciapublica',
name='ano',
field=models.PositiveSmallIntegerField(choices=[(2024, 2024), (2023, 2023), (2022, 2022), (2021, 2021), (2020, 2020), (2019, 2019), (2018, 2018), (2017, 2017), (2016, 2016), (2015, 2015), (2014, 2014), (2013, 2013), (2012, 2012), (2011, 2011), (2010, 2010), (2009, 2009), (2008, 2008), (2007, 2007), (2006, 2006), (2005, 2005), (2004, 2004), (2003, 2003), (2002, 2002), (2001, 2001), (2000, 2000), (1999, 1999), (1998, 1998), (1997, 1997), (1996, 1996), (1995, 1995), (1994, 1994), (1993, 1993), (1992, 1992), (1991, 1991), (1990, 1990), (1989, 1989), (1988, 1988), (1987, 1987), (1986, 1986), (1985, 1985), (1984, 1984), (1983, 1983), (1982, 1982), (1981, 1981), (1980, 1980), (1979, 1979), (1978, 1978), (1977, 1977), (1976, 1976), (1975, 1975), (1974, 1974), (1973, 1973), (1972, 1972), (1971, 1971), (1970, 1970), (1969, 1969), (1968, 1968), (1967, 1967), (1966, 1966), (1965, 1965), (1964, 1964), (1963, 1963), (1962, 1962), (1961, 1961), (1960, 1960), (1959, 1959), (1958, 1958), (1957, 1957), (1956, 1956), (1955, 1955), (1954, 1954), (1953, 1953), (1952, 1952), (1951, 1951), (1950, 1950), (1949, 1949), (1948, 1948), (1947, 1947), (1946, 1946), (1945, 1945), (1944, 1944), (1943, 1943), (1942, 1942), (1941, 1941), (1940, 1940), (1939, 1939), (1938, 1938), (1937, 1937), (1936, 1936), (1935, 1935), (1934, 1934), (1933, 1933), (1932, 1932), (1931, 1931), (1930, 1930), (1929, 1929), (1928, 1928), (1927, 1927), (1926, 1926), (1925, 1925), (1924, 1924), (1923, 1923), (1922, 1922), (1921, 1921), (1920, 1920), (1919, 1919), (1918, 1918), (1917, 1917), (1916, 1916), (1915, 1915), (1914, 1914), (1913, 1913), (1912, 1912), (1911, 1911), (1910, 1910), (1909, 1909), (1908, 1908), (1907, 1907), (1906, 1906), (1905, 1905), (1904, 1904), (1903, 1903), (1902, 1902), (1901, 1901), (1900, 1900), (1899, 1899), (1898, 1898), (1897, 1897), (1896, 1896), (1895, 1895), (1894, 1894), (1893, 1893), (1892, 1892), (1891, 1891), (1890, 1890)], default=2000, verbose_name='Ano'),
preserve_default=False,
),
migrations.RunPython(preencher_ano),
]

17
sapl/audiencia/migrations/0018_auto_20230529_1641.py

@ -0,0 +1,17 @@
# Generated by Django 2.2.28 on 2023-05-29 19:41
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('audiencia', '0017_audienciapublica_ano'),
]
operations = [
migrations.AlterModelOptions(
name='audienciapublica',
options={'ordering': ['ano', 'numero', 'nome', 'tipo'], 'verbose_name': 'Audiência Pública', 'verbose_name_plural': 'Audiências Públicas'},
),
]

12
sapl/audiencia/models.py

@ -1,4 +1,3 @@
import reversion
from django.db import models
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
@ -6,7 +5,7 @@ from model_utils import Choices
from sapl.materia.models import MateriaLegislativa
from sapl.parlamentares.models import (CargoMesa, Parlamentar)
from sapl.utils import (YES_NO_CHOICES, SaplGenericRelation,
from sapl.utils import (RANGE_ANOS, YES_NO_CHOICES, SaplGenericRelation,
restringe_tipos_de_arquivo_txt, texto_upload_path,
OverwriteStorage)
@ -29,7 +28,6 @@ def anexo_upload_path(instance, filename):
instance, filename, subpath='anexo', pk_first=True)
@reversion.register()
class TipoAudienciaPublica(models.Model):
TIPO_AUDIENCIA_CHOICES = Choices(('A', 'audiencia', _('Audiência Pública')),
('P', 'plebiscito', _('Plebiscito')),
@ -41,7 +39,6 @@ class TipoAudienciaPublica(models.Model):
tipo = models.CharField(
max_length=1, verbose_name=_('Tipo de Audiência Pública'), choices=TIPO_AUDIENCIA_CHOICES, default='A')
class Meta:
verbose_name = _('Tipo de Audiência Pública')
verbose_name_plural = _('Tipos de Audiência Pública')
@ -51,8 +48,8 @@ class TipoAudienciaPublica(models.Model):
return self.nome
@reversion.register()
class AudienciaPublica(models.Model):
materia = models.ForeignKey(
MateriaLegislativa,
on_delete=models.PROTECT,
@ -65,6 +62,8 @@ class AudienciaPublica(models.Model):
blank=True,
verbose_name=_('Tipo de Audiência Pública'))
numero = models.PositiveIntegerField(blank=True, verbose_name=_('Número'))
ano = models.PositiveSmallIntegerField(verbose_name=_('Ano'),
choices=RANGE_ANOS)
nome = models.CharField(
max_length=100, verbose_name=_('Nome da Audiência Pública'))
tema = models.CharField(
@ -126,7 +125,7 @@ class AudienciaPublica(models.Model):
class Meta:
verbose_name = _('Audiência Pública')
verbose_name_plural = _('Audiências Públicas')
ordering = ['nome', 'numero', 'tipo']
ordering = ['ano', 'numero', 'nome', 'tipo']
def __str__(self):
return self.nome
@ -175,7 +174,6 @@ class AudienciaPublica(models.Model):
update_fields=update_fields)
@reversion.register()
class AnexoAudienciaPublica(models.Model):
audiencia = models.ForeignKey(AudienciaPublica,
on_delete=models.PROTECT)

12
sapl/audiencia/views.py

@ -19,8 +19,8 @@ class AudienciaCrud(Crud):
public = [RP_LIST, RP_DETAIL, ]
class BaseMixin(Crud.BaseMixin):
list_field_names = [ 'nome', 'tipo', 'materia', 'data']
ordering = '-data', 'nome', 'numero', 'tipo'
list_field_names = ['numero', 'nome', 'tipo', 'materia', 'data']
ordering = '-ano', '-numero', '-data', 'nome', 'tipo'
class ListView(Crud.ListView):
paginate_by = 10
@ -28,20 +28,20 @@ class AudienciaCrud(Crud):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
audiencia_materia = { str(a.id): (a.materia, a.numero) for a in context['object_list'] }
audiencia_materia = {str(a.id): (a.materia, a.numero, a.ano) for a in context['object_list']}
for row in context['rows']:
audiencia_id = row[0][1].split('/')[-1]
tema = str(audiencia_materia[audiencia_id][1]) + ' - ' + row[0][0]
tema = str(audiencia_materia[audiencia_id][1]).rjust(3, '0') + '/' + str(audiencia_materia[audiencia_id][2])
row[0] = (tema, row[0][1])
coluna_materia = row[2] # Se mudar a ordem de listagem, mudar aqui.
coluna_materia = row[3] # Se mudar a ordem de listagem, mudar aqui.
if coluna_materia[0]:
materia = audiencia_materia[audiencia_id][0]
if materia is not None:
url_materia = reverse('sapl.materia:materialegislativa_detail', kwargs={'pk': materia.id})
else:
url_materia = None
row[2] = (coluna_materia[0], url_materia) # Se mudar a ordem de listagem, mudar aqui.
row[3] = (coluna_materia[0], url_materia) # Se mudar a ordem de listagem, mudar aqui.
return context
class CreateView(Crud.CreateView):

13
sapl/base/admin.py

@ -1,7 +1,6 @@
from django.contrib import admin
from django.shortcuts import redirect
from django.utils.translation import ugettext_lazy as _
from reversion.models import Revision
from sapl.base.models import AuditLog
from sapl.utils import register_all_models_in_admin
@ -12,18 +11,6 @@ admin.site.site_title = 'Administração - SAPL'
admin.site.site_header = 'Administração - SAPL'
class RevisionAdmin(admin.ModelAdmin):
list_display = ('user', 'comment', 'date_created')
search_fields = ('=user__username', '=user__email')
date_hierarchy = ('date_created')
def change_view(self, request, obj=None):
self.message_user(request, _('You cannot change history.'))
return redirect('admin:reversion_revision_changelist')
admin.site.register(Revision, RevisionAdmin)
class AuditLogAdmin(admin.ModelAdmin):
pass

888
sapl/base/forms.py

File diff suppressed because it is too large

7
sapl/base/legacy.yaml

@ -1,7 +0,0 @@
TipoAutor:
descricao: des_tipo_autor
Autor:
nome: nom_autor
cargo: des_cargo
tipo: tip_autor

38
sapl/base/management/commands/backfill_auditlog.py

@ -0,0 +1,38 @@
import json
import logging
from django.core.management.base import BaseCommand
from sapl.base.models import AuditLog
logger = logging.getLogger(__name__)
class Command(BaseCommand):
def handle(self, **options):
print("Backfilling AuditLog JSON Field...")
logs = AuditLog.objects.filter(data__isnull=True)
error_counter = 0
if logs:
update_list = []
for log in logs:
try:
obj = log.object[1:-1] \
if log.object.startswith('[') else log.object
data = json.loads(obj)
log.data = data
except Exception as e:
error_counter += 1
logging.error(e)
log.data = None
else:
update_list.append(log)
if len(update_list) == 1000:
AuditLog.objects.bulk_update(update_list, ['data'])
update_list = []
if update_list:
AuditLog.objects.bulk_update(update_list, ['data'])
print(f"Logs backfilled: {len(logs) - error_counter}")
print(f"Logs with errors: {error_counter}")
print("Finished backfilling")

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

15
sapl/base/migrations/0052_auto_20220914_1125.py

@ -0,0 +1,15 @@
# Generated by Django 2.2.28 on 2022-09-14 14:25
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('base', '0051_auto_20220814_2138'),
]
operations = [
migrations.RunSQL("DROP TABLE IF EXISTS reversion_version"),
migrations.RunSQL("DROP TABLE IF EXISTS reversion_revision"),
]

18
sapl/base/migrations/0053_auto_20220919_1705.py

@ -0,0 +1,18 @@
# Generated by Django 2.2.28 on 2022-09-19 20:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('base', '0052_auto_20220914_1125'),
]
operations = [
migrations.AlterField(
model_name='appconfig',
name='sapl_as_sapn',
field=models.BooleanField(choices=[(True, 'Sim'), (False, 'Não')], default=False, verbose_name='Utilizar SAPL apenas como SAPL-Normas?'),
),
]

14
sapl/base/migrations/0054_auto_20220921_1217.py

@ -0,0 +1,14 @@
# Generated by Django 2.2.28 on 2022-09-21 15:17
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('base', '0053_auto_20220919_1705'),
]
operations = [
migrations.RunSQL("DROP TABLE IF EXISTS speedinfo_viewprofiler"),
]

18
sapl/base/migrations/0055_appconfig_mostrar_voto.py

@ -0,0 +1,18 @@
# Generated by Django 2.2.28 on 2022-10-05 18:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('base', '0054_auto_20220921_1217'),
]
operations = [
migrations.AddField(
model_name='appconfig',
name='mostrar_voto',
field=models.BooleanField(choices=[(True, 'Sim'), (False, 'Não')], default=False, verbose_name='Exibir voto do Parlamentar antes de encerrar a votação?'),
),
]

23
sapl/base/migrations/0056_auto_20221118_1330.py

@ -0,0 +1,23 @@
# Generated by Django 2.2.28 on 2022-11-18 16:30
import django.contrib.postgres.fields.jsonb
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('base', '0055_appconfig_mostrar_voto'),
]
operations = [
migrations.AlterModelOptions(
name='auditlog',
options={'ordering': ('-id', '-timestamp'), 'verbose_name': 'AuditLog', 'verbose_name_plural': 'AuditLogs'},
),
migrations.AddField(
model_name='auditlog',
name='data',
field=django.contrib.postgres.fields.jsonb.JSONField(null=True, verbose_name='data'),
),
]

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

Loading…
Cancel
Save