Browse Source

Merge branch '3.1.x' into migra-debian-docker

pull/2676/head
Edward 7 years ago
committed by GitHub
parent
commit
7b476c0db6
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .gitignore
  2. 8
      Dockerfile
  3. 1
      MANIFEST.in
  4. 14
      README.rst
  5. 13
      check_solr.sh
  6. 19
      docker-compose.yml
  7. 7
      docs/deploy.rst
  8. 24
      docs/instalacao31.rst
  9. 89
      docs/solr.rst
  10. 6
      release.sh
  11. 5
      requirements/requirements.txt
  12. 230
      sapl/api/deprecated.py
  13. 264
      sapl/api/forms.py
  14. 20
      sapl/api/serializers.py
  15. 14
      sapl/api/urls.py
  16. 210
      sapl/api/views.py
  17. 8
      sapl/audiencia/forms.py
  18. 34
      sapl/audiencia/migrations/0010_auto_20190219_1511.py
  19. 22
      sapl/audiencia/models.py
  20. 5
      sapl/audiencia/urls.py
  21. 5
      sapl/audiencia/views.py
  22. 7
      sapl/base/email_utils.py
  23. 195
      sapl/base/forms.py
  24. 20
      sapl/base/migrations/0030_appconfig_escolher_numero_materia_proposicao.py
  25. 20
      sapl/base/migrations/0030_appconfig_protocolo_manual.py
  26. 20
      sapl/base/migrations/0031_auto_20190218_1109.py
  27. 16
      sapl/base/migrations/0032_merge_20190219_0941.py
  28. 20
      sapl/base/migrations/0033_auto_20190415_1050.py
  29. 29
      sapl/base/migrations/0034_auto_20190417_0941.py
  30. 20
      sapl/base/migrations/0035_auto_20190417_1009.py
  31. 20
      sapl/base/migrations/0036_auto_20190417_1432.py
  32. 24
      sapl/base/models.py
  33. 29
      sapl/base/search_indexes.py
  34. 13
      sapl/base/templatetags/common_tags.py
  35. 2
      sapl/base/tests/test_login.py
  36. 58
      sapl/base/urls.py
  37. 629
      sapl/base/views.py
  38. 5
      sapl/comissoes/forms.py
  39. 3
      sapl/comissoes/urls.py
  40. 18
      sapl/comissoes/views.py
  41. 56
      sapl/compilacao/forms.py
  42. 20
      sapl/compilacao/migrations/0011_tipotextoarticulado_rodape_global.py
  43. 27
      sapl/compilacao/migrations/0012_bug_auto_inserido.py
  44. 7
      sapl/compilacao/models.py
  45. 45
      sapl/compilacao/views.py
  46. 37
      sapl/crispy_layout_mixin.py
  47. 16
      sapl/crud/base.py
  48. 4
      sapl/legacy/test_renames.py
  49. 315
      sapl/lexml/OAIServer.py
  50. 6
      sapl/lexml/models.py
  51. 5
      sapl/lexml/urls.py
  52. 33
      sapl/lexml/views.py
  53. 457
      sapl/materia/forms.py
  54. 20
      sapl/materia/migrations/0041_proposicao_numero_materia_futuro.py
  55. 21
      sapl/materia/migrations/0042_tipomaterialegislativa_sequencia_regimental.py
  56. 19
      sapl/materia/migrations/0043_auto_20190320_1749.py
  57. 22
      sapl/materia/migrations/0044_auto_20190327_1409.py
  58. 20
      sapl/materia/migrations/0045_auto_20190415_1050.py
  59. 20
      sapl/materia/migrations/0046_auto_20190417_0941.py
  60. 28
      sapl/materia/migrations/0046_auto_20190417_1212.py
  61. 20
      sapl/materia/migrations/0047_auto_20190417_1432.py
  62. 16
      sapl/materia/migrations/0048_merge_20190426_0828.py
  63. 67
      sapl/materia/models.py
  64. 235
      sapl/materia/tests/test_materia.py
  65. 13
      sapl/materia/tests/test_materia_form.py
  66. 9
      sapl/materia/urls.py
  67. 511
      sapl/materia/views.py
  68. 45
      sapl/norma/forms.py
  69. 19
      sapl/norma/migrations/0023_auto_20190219_1535.py
  70. 19
      sapl/norma/migrations/0024_auto_20190425_0917.py
  71. 24
      sapl/norma/models.py
  72. 31
      sapl/norma/views.py
  73. 160
      sapl/parlamentares/forms.py
  74. 37
      sapl/parlamentares/migrations/0026_bloco.py
  75. 19
      sapl/parlamentares/migrations/0027_auto_20190430_0839.py
  76. 34
      sapl/parlamentares/models.py
  77. 15
      sapl/parlamentares/tests/test_parlamentares.py
  78. 15
      sapl/parlamentares/urls.py
  79. 160
      sapl/parlamentares/views.py
  80. 335
      sapl/protocoloadm/forms.py
  81. 35
      sapl/protocoloadm/migrations/0018_auto_20190314_1532.py
  82. 28
      sapl/protocoloadm/migrations/0019_auto_20190426_0833.py
  83. 25
      sapl/protocoloadm/migrations/0019_auto_20190429_0828.py
  84. 21
      sapl/protocoloadm/migrations/0020_tramitacaoadministrativo_timestamp.py
  85. 16
      sapl/protocoloadm/migrations/0021_merge_20190429_1531.py
  86. 61
      sapl/protocoloadm/models.py
  87. 295
      sapl/protocoloadm/tests/test_protocoloadm.py
  88. 7
      sapl/protocoloadm/urls.py
  89. 286
      sapl/protocoloadm/views.py
  90. 8
      sapl/relatorios/templates/pdf_etiqueta_protocolo_gerar.py
  91. 146
      sapl/relatorios/templates/pdf_sessao_plenaria_gerar.py
  92. 5
      sapl/relatorios/urls.py
  93. 180
      sapl/relatorios/views.py
  94. 7
      sapl/rules/map_rules.py
  95. 324
      sapl/sessao/forms.py
  96. 2
      sapl/sessao/migrations/0028_auto_20181031_0902.py
  97. 25
      sapl/sessao/migrations/0033_auto_20190228_1803.py
  98. 34
      sapl/sessao/migrations/0034_oradorordemdia.py
  99. 20
      sapl/sessao/migrations/0035_resumoordenacao_decimo_quarto.py
  100. 21
      sapl/sessao/migrations/0036_auto_20190412_1106.py

2
.gitignore

@ -54,7 +54,7 @@ coverage.xml
# Django stuff:
*.log
sapl.log.*
*.swp
# Sphinx documentation

8
Dockerfile

@ -4,8 +4,8 @@ FROM python:3.7-slim
ENV BUILD_PACKAGES apt-file libpq-dev graphviz-dev graphviz build-essential git pkg-config \
python3-dev libxml2-dev libjpeg-dev libssl-dev libffi-dev libxslt1-dev pgadmin3 \
python3-lxml python3-magic postgresql-contrib postgresql-client \
python3-psycopg2 poppler-utils antiword curl jq vim openssh-client bash \
software-properties-common python3-setuptools python3-venv nginx tzdata nodejs
python3-psycopg2 poppler-utils vim curl jq vim openssh-client bash \
software-properties-common python3-setuptools python3-venv nginx tzdata nodejs \
ENV DEBIAN_FRONTEND noninteractive
@ -76,9 +76,11 @@ 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 && \
ln -sf /dev/stdout /var/log/nginx/access.log && \
ln -sf /dev/stderr /var/log/nginx/error.log && \
mkdir /var/log/sapl/
mkdir /var/log/sapl/ && touch /var/interlegis/sapl/sapl.log && \
ln -s /var/interlegis/sapl/sapl.log /var/log/sapl/sapl.log
# Debian não possui usuário 'nginx' necessário para o Debian
RUN useradd --no-create-home nginx

1
MANIFEST.in

@ -1,4 +1,5 @@
include README.rst LICENSE.txt
include sapl/webpack-stats.json
recursive-include sapl *.html *.yaml
recursive-include sapl/static *
recursive-include sapl/relatorios/templates *.py

14
README.rst

@ -1,4 +1,4 @@
.. image:: https://travis-ci.org/interlegis/sapl.svg?branch=master
.. image:: https://travis-ci.org/interlegis/sapl.svg?branch=3.1.x
:target: https://travis-ci.org/interlegis/sapl
@ -17,17 +17,17 @@ atual do sistema (2.5), visite a página do `projeto na Interlegis wiki <https:/
Instalação do Ambiente de Desenvolvimento
=========================================
`Instalação do Ambiente de Desenvolvimento <https://github.com/interlegis/sapl/blob/master/docs/instalacao31.rst>`_
`Instalação do Ambiente de Desenvolvimento <https://github.com/interlegis/sapl/blob/3.1.x/docs/instalacao31.rst>`_
Instalação do Solr
======================
`Instalação e configuração do Solr <https://github.com/interlegis/sapl/blob/master/docs/solr.rst>`_
`Instalação e configuração do Solr <https://github.com/interlegis/sapl/blob/3.1.x/docs/solr.rst>`_
Instruções para Deploy
======================
`Deploy SAPL com Nginx + Gunicorn <https://github.com/interlegis/sapl/blob/master/docs/deploy.rst>`_
`Deploy SAPL com Nginx + Gunicorn <https://github.com/interlegis/sapl/blob/3.1.x/docs/deploy.rst>`_
Instruções para Importação da base mysql 2.5
@ -37,19 +37,19 @@ Instruções para Importação da base mysql 2.5
Instruções para Tradução
========================
`Instruções para Tradução <https://github.com/interlegis/sapl/blob/master/docs/traducao.rst>`_
`Instruções para Tradução <https://github.com/interlegis/sapl/blob/3.1.x/docs/traducao.rst>`_
Orientações gerais de implementação
===================================
`Instruções para Implementação <https://github.com/interlegis/sapl/blob/master/docs/implementacoes.rst>`_
`Instruções para Implementação <https://github.com/interlegis/sapl/blob/3.1.x/docs/implementacoes.rst>`_
Orientações gerais sobre o GitHub
===================================
`Instruções para GitHub <https://github.com/interlegis/sapl/blob/master/docs/howtogit.rst>`_
`Instruções para GitHub <https://github.com/interlegis/sapl/blob/3.1.x/docs/howtogit.rst>`_

13
check_solr.sh

@ -4,15 +4,22 @@
SOLR_URL=$1
RETRY_COUNT=1
RETRY_LIMIT=4
echo "Waiting for solr connection at $SOLR_URL ..."
while true; do
while [[ $RETRY_COUNT < $RETRY_LIMIT ]]; do
echo "Attempt to connect to solr: $RETRY_COUNT of $RETRY_LIMIT"
let RETRY_COUNT=RETRY_COUNT+1;
echo "$SOLR_URL/solr/admin/collections?action=LIST"
RESULT=$(curl -s -o /dev/null -I "$SOLR_URL/solr/admin/collections?action=LIST" -w '%{http_code}')
echo $RESULT
if [ "$RESULT" -eq '200' ]; then
if [ $RESULT == 200 ]; then
echo "Solr server is up!"
break
exit 1
else
sleep 3
fi
done
echo "Solr connection failed."
exit 2

19
docker-compose.yml

@ -11,8 +11,7 @@ sapldb:
ports:
- "5433:5432"
sapl:
# image: interlegis/sapl:3.1.143
build: .
image: interlegis/sapl:3.1.155
restart: always
environment:
ADMIN_PASSWORD: interlegis
@ -24,11 +23,27 @@ sapl:
EMAIL_HOST_USER: usuariosmtp
EMAIL_SEND_USER: usuariosmtp
EMAIL_HOST_PASSWORD: senhasmtp
# USE_SOLR: 'True'
# SOLR_COLLECTION: sapl
# SOLR_URL: http://saplsolr:8983
TZ: America/Sao_Paulo
volumes:
- sapl_data:/var/interlegis/sapl/data
- sapl_media:/var/interlegis/sapl/media
links:
- sapldb
# - saplsolr
ports:
- "80:80"
#saplsolr:
# image: solr:7.4-alpine
# restart: always
# command: bin/solr start -c -f
# volumes:
# - solr_data:/opt/solr/server/solr
# - solr_configsets:/opt/solr/server/solr/configsets
# ports:
# - "8983:8983"

7
docs/deploy.rst

@ -24,11 +24,8 @@ Entrar no ambiente virtual::
Arquivos Estáticos
------------------
Com o ambiente em produção, os arquivos estáticos devem ser servidos pelo web service, em nosso caso o `NGINX`, logo para ter acesso aos arquivos primeiro devemos rodar o seguinte comando::
./manage.py compilescss
para que os arquivos SASS/SCSS sejam compilados em arquivos .css em ambiente de produção, e em seguida rode::
Com o ambiente em produção, os arquivos estáticos devem ser servidos pelo web service, em nosso caso o `NGINX`,
em ambiente de produção, para tanto, rode::
./manage.py collectstatic --no-input --clear

24
docs/instalacao31.rst

@ -39,14 +39,14 @@ Instalar o virtualenv usando python 3 para o projeto.
sudo mkdir -p /var/interlegis/.virtualenvs
* Ajustar as permissões - onde ``sapl31`` trocar por usuario::
* Ajustar as permissões::
sudo chown -R sapl31:sapl31 /var/interlegis/
sudo chown -R $USER:$USER /var/interlegis/
* Edite o arquivo ``.bashrc`` e adicione ao seu final as configurações abaixo para o virtualenvwrapper::
nano /home/sapl31/.bashrc
nano /home/$USER/.bashrc
export VIRTUALENVWRAPPER_PYTHON=/usr/bin/python3
export WORKON_HOME=/var/interlegis/.virtualenvs
@ -56,7 +56,7 @@ Instalar o virtualenv usando python 3 para o projeto.
* Carregue as configurações do virtualenvwrapper::
source /home/sapl31/.bashrc
source /home/$USER/.bashrc
@ -116,7 +116,7 @@ Instalação e configuração das dependências do projeto
* (caso você já possua uma instalação do postrgresql anterior ao processo de instalação do ambiente de desenvolvimento do SAPL em sua máquina e sábia como fazer, esteja livre para proceder como desejar, porém, ao configurar o arquivo ``.env`` no próximo passo, as mesmas definições deverão ser usadas)
* **Ajustar as permissões - onde $USER trocar por usuario**::
* **Ajustar as permissões - onde $USER trocar por usuário**::
eval $(echo "sudo chown -R $USER:$USER /var/interlegis/")
@ -225,6 +225,7 @@ Preparação do ambiente::
* **Instalação do NodeJs LTS 10.15.x**::
sudo apt-get install curl
curl -sL https://deb.nodesource.com/setup_10.x | sudo -E bash -
sudo apt-get install -y nodejs
@ -281,4 +282,15 @@ Feito isso, e você ativando a variável de ambiente FRONTEND_CUSTOM=True (vide
**Deste ponto em diante, é exigido o conhecimento que você pode adquirir em https://cli.vuejs.org/guide/ e em https://vuejs.org/v2/guide/ para colaborar com sapl-frontend**
**OBS: após a separação do sapl para o sapl-frontend, o conteúdo da pasta static é compilado e minificado. É gerado pelo build do sapl-frontend e não deve-se tentar customizar ou criar elementos manipulando diretamente informações na pasta static.**
Sobre a pasta static
--------------------
Após a separação do sapl em sapl e sapl-frontend, o conteúdo da pasta sapl/static/sapl/frontend é compilado e minificado. É gerado pelo build do sapl-frontend e não deve-se tentar customizar ou criar elementos manipulando diretamente informações na pasta sapl/static/sapl/frontend.
Para aplicar css e javascript sem sapl-frontend:
1) Não altere diretamente o conteúdo da pasta sapl/static/sapl/frontend. Isso deve ser feito no projeto sapl-frontend. Você perderá qualquer manipulação dentro desta pasta.
2) Caso venha a criar algum código css/js diretamente no django, crie seus arquivos na pasta sapl/static/sapl.
3) Não crie nenhum novo conteúdo na pasta sapl/static. Projetos Django podem ser usados como app de outro projeto. É o que ocorre com o Sapl, que é usado como uma app em outros projetos. Qualquer conteúdo colocado dentro sapl/static e não em sapl/static/sapl, pode estar causando erro no uso do Sapl como app em outro projeto.

89
docs/solr.rst

@ -2,24 +2,87 @@
Instruções para instalar o Solr
================================
Solr é a ferramenta utilizada pelo SAPL 3.1 para indexar documentos para que possa ser feita
a Pesquisa Textual.
**O servidor do Solr NÃO DEVE SER EXPOSTO NA INTERNET. Assim como o servidor de bancos de dados Postgres ele deve estar acessível pelo SAPL na rede interna (atrás de NATs/firewalls/proxies/etc).**
Adicione ao arquivo ``.env`` o seguinte atributo:
Solr é uma plataforma open source de indexação e busca textual utilizada pelo SAPL 3.1 para indexar documentos (normas jurídicas, matérias legislativas e documentos acessórios).
``SOLR_URL = 'http://127.0.0.1:8983/solr'``
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);
Dentro do diretório principal siga os seguintes passos::
1) Faça o download da distribuição *binária* do Apache Solr do site oficial do projeto **http://lucene.apache.org/solr**
curl -LO https://archive.apache.org/dist/lucene/solr/4.10.2/solr-4.10.2.tgz
tar xvzf solr-4.10.2.tgz
cd solr-4.10.2
cd example
java -jar start.jar
./manage.py build_solr_schema --filename solr-4.10.2/example/solr/collection1/conf/schema.xml
As instalações Solr suportadas até o momento vão da 7.4 à 8;
Após isso, deve-se parar o servidor do Solr e restartar com ``java -jar start.jar``
2) Descompacte o arquivo em uma pasta do diretório (referenciada neste tutorial como $SOLR_HOME)
**OBS: Toda vez que o código da pesquisa textual for modificado, os comandos de build_solr_schema e start.jar devem ser rodados, nessa mesma ordem.**
3) Inicie o Solr com o comando:
**$SOLR_HOME/bin/solr start -c**
4) Por meio do browser, acesse a URL **http://localhost:8983** (ou informe o endereço da máquina onde o Solr foi instalado)
5) Pare o servidor do SAPL;
6) Edite o arquivo .env adicionando as seguintes linhas:
USE_SOLR = True
SOLR_COLLECTION = sapl
SOLR_URL = http://localhost:8983
(o valor do campo SOLR_URL deve corresponder à URL acessada no item 3)
7) Entre no diretório raiz do SAPL e digite o comando: **python3 solr_api.py -c sapl -u http://localhost:8983`**
(a URL informada acima deve ser a mesma dos itens 3 e 6)
8) Enquanto o Solr realiza a indexação da base de dados do SAPL, inicie em uma outra tela o SAPL;
9) Após realizados os passos com sucesso, nas telas de busca de Matéria Legislativa e Normas deverá aparecer um botão
de 'Pesquisa Textual' na tela de busca tradicional.
**Observações:**
* Para parar o Solr execute o comando **$SOLR_HOME/bin/solr stop**
* Comandos de manutenção da base textual do Solr:
1. **python3 manage.py rebuild_index** : Apaga os dados da coleção `sapl` no Solr e reindexa tudo do início;
2. **python3 manage.py clear_index** : Apaga todos os dados da coleção `sapl` do Solr. **Este comando não irá apagar os dados do BD Postgres, somente os dados do Solr serão apagados.**
3. **python3 manage.py update_index** : atualiza os dados do Solr:
3.1. **python3 manage.py update_index --remove** : remove objetos do Solr que não mais existem no BD Postgres (no caso do Postgres e Solr derem dessincronizados).
3.2. **python3 manage.py update_index --age <N>** : reindexa os documentos inseridos/alterados nas últimas <N> horas;
3.3. **python3 manage.py update_index -s YYYY-MM-DDTHH:MM:SS -e YYYY-MM-DDTHH:MM:SS** : reindexa os documentos que foram inseridos/atualizados entre a data inicial (-s) e a data final (-e). Ambos os argumentos de início e fim são opcionais.
### FAQ
1. Uma dúvida quanto a indexação do Solr, pelo que entendi de tempos e tempos tenho que rodar o comando para poder indexar novos arquivos certo?
Errado. Cada novo documento inserido, atualizado, ou removido do SAPL dispara uma nova indexação somente daquele documento no Solr automaticamente.
2. O comando **python3 solr_api.py -c sapl -u http://localhost:8983** indexa os novos arquivos?
Não. Este comando é para construir a coleção do Solr a primeira vez e, por acaso, faz a indexação inicial. Não deve ser usado se a coleção já foi criada.
3. Ou teria que reindexar do zero com *rebuild_index*?
Pode acontecer do Postgres e o Solr se dessincronizarem (ex: o Solr ficou fora do ar por um dia e foram inseridos registros no SAPL). Ou por algum motivo se deseja refazer o índice do Solr. Neste caso pode-se refazer a indexação no Solr com o comando : **python3 manage.py rebuild_index** (direto na linha de comando, a partir da pasta raiz do SAPL). Mas existem maneiras de atualizar somente os documentos inseridos/alterados a partir de uma determinada data ao invés de atualizar tudo do zero de novo.
4. Pergunto isso pois estou querendo criar um script para crontab para indexar esses novos arquivos
Desnecessário.

6
release.sh

@ -14,14 +14,16 @@ function bump_version {
sed -e s/$VERSION/$NEXT_VERSION/g setup.py > tmp2
mv tmp2 setup.py
sed -e s/$VERSION/$NEXT_VERSION/g sapl/templates/base.html > tmp3
mv tmp3 sapl/templates/base.html
sed -e s/$VERSION/$NEXT_VERSION/g sapl/settings.py > tmp4
mv tmp4 sapl/settings.py
}
function commit_and_push {
echo "committing..."
git add docker-compose.yml setup.py sapl/templates/base.html
git add docker-compose.yml setup.py sapl/settings.py sapl/templates/base.html
git commit -m "Release: $NEXT_VERSION"
git tag $NEXT_VERSION

5
requirements/requirements.txt

@ -24,11 +24,12 @@ rtyaml==0.0.5
python-magic==0.4.15
unipath==1.1
WeasyPrint==44
Pillow==5.1.0
gunicorn==19.9.0
textract==1.5.0
pysolr==3.6.0
whoosh==2.7.4
pyoai==2.5.0
git+git://github.com/interlegis/trml2pdf.git
git+git://github.com/interlegis/django-admin-bootstrapped

230
sapl/api/deprecated.py

@ -1,11 +1,20 @@
import logging
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.utils.translation import ugettext_lazy as _
from django_filters.filters import CharFilter, ModelChoiceFilter, DateFilter
from django_filters.rest_framework.backends import DjangoFilterBackend
from django_filters.rest_framework.filterset import FilterSet
from rest_framework import serializers
from rest_framework import serializers
from rest_framework.generics import ListAPIView
from rest_framework.mixins import ListModelMixin, RetrieveModelMixin
@ -13,14 +22,231 @@ from rest_framework.permissions import (IsAuthenticated,
IsAuthenticatedOrReadOnly, AllowAny)
from rest_framework.viewsets import GenericViewSet
from sapl.api.forms import (AutorChoiceFilterSet, AutoresPossiveisFilterSet,
AutorSearchForFieldFilterSet)
from sapl.api.serializers import ModelChoiceSerializer, AutorSerializer,\
ChoiceSerializer
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)
return queryset.filter(q)
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):

264
sapl/api/forms.py

@ -1,231 +1,65 @@
import logging
from django.db.models import Q
from django.forms.fields import CharField, MultiValueField
from django.forms.widgets import MultiWidget, TextInput
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from django_filters.filters import CharFilter, ModelChoiceFilter, DateFilter
from django.db.models.fields.files import FileField
from django.template.defaultfilters import capfirst
import django_filters
from django_filters.filters import CharFilter, NumberFilter
from django_filters.rest_framework.filterset import FilterSet
from rest_framework import serializers
from sapl.base.models import Autor, TipoAutor
from sapl.parlamentares.models import Legislatura
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):
from django_filters.utils import resolve_field
from sapl.sessao.models import SessaoPlenaria
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 SaplFilterSetMixin(FilterSet):
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())
o = CharFilter(method='filter_o')
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):
fields = '__all__'
filter_overrides = {
FileField: {
'filter_class': django_filters.CharFilter,
'extra': lambda f: {
'lookup_expr': 'exact',
},
},
}
def filter_o(self, queryset, name, value):
try:
self.logger.debug(
"Tentando obter TipoAutor correspondente à pk {}.".format(value))
tipo = TipoAutor.objects.get(pk=value)
return queryset.order_by(
*map(str.strip, value.split(',')))
except:
self.logger.error("TipoAutor(pk={}) inexistente.".format(value))
raise serializers.ValidationError(_('Tipo de Autor inexistente.'))
qs = queryset.filter(tipo=tipo)
return qs
return queryset
@property
def qs(self):
qs = super().qs
@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
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
class SessaoPlenariaFilterSet(SaplFilterSetMixin):
year = NumberFilter(method='filter_year')
month = NumberFilter(method='filter_month')
if not tipo:
return qs
class Meta(SaplFilterSetMixin.Meta):
model = SessaoPlenaria
tipo = TipoAutor.objects.get(pk=tipo)
if not tipo.content_type:
def filter_year(self, queryset, name, value):
qs = queryset.filter(data_inicio__year=value)
return qs
filter_for_model = 'filter_%s' % tipo.content_type.model
if not hasattr(self, filter_for_model):
def filter_month(self, queryset, name, value):
qs = queryset.filter(data_inicio__month=value)
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)
return queryset.filter(q)
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

20
sapl/api/serializers.py

@ -1,6 +1,13 @@
from django.conf import settings
from rest_framework import serializers
from rest_framework.relations import StringRelatedField
from sapl.base.models import Autor
from sapl.base.models import Autor, CasaLegislativa
class IntRelatedField(StringRelatedField):
def to_representation(self, value):
return int(value)
class ChoiceSerializer(serializers.Serializer):
@ -38,3 +45,14 @@ class AutorSerializer(serializers.ModelSerializer):
class Meta:
model = Autor
fields = '__all__'
class CasaLegislativaSerializer(serializers.ModelSerializer):
version = serializers.SerializerMethodField()
def get_version(self, obj):
return settings.SAPL_VERSION
class Meta:
model = CasaLegislativa
fields = '__all__'

14
sapl/api/urls.py

@ -8,7 +8,7 @@ from rest_framework.routers import DefaultRouter
from sapl.api.deprecated import MateriaLegislativaViewSet, SessaoPlenariaViewSet,\
AutoresProvaveisListView, AutoresPossiveisListView, AutorListView,\
ModelChoiceView
from sapl.api.views import SaplSetViews
from sapl.api.views import SaplApiViewSetConstrutor
from .apps import AppConfig
@ -21,9 +21,10 @@ router.register(r'materia$', MateriaLegislativaViewSet)
router.register(r'sessao-plenaria', SessaoPlenariaViewSet)
for app, built_sets in SaplSetViews.items():
for app, built_sets in SaplApiViewSetConstrutor._built_sets.items():
for view_prefix, viewset in built_sets.items():
router.register(app + '/' + view_prefix, viewset)
router.register(app.label + '/' +
view_prefix._meta.model_name, viewset)
urlpatterns_router = router.urls
@ -40,7 +41,7 @@ schema_view = get_schema_view(
permission_classes=(permissions.AllowAny,),
)
urlpatterns_api = [
urlpatterns_api_doc = [
url(r'^docs/swagger(?P<format>\.json|\.yaml)$',
schema_view.without_ui(cache_timeout=0), name='schema-json'),
url(r'^docs/swagger/$',
@ -60,13 +61,16 @@ deprecated_urlpatterns_api = [
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)),
url(r'^api/', include(urlpatterns_api_doc)),
url(r'^api/', include(urlpatterns_router)),
# 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')),

210
sapl/api/views.py

@ -9,6 +9,7 @@ from django.utils.decorators import classonlymethod
from django.utils.text import capfirst
from django.utils.translation import ugettext_lazy as _
import django_filters
from django_filters.filters import CharFilter
from django_filters.rest_framework.backends import DjangoFilterBackend
from django_filters.rest_framework.filterset import FilterSet
from django_filters.utils import resolve_field
@ -17,17 +18,42 @@ from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from sapl.api.forms import SaplFilterSetMixin
from sapl.api.permissions import SaplModelPermissions
from sapl.api.serializers import ChoiceSerializer
from sapl.base.models import Autor, AppConfig, DOC_ADM_OSTENSIVO
from sapl.materia.models import Proposicao
from sapl.materia.models import Proposicao, TipoMateriaLegislativa,\
MateriaLegislativa, Tramitacao
from sapl.parlamentares.models import Parlamentar
from sapl.utils import models_with_gr_for_model
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
class SaplApiViewSetConstrutor(ModelViewSet):
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 SaplApiViewSet(ModelViewSet):
filter_backends = (DjangoFilterBackend,)
class SaplApiViewSetConstrutor():
_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
@ -73,40 +99,12 @@ class SaplApiViewSetConstrutor(ModelViewSet):
# Define uma classe padrão para filtro caso não tenha sido
# criada a classe sapl.api.forms.{model}FilterSet
class SaplFilterSet(FilterSet):
class Meta:
class SaplFilterSet(SaplFilterSetMixin):
class Meta(SaplFilterSetMixin.Meta):
model = _model
fields = '__all__'
filter_overrides = {
FileField: {
'filter_class': django_filters.CharFilter,
'extra': lambda f: {
'lookup_expr': 'exact',
},
},
}
@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
# Define uma classe padrão ModelViewSet de DRF
class ModelSaplViewSet(cls):
class ModelSaplViewSet(SaplApiViewSet):
queryset = _model.objects.all()
# Utiliza o filtro customizado pela classe
@ -130,12 +128,12 @@ class SaplApiViewSetConstrutor(ModelViewSet):
apps_sapl = [apps.apps.get_app_config(
n[5:]) for n in settings.SAPL_APPS]
for app in apps_sapl:
built_sets[app.label] = {}
cls._built_sets[app] = {}
for model in app.get_models():
built_sets[app.label][model._meta.model_name] = build(model)
cls._built_sets[app][model] = build(model)
return built_sets
SaplApiViewSetConstrutor.build_class()
"""
1. Constroi uma rest_framework.viewsets.ModelViewSet para
@ -198,15 +196,39 @@ class SaplApiViewSetConstrutor(ModelViewSet):
}
"""
SaplSetViews = SaplApiViewSetConstrutor.build_class()
# 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 para recuperar e transformar o default
class customize(object):
def __init__(self, model):
self.model = model
def __call__(self, cls):
class _SaplApiViewSet(
cls,
SaplApiViewSetConstrutor._built_sets[
self.model._meta.app_config][self.model]
):
pass
if hasattr(_SaplApiViewSet, 'build'):
_SaplApiViewSet = _SaplApiViewSet.build()
SaplApiViewSetConstrutor._built_sets[
self.model._meta.app_config][self.model] = _SaplApiViewSet
return _SaplApiViewSet
# Customização para AutorViewSet com implementação de actions específicas
class _AutorViewSet(SaplSetViews['base']['autor']):
@customize(Autor)
class _AutorViewSet:
"""
Neste exemplo de customização do que foi criado em
SaplApiViewSetConstrutor além do ofertado por
@ -251,7 +273,7 @@ class _AutorViewSet(SaplSetViews['base']['autor']):
return Response(serializer.data)
@classonlymethod
def build_class_with_actions(cls):
def build(cls):
models_with_gr_for_autor = models_with_gr_for_model(Autor)
@ -274,7 +296,8 @@ class _AutorViewSet(SaplSetViews['base']['autor']):
return cls
class _ParlamentarViewSet(SaplSetViews['parlamentares']['parlamentar']):
@customize(Parlamentar)
class _ParlamentarViewSet:
@action(detail=True)
def proposicoes(self, request, *args, **kwargs):
"""
@ -299,15 +322,16 @@ class _ParlamentarViewSet(SaplSetViews['parlamentares']['parlamentar']):
page = self.paginate_queryset(qs)
if page is not None:
serializer = SaplSetViews[
'materia']['proposicao'].serializer_class(page, many=True)
serializer = SaplApiViewSetConstrutor.get_class_for_model(
Proposicao).serializer_class(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(page, many=True)
return Response(serializer.data)
class _ProposicaoViewSet(SaplSetViews['materia']['proposicao']):
@customize(Proposicao)
class _ProposicaoViewSet():
"""
list:
Retorna lista de Proposições
@ -360,7 +384,49 @@ class _ProposicaoViewSet(SaplSetViews['materia']['proposicao']):
return qs
class _DocumentoAdministrativoViewSet(SaplSetViews['protocoloadm']['documentoadministrativo']):
@customize(MateriaLegislativa)
class _MateriaLegislativaViewSet:
@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.last()
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):
@ -394,8 +460,8 @@ class _DocumentoAdministrativoViewSet(SaplSetViews['protocoloadm']['documentoadm
return qs
class _DocumentoAcessorioAdministrativoViewSet(
SaplSetViews['protocoloadm']['documentoacessorioadministrativo']):
@customize(DocumentoAcessorioAdministrativo)
class _DocumentoAcessorioAdministrativoViewSet:
permission_classes = (
_DocumentoAdministrativoViewSet.DocumentoAdministrativoPermission, )
@ -408,8 +474,8 @@ class _DocumentoAcessorioAdministrativoViewSet(
return qs
class _TramitacaoAdministrativoViewSet(
SaplSetViews['protocoloadm']['tramitacaoadministrativo']):
@customize(TramitacaoAdministrativo)
class _TramitacaoAdministrativoViewSet(BusinessRulesNotImplementedMixin):
# TODO: Implementar regras de manutenção das tramitações de docs adms
permission_classes = (
@ -422,25 +488,41 @@ class _TramitacaoAdministrativoViewSet(
qs = qs.exclude(documento__restrito=True)
return qs
def create(self, request, *args, **kwargs):
raise Exception(_("POST Create não implementado"))
def put(self, request, *args, **kwargs):
raise Exception(_("PUT Update não implementado"))
@customize(Anexado)
class _AnexadoViewSet(BusinessRulesNotImplementedMixin):
def patch(self, request, *args, **kwargs):
raise Exception(_("PATCH Partial Update não implementado"))
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
def delete(self, request, *args, **kwargs):
raise Exception(_("DELETE Delete não implementado"))
@customize(SessaoPlenaria)
class _SessaoPlenariaViewSet:
SaplSetViews['base']['autor'] = _AutorViewSet.build_class_with_actions()
@action(detail=False)
def years(self, request, *args, **kwargs):
years = choice_anos_com_sessaoplenaria()
SaplSetViews['materia']['proposicao'] = _ProposicaoViewSet
serializer = ChoiceSerializer(years, many=True)
return Response(serializer.data)
@action(detail=True)
def expedientes(self, request, *args, **kwargs):
sessao = self.get_object()
SaplSetViews['parlamentares']['parlamentar'] = _ParlamentarViewSet
page = self.paginate_queryset(sessao.expedientesessao_set.all())
if page is not None:
serializer = SaplApiViewSetConstrutor.get_class_for_model(
ExpedienteSessao).serializer_class(page, many=True)
return self.get_paginated_response(serializer.data)
SaplSetViews['protocoloadm']['documentoadministrativo'] = _DocumentoAdministrativoViewSet
SaplSetViews['protocoloadm']['documentoacessorioadministrativo'] = _DocumentoAcessorioAdministrativoViewSet
SaplSetViews['protocoloadm']['tramitacaoadministrativo'] = _TramitacaoAdministrativoViewSet
serializer = self.get_serializer(page, many=True)
return Response(serializer.data)

8
sapl/audiencia/forms.py

@ -7,12 +7,12 @@ from django.utils.translation import ugettext_lazy as _
from sapl.audiencia.models import AudienciaPublica, TipoAudienciaPublica, AnexoAudienciaPublica
from crispy_forms.layout import HTML, Button, Column, Fieldset, Layout
from crispy_forms.helper import FormHelper
from sapl.crispy_layout_mixin import SaplFormHelper
from sapl.crispy_layout_mixin import SaplFormLayout, form_actions, to_row
from sapl.materia.models import MateriaLegislativa, TipoMateriaLegislativa
from sapl.utils import timezone
from sapl.utils import timezone, FileFieldCheckMixin
class AudienciaForm(forms.ModelForm):
class AudienciaForm(FileFieldCheckMixin, forms.ModelForm):
logger = logging.getLogger(__name__)
data_atual = timezone.now()
@ -134,7 +134,7 @@ class AnexoAudienciaPublicaForm(forms.ModelForm):
row2 = to_row(
[('assunto', 12)])
self.helper = FormHelper()
self.helper = SaplFormHelper()
self.helper.layout = SaplFormLayout(
Fieldset(_('Identificação Básica'),
row1, row2))

34
sapl/audiencia/migrations/0010_auto_20190219_1511.py

@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-02-19 18:11
from __future__ import unicode_literals
from django.db import migrations, models
import django.utils.timezone
import sapl.utils
class Migration(migrations.Migration):
dependencies = [
('audiencia', '0009_remove_anexoaudienciapublica_indexacao'),
]
operations = [
migrations.AlterField(
model_name='anexoaudienciapublica',
name='arquivo',
field=models.FileField(default='Assunto não existente.', upload_to=sapl.utils.texto_upload_path, verbose_name='Arquivo'),
preserve_default=False,
),
migrations.AlterField(
model_name='anexoaudienciapublica',
name='assunto',
field=models.TextField(verbose_name='Assunto'),
),
migrations.AlterField(
model_name='anexoaudienciapublica',
name='data',
field=models.DateField(auto_now=True, default=django.utils.timezone.now),
preserve_default=False,
),
]

22
sapl/audiencia/models.py

@ -155,13 +155,12 @@ class AnexoAudienciaPublica(models.Model):
audiencia = models.ForeignKey(AudienciaPublica,
on_delete=models.PROTECT)
arquivo = models.FileField(
blank=True,
null=True,
upload_to=texto_upload_path,
verbose_name=_('Arquivo'))
data = models.DateField(auto_now=timezone.now,blank=True, null=True)
data = models.DateField(
auto_now=timezone.now)
assunto = models.TextField(
blank=True, verbose_name=_('Assunto'))
verbose_name=_('Assunto'))
class Meta:
verbose_name = _('Anexo de Documento Acessório')
@ -174,22 +173,19 @@ class AnexoAudienciaPublica(models.Model):
if self.arquivo:
self.arquivo.delete()
return models.Model.delete(
self, using=using, keep_parents=keep_parents)
def save(self, force_insert=False, force_update=False, using=None,
update_fields=None):
return models.Model.delete(self, using=using, keep_parents=keep_parents)
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
if not self.pk and self.arquivo:
arquivo = self.arquivo
self.arquivo = None
models.Model.save(self, force_insert=force_insert,
models.Model.save(
self,
force_insert=force_insert,
force_update=force_update,
using=using,
update_fields=update_fields)
self.arquivo = arquivo
return models.Model.save(self, force_insert=force_insert,
force_update=force_update,
using=using,
return models.Model.save(self, force_insert=force_insert, force_update=force_update, using=using,
update_fields=update_fields)

5
sapl/audiencia/urls.py

@ -1,11 +1,10 @@
from django.conf.urls import include, url
from sapl.audiencia.views import (index, AudienciaCrud,AnexoAudienciaPublicaCrud)
from sapl.audiencia.views import (index, AudienciaCrud, AnexoAudienciaPublicaCrud)
from .apps import AppConfig
app_name = AppConfig.name
urlpatterns = [
url(r'^audiencia/', include(AudienciaCrud.get_urls() +
AnexoAudienciaPublicaCrud.get_urls())),
url(r'^audiencia/', include(AudienciaCrud.get_urls() + AnexoAudienciaPublicaCrud.get_urls())),
]

5
sapl/audiencia/views.py

@ -86,6 +86,7 @@ class AnexoAudienciaPublicaCrud(MasterDetailCrud):
model = AnexoAudienciaPublica
parent_field = 'audiencia'
help_topic = 'numeracao_docsacess'
public = [RP_LIST, RP_DETAIL, ]
class BaseMixin(MasterDetailCrud.BaseMixin):
list_field_names = ['assunto']
@ -104,7 +105,5 @@ class AnexoAudienciaPublicaCrud(MasterDetailCrud):
kwargs = {self.crud.parent_field: self.kwargs['pk']}
return qs.filter(**kwargs).order_by('-data', '-id')
class DetailView(AudienciaPublicaMixin,
MasterDetailCrud.DetailView):
class DetailView(AudienciaPublicaMixin, MasterDetailCrud.DetailView):
pass

7
sapl/base/email_utils.py

@ -11,6 +11,7 @@ from sapl.materia.models import AcompanhamentoMateria
from sapl.protocoloadm.models import AcompanhamentoDocumento
from sapl.settings import EMAIL_SEND_USER
from sapl.utils import mail_service_configured
from django.utils.translation import ugettext_lazy as _
def load_email_templates(templates, context={}):
@ -208,8 +209,8 @@ def do_envia_email_tramitacao(base_url, tipo, doc_mat, status, unidade_destino):
# Envia email de tramitacao para usuarios cadastrados
#
if not mail_service_configured():
logger = logging.getLogger(__name__)
if not mail_service_configured():
logger.warning(_('Servidor de email não configurado.'))
return
@ -220,6 +221,10 @@ def do_envia_email_tramitacao(base_url, tipo, doc_mat, status, unidade_destino):
destinatarios = AcompanhamentoDocumento.objects.filter(documento=doc_mat,
confirmado=True)
if not destinatarios:
logger.debug(_('Não existem destinatários cadastrados para essa matéria.'))
return
casa = CasaLegislativa.objects.first()
sender = EMAIL_SEND_USER

195
sapl/base/forms.py

@ -1,7 +1,8 @@
import logging
import os
from crispy_forms.bootstrap import FieldWithButtons, InlineRadios, StrictButton
from crispy_forms.helper import FormHelper
from sapl.crispy_layout_mixin import SaplFormHelper
from crispy_forms.layout import HTML, Button, Div, Field, Fieldset, Layout, Row
from django import forms
from django.conf import settings
@ -28,7 +29,7 @@ from sapl.crispy_layout_mixin import (SaplFormLayout, form_actions, to_column,
from sapl.materia.models import (
MateriaLegislativa, UnidadeTramitacao, StatusTramitacao)
from sapl.norma.models import (NormaJuridica, NormaEstatisticas)
from sapl.parlamentares.models import SessaoLegislativa
from sapl.parlamentares.models import SessaoLegislativa, Partido
from sapl.sessao.models import SessaoPlenaria
from sapl.settings import MAX_IMAGE_UPLOAD_SIZE
from sapl.utils import (RANGE_ANOS, YES_NO_CHOICES,
@ -36,8 +37,7 @@ from sapl.utils import (RANGE_ANOS, YES_NO_CHOICES,
RangeWidgetOverride, autor_label, autor_modal,
models_with_gr_for_model, qs_override_django_filter,
choice_anos_com_normas, choice_anos_com_materias,
FilterOverridesMetaMixin)
FilterOverridesMetaMixin, FileFieldCheckMixin)
from .models import AppConfig, CasaLegislativa
@ -64,19 +64,46 @@ def get_roles():
class UsuarioCreateForm(ModelForm):
logger = logging.getLogger(__name__)
username = forms.CharField(required=True, label="Nome de usuário",
max_length=30)
firstname = forms.CharField(required=True, label="Nome", max_length=30)
lastname = forms.CharField(required=True, label="Sobrenome", max_length=30)
password1 = forms.CharField(required=True, widget=forms.PasswordInput,
label='Senha', max_length=128)
password2 = forms.CharField(required=True, widget=forms.PasswordInput,
label='Confirmar senha', max_length=128)
user_active = forms.ChoiceField(required=False, choices=YES_NO_CHOICES,
label="Usuário ativo?", initial='True')
username = forms.CharField(
required=True,
label="Nome de usuário",
max_length=30
)
firstname = forms.CharField(
required=True,
label="Nome",
max_length=30
)
lastname = forms.CharField(
required=True,
label="Sobrenome",
max_length=30
)
password1 = forms.CharField(
required=True,
widget=forms.PasswordInput,
label='Senha',
min_length=6,
max_length=128
)
password2 = forms.CharField(
required=True,
widget=forms.PasswordInput,
label='Confirmar senha',
min_length=6,
max_length=128
)
user_active = forms.ChoiceField(
required=True,
choices=YES_NO_CHOICES,
label="Usuário ativo?",
initial='True'
)
roles = forms.MultipleChoiceField(
required=True, widget=forms.CheckboxSelectMultiple(), choices=get_roles)
required=True,
widget=forms.CheckboxSelectMultiple(),
choices=get_roles
)
class Meta:
model = get_user_model()
@ -84,7 +111,7 @@ class UsuarioCreateForm(ModelForm):
'password1', 'password2', 'user_active', 'roles']
def clean(self):
super(UsuarioCreateForm, self).clean()
super().clean()
if not self.is_valid():
return self.cleaned_data
@ -99,7 +126,7 @@ class UsuarioCreateForm(ModelForm):
def __init__(self, *args, **kwargs):
super(UsuarioCreateForm, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
row0 = to_row([('username', 12)])
@ -112,16 +139,38 @@ class UsuarioCreateForm(ModelForm):
[('password1', 6),
('password2', 6)])
row4 = to_row([(form_actions(label='Confirmar'), 6)])
self.helper = FormHelper()
self.helper = SaplFormHelper()
self.helper.layout = Layout(
row0,
row1,
row3,
row2,
'roles',
row4)
form_actions(label='Confirmar'))
class UsuarioFilterSet(django_filters.FilterSet):
username = django_filters.CharFilter(
label=_('Nome de Usuário'),
lookup_expr='icontains')
class Meta:
model = User
fields = ['username']
def __init__(self, *args, **kwargs):
super(UsuarioFilterSet, self).__init__(*args, **kwargs)
row0 = to_row([('username', 12)])
self.form.helper = SaplFormHelper()
self.form.helper.form_method = 'GET'
self.form.helper.layout = Layout(
Fieldset(_('Pesquisa de Usuário'),
row0,
form_actions(label='Pesquisar'))
)
class UsuarioEditForm(ModelForm):
@ -154,12 +203,12 @@ class UsuarioEditForm(ModelForm):
row3 = to_row([(form_actions(label='Salvar Alterações'), 6)])
self.helper = FormHelper()
self.helper = SaplFormHelper()
self.helper.layout = Layout(
row1,
row2,
'roles',
row3)
form_actions(label='Salvar Alterações'))
def clean(self):
super(UsuarioEditForm, self).clean()
@ -176,7 +225,7 @@ class UsuarioEditForm(ModelForm):
return data
class SessaoLegislativaForm(ModelForm):
class SessaoLegislativaForm(FileFieldCheckMixin, ModelForm):
logger = logging.getLogger(__name__)
class Meta:
@ -432,7 +481,7 @@ class AutorForm(ModelForm):
controle_acesso = Fieldset(_('Controle de Acesso do Autor'),
*controle_acesso)
self.helper = FormHelper()
self.helper = SaplFormHelper()
self.helper.layout = SaplFormLayout(autor_select, controle_acesso)
super(AutorForm, self).__init__(*args, **kwargs)
@ -697,7 +746,7 @@ class RelatorioAtasFilterSet(django_filters.FilterSet):
row1 = to_row([('data_inicio', 12)])
self.form.helper = FormHelper()
self.form.helper = SaplFormHelper()
self.form.helper.form_method = 'GET'
self.form.helper.layout = Layout(
Fieldset(_('Atas das Sessões Plenárias'),
@ -733,7 +782,7 @@ class RelatorioNormasMesFilterSet(django_filters.FilterSet):
row1 = to_row([('ano', 12)])
self.form.helper = FormHelper()
self.form.helper = SaplFormHelper()
self.form.helper.form_method = 'GET'
self.form.helper.layout = Layout(
Fieldset(_('Normas por mês do ano.'),
@ -762,7 +811,7 @@ class EstatisticasAcessoNormasForm(Form):
row1 = to_row([('ano', 12)])
self.helper = FormHelper()
self.helper = SaplFormHelper()
self.helper.form_method = 'GET'
self.helper.layout = Layout(
Fieldset(_('Normas por acessos nos meses do ano.'),
@ -800,7 +849,7 @@ class RelatorioNormasVigenciaFilterSet(django_filters.FilterSet):
row1 = to_row([('ano', 12)])
row2 = to_row([('vigencia', 12)])
self.form.helper = FormHelper()
self.form.helper = SaplFormHelper()
self.form.helper.form_method = 'GET'
self.form.helper.layout = Layout(
Fieldset(_('Normas por vigência.'),
@ -828,7 +877,7 @@ class RelatorioPresencaSessaoFilterSet(django_filters.FilterSet):
row1 = to_row([('data_inicio', 12)])
self.form.helper = FormHelper()
self.form.helper = SaplFormHelper()
self.form.helper.form_method = 'GET'
self.form.helper.layout = Layout(
Fieldset(_('Presença dos parlamentares nas sessões plenárias'),
@ -859,7 +908,7 @@ class RelatorioHistoricoTramitacaoFilterSet(django_filters.FilterSet):
self.filters['tipo'].label = 'Tipo de Matéria'
self.filters['tramitacao__unidade_tramitacao_local'
].label = _('Unidade Local (Último Local)')
].label = _('Unidade Local')
self.filters['tramitacao__status'].label = _('Status')
row1 = to_row([('tramitacao__data_tramitacao', 12)])
@ -868,7 +917,7 @@ class RelatorioHistoricoTramitacaoFilterSet(django_filters.FilterSet):
('tramitacao__unidade_tramitacao_local', 4),
('tramitacao__status', 4)])
self.form.helper = FormHelper()
self.form.helper = SaplFormHelper()
self.form.helper.form_method = 'GET'
self.form.helper.layout = Layout(
Fieldset(_('Histórico de Tramitação'),
@ -894,6 +943,8 @@ class RelatorioDataFimPrazoTramitacaoFilterSet(django_filters.FilterSet):
*args, **kwargs)
self.filters['tipo'].label = 'Tipo de Matéria'
self.filters['tramitacao__unidade_tramitacao_local'].label = 'Unidade de tramitação local'
self.filters['tramitacao__status'].label = 'Status de tramitação'
row1 = to_row([('tramitacao__data_fim_prazo', 12)])
row2 = to_row(
@ -901,7 +952,7 @@ class RelatorioDataFimPrazoTramitacaoFilterSet(django_filters.FilterSet):
('tramitacao__unidade_tramitacao_local', 4),
('tramitacao__status', 4)])
self.form.helper = FormHelper()
self.form.helper = SaplFormHelper()
self.form.helper.form_method = 'GET'
self.form.helper.layout = Layout(
Fieldset(_('Tramitações por fim de prazo'),
@ -932,7 +983,7 @@ class RelatorioReuniaoFilterSet(django_filters.FilterSet):
('nome', 4),
('tema', 4)])
self.form.helper = FormHelper()
self.form.helper = SaplFormHelper()
self.form.helper.form_method = 'GET'
self.form.helper.layout = Layout(
Fieldset(_('Reunião de Comissão'),
@ -962,7 +1013,7 @@ class RelatorioAudienciaFilterSet(django_filters.FilterSet):
[('tipo', 4),
('nome', 4)])
self.form.helper = FormHelper()
self.form.helper = SaplFormHelper()
self.form.helper.form_method = 'GET'
self.form.helper.layout = Layout(
Fieldset(_('Audiência Pública'),
@ -1006,7 +1057,7 @@ class RelatorioMateriasTramitacaoilterSet(django_filters.FilterSet):
row3 = to_row([('tramitacao__unidade_tramitacao_destino', 12)])
row4 = to_row([('tramitacao__status', 12)])
self.form.helper = FormHelper()
self.form.helper = SaplFormHelper()
self.form.helper.form_method = 'GET'
self.form.helper.layout = Layout(
Fieldset(_('Pesquisa de Matéria em Tramitação'),
@ -1032,7 +1083,7 @@ class RelatorioMateriasPorAnoAutorTipoFilterSet(django_filters.FilterSet):
row1 = to_row(
[('ano', 12)])
self.form.helper = FormHelper()
self.form.helper = SaplFormHelper()
self.form.helper.form_method = 'GET'
self.form.helper.layout = Layout(
Fieldset(_('Pesquisar'),
@ -1074,7 +1125,7 @@ class RelatorioMateriasPorAutorFilterSet(django_filters.FilterSet):
'limpar Autor',
css_class='btn btn-primary btn-sm'), 10)])
self.form.helper = FormHelper()
self.form.helper = SaplFormHelper()
self.form.helper.form_method = 'GET'
self.form.helper.layout = Layout(
Fieldset(_('Pesquisar'),
@ -1086,7 +1137,7 @@ class RelatorioMateriasPorAutorFilterSet(django_filters.FilterSet):
)
class CasaLegislativaForm(ModelForm):
class CasaLegislativaForm(FileFieldCheckMixin, ModelForm):
class Meta:
@ -1116,7 +1167,11 @@ class CasaLegislativaForm(ModelForm):
}
def clean_logotipo(self):
logotipo = self.cleaned_data.get('logotipo', False)
# chama __clean de FileFieldCheckMixin
# por estar em clean de campo
super(CasaLegislativaForm, self)._check()
logotipo = self.cleaned_data.get('logotipo')
if logotipo:
if logotipo.size > MAX_IMAGE_UPLOAD_SIZE:
raise ValidationError("Imagem muito grande. ( > 2MB )")
@ -1148,13 +1203,15 @@ class ConfiguracoesAppForm(ModelForm):
class Meta:
model = AppConfig
fields = ['documentos_administrativos',
'sequencia_numeracao',
'sequencia_numeracao_protocolo',
'sequencia_numeracao_proposicao',
'esfera_federacao',
# 'painel_aberto', # TODO: a ser implementado na versão 3.2
'texto_articulado_proposicao',
'texto_articulado_materia',
'texto_articulado_norma',
'proposicao_incorporacao_obrigatoria',
'protocolo_manual',
'cronometro_discurso',
'cronometro_aparte',
'cronometro_ordem',
@ -1162,7 +1219,8 @@ class ConfiguracoesAppForm(ModelForm):
'mostrar_brasao_painel',
'receber_recibo_proposicao',
'assinatura_ata',
'estatisticas_acesso_normas']
'estatisticas_acesso_normas',
'escolher_numero_materia_proposicao']
def __init__(self, *args, **kwargs):
super(ConfiguracoesAppForm, self).__init__(*args, **kwargs)
@ -1180,7 +1238,7 @@ class ConfiguracoesAppForm(ModelForm):
self.logger.error('Não há casa legislativa relacionada.')
raise ValidationError("Não há casa legislativa relacionada.")
if (not bool(casa.logotipo) and mostrar_brasao_painel):
if not casa.logotipo and mostrar_brasao_painel:
self.logger.error('Não há logitipo configurado para esta '
'CasaLegislativa ({}).'.format(casa))
raise ValidationError("Não há logitipo configurado para esta "
@ -1196,7 +1254,7 @@ class RecuperarSenhaForm(PasswordResetForm):
def __init__(self, *args, **kwargs):
row1 = to_row(
[('email', 12)])
self.helper = FormHelper()
self.helper = SaplFormHelper()
self.helper.layout = Layout(
Fieldset(_('Insira o e-mail cadastrado com a sua conta'),
row1,
@ -1233,7 +1291,7 @@ class NovaSenhaForm(SetPasswordForm):
[('new_password1', 6),
('new_password2', 6)])
self.helper = FormHelper()
self.helper = SaplFormHelper()
self.helper.layout = Layout(
row1,
form_actions(label='Enviar'))
@ -1266,7 +1324,7 @@ class AlterarSenhaForm(Form):
[('new_password1', 6),
('new_password2', 6)])
self.helper = FormHelper()
self.helper = SaplFormHelper()
self.helper.layout = Layout(
row1,
row2,
@ -1322,3 +1380,46 @@ class AlterarSenhaForm(Form):
"Nova senha não pode ser igual à senha anterior")
return self.cleaned_data
class PartidoForm(FileFieldCheckMixin, ModelForm):
class Meta:
model = Partido
exclude = []
def __init__(self, *args, **kwargs):
super(PartidoForm, self).__init__(*args, **kwargs)
# TODO Utilizar esses campos na issue #2161 de alteração de nomes de partidos
# if self.instance:
# if self.instance.nome:
# self.fields['nome'].widget.attrs['readonly'] = True
# self.fields['sigla'].widget.attrs['readonly'] = True
row1 = to_row(
[('sigla', 2),
('nome', 6),
('data_criacao', 2),
('data_extincao', 2),])
row2 = to_row([('observacao', 12)])
row3 = to_row([('logo_partido', 12)])
self.helper = SaplFormHelper()
self.helper.layout = Layout(
row1, row2, row3,
form_actions(label='Salvar'))
def clean(self):
cleaned_data = super(PartidoForm, self).clean()
if not self.is_valid():
return cleaned_data
if cleaned_data['data_criacao'] and cleaned_data['data_extincao']:
if cleaned_data['data_criacao'] > cleaned_data['data_extincao']:
raise ValidationError("Certifique-se de que a data de criação seja anterior à data de extinção.")
return cleaned_data

20
sapl/base/migrations/0030_appconfig_escolher_numero_materia_proposicao.py

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-02-19 11:14
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('base', '0029_remove_appconfig_relatorios_atos'),
]
operations = [
migrations.AddField(
model_name='appconfig',
name='escolher_numero_materia_proposicao',
field=models.BooleanField(choices=[(True, 'Sim'), (False, 'Não')], default=False, verbose_name='Indicar número da matéria a ser gerada na proposição?'),
),
]

20
sapl/base/migrations/0030_appconfig_protocolo_manual.py

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-02-15 18:25
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('base', '0029_remove_appconfig_relatorios_atos'),
]
operations = [
migrations.AddField(
model_name='appconfig',
name='protocolo_manual',
field=models.BooleanField(choices=[(True, 'Sim'), (False, 'Não')], default=False, verbose_name='Protocolar proposição somente com recibo?'),
),
]

20
sapl/base/migrations/0031_auto_20190218_1109.py

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-02-18 14:09
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('base', '0030_appconfig_protocolo_manual'),
]
operations = [
migrations.AlterField(
model_name='appconfig',
name='protocolo_manual',
field=models.BooleanField(choices=[(True, 'Sim'), (False, 'Não')], default=False, verbose_name='Informar data e hora de protocolo?'),
),
]

16
sapl/base/migrations/0032_merge_20190219_0941.py

@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-02-19 12:41
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('base', '0031_auto_20190218_1109'),
('base', '0030_appconfig_escolher_numero_materia_proposicao'),
]
operations = [
]

20
sapl/base/migrations/0033_auto_20190415_1050.py

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-04-15 13:50
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('base', '0032_merge_20190219_0941'),
]
operations = [
migrations.AlterField(
model_name='appconfig',
name='sequencia_numeracao',
field=models.CharField(choices=[('A', 'Sequencial por ano para cada autor'), ('B', 'Sequencial por ano indepententemente do autor'), ('L', 'Sequencial por legislatura'), ('U', 'Sequencial único')], default='A', max_length=1, verbose_name='Sequência de numeração'),
),
]

29
sapl/base/migrations/0034_auto_20190417_0941.py

@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-04-17 12:41
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('base', '0033_auto_20190415_1050'),
]
operations = [
migrations.RemoveField(
model_name='appconfig',
name='sequencia_numeracao',
),
migrations.AddField(
model_name='appconfig',
name='sequencia_numeracao_proposicao',
field=models.CharField(choices=[('A', 'Sequencial por ano para cada autor'), ('B', 'Sequencial por ano indepententemente do autor'), ('L', 'Sequencial por legislatura'), ('U', 'Sequencial único')], default='A', max_length=1, verbose_name='Sequência de numeração de proposições'),
),
migrations.AddField(
model_name='appconfig',
name='sequencia_numeracao_protocolo',
field=models.CharField(choices=[('A', 'Sequencial por ano para cada autor'), ('L', 'Sequencial por legislatura'), ('U', 'Sequencial único')], default='A', max_length=1, verbose_name='Sequência de numeração de protocolos'),
),
]

20
sapl/base/migrations/0035_auto_20190417_1009.py

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-04-17 13:09
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('base', '0034_auto_20190417_0941'),
]
operations = [
migrations.AlterField(
model_name='appconfig',
name='sequencia_numeracao_proposicao',
field=models.CharField(choices=[('A', 'Sequencial por ano para cada autor'), ('B', 'Sequencial por ano indepententemente do autor')], default='A', max_length=1, verbose_name='Sequência de numeração de proposições'),
),
]

20
sapl/base/migrations/0036_auto_20190417_1432.py

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-04-17 17:32
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('base', '0035_auto_20190417_1009'),
]
operations = [
migrations.AlterField(
model_name='appconfig',
name='sequencia_numeracao_protocolo',
field=models.CharField(choices=[('A', 'Sequencial por ano'), ('L', 'Sequencial por legislatura'), ('U', 'Sequencial único')], default='A', max_length=1, verbose_name='Sequência de numeração de protocolos'),
),
]

24
sapl/base/models.py

@ -18,10 +18,13 @@ TIPO_DOCUMENTO_ADMINISTRATIVO = ((DOC_ADM_OSTENSIVO, _('Ostensiva')),
RELATORIO_ATOS_ACESSADOS = (('S', _('Sim')),
('N', _('Não')))
SEQUENCIA_NUMERACAO = (('A', _('Sequencial por ano')),
SEQUENCIA_NUMERACAO_PROTOCOLO = (('A', _('Sequencial por ano')),
('L', _('Sequencial por legislatura')),
('U', _('Sequencial único')))
SEQUENCIA_NUMERACAO_PROPOSICAO = (('A', _('Sequencial por ano para cada autor')),
('B', _('Sequencial por ano indepententemente do autor')))
ESFERA_FEDERACAO_CHOICES = (('M', _('Municipal')),
('E', _('Estadual')),
('F', _('Federal')),
@ -95,10 +98,15 @@ class AppConfig(models.Model):
verbose_name=_('Estatísticas de acesso a normas'),
choices=RELATORIO_ATOS_ACESSADOS, default='N')
sequencia_numeracao = models.CharField(
sequencia_numeracao_proposicao = models.CharField(
max_length=1,
verbose_name=_('Sequência de numeração de proposições'),
choices=SEQUENCIA_NUMERACAO_PROPOSICAO, default='A')
sequencia_numeracao_protocolo = models.CharField(
max_length=1,
verbose_name=_('Sequência de numeração'),
choices=SEQUENCIA_NUMERACAO, default='A')
verbose_name=_('Sequência de numeração de protocolos'),
choices=SEQUENCIA_NUMERACAO_PROTOCOLO, default='A')
esfera_federacao = models.CharField(
max_length=1,
@ -160,6 +168,14 @@ class AppConfig(models.Model):
verbose_name=_('Protocolar proposição somente com recibo?'),
choices=YES_NO_CHOICES, default=True)
protocolo_manual = models.BooleanField(
verbose_name=_('Informar data e hora de protocolo?'),
choices=YES_NO_CHOICES, default=False)
escolher_numero_materia_proposicao = models.BooleanField(
verbose_name=_('Indicar número da matéria a ser gerada na proposição?'),
choices=YES_NO_CHOICES, default=False)
class Meta:
verbose_name = _('Configurações da Aplicação')
verbose_name_plural = _('Configurações da Aplicação')

29
sapl/base/search_indexes.py

@ -1,5 +1,4 @@
import os.path
import textract
import logging
from django.db.models import F, Q, Value
@ -11,7 +10,6 @@ from haystack.constants import Indexable
from haystack.fields import CharField
from haystack.indexes import SearchIndex
from haystack.utils import get_model_ct_tuple
from textract.exceptions import ExtensionNotSupported
from sapl.compilacao.models import (STATUS_TA_IMMUTABLE_PUBLIC,
STATUS_TA_PUBLIC, Dispositivo)
@ -49,19 +47,6 @@ class TextExtractField(CharField):
data = ''
return data
def whoosh_extraction(self, arquivo):
if arquivo.path.endswith('html') or arquivo.path.endswith('xml'):
with open(arquivo.path, 'r', encoding="utf8", errors='ignore') as f:
content = ' '.join(f.read())
return RemoveTag(content)
else:
return textract.process(
arquivo.path,
language='pt-br').decode('utf-8').replace('\n', ' ').replace(
'\t', ' ')
def print_error(self, arquivo, error):
msg = 'Erro inesperado processando arquivo %s erro: %s' % (
arquivo.path, error)
@ -80,20 +65,6 @@ class TextExtractField(CharField):
except Exception as err:
print(str(err))
self.print_error(arquivo, err)
# Em ambiente de DEV utiliza-se o Whoosh
# Como ele não possui extração, faz-se uso do textract
else:
try:
self.logger.debug("Tentando whoosh_extraction no arquivo {}".format(arquivo.path))
return self.whoosh_extraction(arquivo)
self.print_error(arquivo)
except ExtensionNotSupported as err:
print(str(err))
self.logger.error(str(err))
except Exception as err:
print(str(err))
self.print_error(arquivo, str(err))
return ''
def ta_extractor(self, value):

13
sapl/base/templatetags/common_tags.py

@ -1,3 +1,6 @@
from _functools import reduce
import re
from django import template
from django.template.defaultfilters import stringfilter
from django.utils.safestring import mark_safe
@ -9,7 +12,6 @@ from sapl.norma.models import NormaJuridica
from sapl.parlamentares.models import Filiacao
from sapl.utils import filiacao_data, SEPARADOR_HASH_PROPOSICAO
register = template.Library()
@ -286,3 +288,12 @@ def render_chunk_vendors(extension=None):
return mark_safe('\n'.join(tags))
except:
return ''
@register.filter(is_safe=True)
@stringfilter
def dont_break_out(value):
_safe = '<div class="dont-break-out">{}</div>'.format(value)
_safe = mark_safe(_safe)
return _safe

2
sapl/base/tests/test_login.py

@ -13,7 +13,7 @@ def user():
def test_login_aparece_na_barra_para_usuario_nao_logado(client):
response = client.get('/')
assert '<a class="nav-link" href="/login/"><img src="/static/img/user.png"></a>' in str(
assert '<a class="nav-link" href="/login/"><img src="/static/sapl/frontend/img/user.png"></a>' in str(
response.content)

58
sapl/base/urls.py

@ -8,14 +8,14 @@ from django.contrib.auth.views import (password_reset, password_reset_complete,
password_reset_done)
from django.views.generic.base import RedirectView, TemplateView
from sapl.base.views import AutorCrud, ConfirmarEmailView, TipoAutorCrud
from sapl.base.views import AutorCrud, ConfirmarEmailView, TipoAutorCrud, get_estatistica
from sapl.settings import EMAIL_SEND_USER, MEDIA_URL
from .apps import AppConfig
from .forms import LoginForm, NovaSenhaForm, RecuperarSenhaForm
from .views import (AlterarSenha, AppConfigCrud, CasaLegislativaCrud,
CreateUsuarioView, DeleteUsuarioView, EditUsuarioView,
HelpTopicView, ListarUsuarioView, LogotipoView,
HelpTopicView, PesquisarUsuarioView, LogotipoView,
RelatorioAtasView, RelatorioAudienciaView,
RelatorioDataFimPrazoTramitacaoView,
RelatorioHistoricoTramitacaoView,
@ -27,12 +27,24 @@ from .views import (AlterarSenha, AppConfigCrud, CasaLegislativaCrud,
RelatorioNormasPublicadasMesView,
RelatorioNormasVigenciaView,
EstatisticasAcessoNormas,
RelatoriosListView)
RelatoriosListView,
ListarInconsistenciasView, ListarProtocolosDuplicadosView,
ListarProtocolosComMateriasView,
ListarMatProtocoloInexistenteView,
ListarParlamentaresDuplicadosView,
ListarFiliacoesSemDataFiliacaoView,
ListarMandatoSemDataInicioView,
ListarParlMandatosIntersecaoView,
ListarParlFiliacoesIntersecaoView,
ListarAutoresDuplicadosView,
ListarBancadaComissaoAutorExternoView,
ListarLegislaturaInfindavelView)
app_name = AppConfig.name
admin_user = [
url(r'^sistema/usuario/$', ListarUsuarioView.as_view(), name='user_list'),
url(r'^sistema/usuario/$', PesquisarUsuarioView.as_view(), name='usuario'),
url(r'^sistema/usuario/create$', CreateUsuarioView.as_view(), name='user_create'),
url(r'^sistema/usuario/(?P<pk>\d+)/edit$', EditUsuarioView.as_view(), name='user_edit'),
url(r'^sistema/usuario/(?P<pk>\d+)/delete$', DeleteUsuarioView.as_view(), name='user_delete')
@ -127,6 +139,44 @@ urlpatterns = [
'(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})$',
ConfirmarEmailView.as_view(), name='confirmar_email'),
url(r'^sistema/inconsistencias/$',
ListarInconsistenciasView.as_view(),
name='lista_inconsistencias'),
url(r'^sistema/inconsistencias/protocolos_duplicados$',
ListarProtocolosDuplicadosView.as_view(),
name='lista_protocolos_duplicados'),
url(r'^sistema/inconsistencias/protocolos_com_materias$',
ListarProtocolosComMateriasView.as_view(),
name='lista_protocolos_com_materias'),
url(r'^sistema/inconsistencias/materias_protocolo_inexistente$',
ListarMatProtocoloInexistenteView.as_view(),
name='lista_materias_protocolo_inexistente'),
url(r'^sistema/inconsistencias/filiacoes_sem_data_filiacao$',
ListarFiliacoesSemDataFiliacaoView.as_view(),
name='lista_filiacoes_sem_data_filiacao'),
url(r'^sistema/inconsistencias/mandato_sem_data_inicio',
ListarMandatoSemDataInicioView.as_view(),
name='lista_mandato_sem_data_inicio'),
url(r'^sistema/inconsistencias/parlamentares_duplicados$',
ListarParlamentaresDuplicadosView.as_view(),
name='lista_parlamentares_duplicados'),
url(r'^sistema/inconsistencias/parlamentares_mandatos_intersecao$',
ListarParlMandatosIntersecaoView.as_view(),
name='lista_parlamentares_mandatos_intersecao'),
url(r'^sistema/inconsistencias/parlamentares_filiacoes_intersecao$',
ListarParlFiliacoesIntersecaoView.as_view(),
name='lista_parlamentares_filiacoes_intersecao'),
url(r'^sistema/inconsistencias/autores_duplicados$',
ListarAutoresDuplicadosView.as_view(),
name='lista_autores_duplicados'),
url(r'^sistema/inconsistencias/bancada_comissao_autor_externo$',
ListarBancadaComissaoAutorExternoView.as_view(),
name='lista_bancada_comissao_autor_externo'),
url(r'^sistema/inconsistencias/legislatura_infindavel$',
ListarLegislaturaInfindavelView.as_view(),
name='lista_legislatura_infindavel'),
url(r'^sistema/estatisticas', get_estatistica),
# todos os sublinks de sistema devem vir acima deste
url(r'^sistema/$', permission_required('base.view_tabelas_auxiliares')

629
sapl/base/views.py

@ -1,18 +1,20 @@
import collections
import itertools
import datetime
import logging
import os
from django.contrib import messages
from django.contrib.auth import get_user_model
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.contrib.auth.models import Group
from django.contrib.auth.models import Group, User
from django.contrib.auth.tokens import default_token_generator
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied, ValidationError
from django.core.mail import send_mail
from django.core.urlresolvers import reverse
from django.core.urlresolvers import reverse, reverse_lazy
from django.db import connection
from django.db.models import Count, Q
from django.http import Http404, HttpResponseRedirect
from django.db.models import Count, Q, ProtectedError
from django.http import Http404, HttpResponseRedirect, JsonResponse
from django.template import TemplateDoesNotExist
from django.template.loader import get_template
from django.utils import timezone
@ -35,10 +37,13 @@ from sapl.crud.base import CrudAux, make_pagination
from sapl.materia.models import (Autoria, MateriaLegislativa, Proposicao,
TipoMateriaLegislativa, StatusTramitacao, UnidadeTramitacao)
from sapl.norma.models import (NormaJuridica, NormaEstatisticas)
from sapl.parlamentares.models import Parlamentar, Legislatura, Mandato, Filiacao
from sapl.protocoloadm.models import Protocolo
from sapl.sessao.models import (PresencaOrdemDia, SessaoPlenaria,
SessaoPlenariaPresenca)
SessaoPlenariaPresenca, Bancada)
from sapl.utils import (parlamentares_ativos, gerar_hash_arquivo, SEPARADOR_HASH_PROPOSICAO,
show_results_filter_set, mail_service_configured)
show_results_filter_set, mail_service_configured,
intervalos_tem_intersecao,)
from .forms import (AlterarSenhaForm, CasaLegislativaForm,
ConfiguracoesAppForm, RelatorioAtasFilterSet,
@ -52,7 +57,7 @@ from .forms import (AlterarSenhaForm, CasaLegislativaForm,
RelatorioReuniaoFilterSet, UsuarioCreateForm,
UsuarioEditForm, RelatorioNormasMesFilterSet,
RelatorioNormasVigenciaFilterSet,
EstatisticasAcessoNormasForm)
EstatisticasAcessoNormasForm, UsuarioFilterSet)
from .models import AppConfig, CasaLegislativa
@ -606,12 +611,13 @@ class RelatorioMateriasTramitacaoView(FilterView):
qs = filtra_url_materias_em_tramitacao(
qr, qs, 'tramitacao__status', 'status')
context['object_list'] = qs
li = [li1 for li1 in qs if li1.tramitacao_set.last() and li1.tramitacao_set.last().status.indicador != 'F']
context['object_list'] = li
qtdes = {}
for tipo in TipoMateriaLegislativa.objects.all():
qs = context['object_list']
qtde = len(qs.filter(tipo_id=tipo.id))
li = context['object_list']
qtde = sum(1 for i in li if i.tipo_id==tipo.id)
if qtde > 0:
qtdes[tipo] = qtde
context['qtdes'] = qtdes
@ -918,42 +924,575 @@ class EstatisticasAcessoNormas(TemplateView):
return self.render_to_response(context)
class ListarUsuarioView(PermissionRequiredMixin, ListView):
class ListarInconsistenciasView(PermissionRequiredMixin, ListView):
model = get_user_model()
template_name = 'base/lista_inconsistencias.html'
context_object_name = 'tabela_inconsistencias'
permission_required = ('base.list_appconfig',)
def get_queryset(self):
tabela = []
tabela.append(
('protocolos_duplicados',
'Protocolos duplicados',
len(protocolos_duplicados())
)
)
tabela.append(
('protocolos_com_materias',
'Protocolos que excedem o limite de matérias vinculadas',
len(protocolos_com_materias())
)
)
tabela.append(
('materias_protocolo_inexistente',
'Matérias Legislativas com protocolo inexistente',
len(materias_protocolo_inexistente())
)
)
tabela.append(
('filiacoes_sem_data_filiacao',
'Filiações sem data filiação',
len(filiacoes_sem_data_filiacao())
)
)
tabela.append(
('mandato_sem_data_inicio',
'Mandatos sem data inicial',
len(mandato_sem_data_inicio())
)
)
tabela.append(
('parlamentares_duplicados',
'Parlamentares duplicados',
len(parlamentares_duplicados())
)
)
tabela.append(
('parlamentares_mandatos_intersecao',
'Parlamentares com mandatos em interseção',
len(parlamentares_mandatos_intersecao())
)
)
tabela.append(
('parlamentares_filiacoes_intersecao',
'Parlamentares com filiações em interseção',
len(parlamentares_filiacoes_intersecao())
)
)
tabela.append(
('autores_duplicados',
'Autores duplicados',
len(autores_duplicados())
)
)
tabela.append(
('bancada_comissao_autor_externo',
'Bancadas e Comissões com autor externo',
len(bancada_comissao_autor_externo())
)
)
tabela.append(
('legislatura_infindavel',
'Legislaturas sem data fim',
len(legislatura_infindavel())
)
)
return tabela
def legislatura_infindavel():
return Legislatura.objects.filter(data_fim__isnull=True).order_by('-numero')
class ListarLegislaturaInfindavelView(PermissionRequiredMixin, ListView):
model = get_user_model()
template_name = 'base/legislatura_infindavel.html'
context_object_name = 'legislatura_infindavel'
permission_required = ('base.list_appconfig',)
paginate_by = 10
def get_queryset(self):
return legislatura_infindavel()
def get_context_data(self, **kwargs):
context = super(
ListarLegislaturaInfindavelView, self
).get_context_data(**kwargs)
paginator = context['paginator']
page_obj = context['page_obj']
context['page_range'] = make_pagination(
page_obj.number, paginator.num_pages)
context[
'NO_ENTRIES_MSG'
] = 'Nenhuma encontrada.'
return context
def bancada_comissao_autor_externo():
tipo_autor_externo = TipoAutor.objects.filter(descricao='Externo')
lista_bancada_autor_externo = []
for bancada in Bancada.objects.all().order_by('nome'):
autor_externo = bancada.autor.filter(tipo=tipo_autor_externo)
if autor_externo:
q_autor_externo = bancada.autor.get(tipo=tipo_autor_externo)
lista_bancada_autor_externo.append(
(q_autor_externo, bancada, 'Bancada', 'sistema/bancada')
)
lista_comissao_autor_externo = []
for comissao in Comissao.objects.all().order_by('nome'):
autor_externo = comissao.autor.filter(tipo=tipo_autor_externo)
if autor_externo:
q_autor_externo = comissao.autor.get(tipo=tipo_autor_externo)
lista_comissao_autor_externo.append(
(q_autor_externo, comissao, 'Comissão', 'comissao')
)
return lista_bancada_autor_externo + lista_comissao_autor_externo
class ListarBancadaComissaoAutorExternoView(PermissionRequiredMixin, ListView):
model = get_user_model()
template_name = 'base/bancada_comissao_autor_externo.html'
context_object_name = 'bancada_comissao_autor_externo'
permission_required = ('base.list_appconfig',)
paginate_by = 10
def get_queryset(self):
return bancada_comissao_autor_externo()
def get_context_data(self, **kwargs):
context = super(
ListarBancadaComissaoAutorExternoView, self
).get_context_data(**kwargs)
paginator = context['paginator']
page_obj = context['page_obj']
context['page_range'] = make_pagination(
page_obj.number, paginator.num_pages)
context[
'NO_ENTRIES_MSG'
] = 'Nenhuma encontrada.'
return context
def autores_duplicados():
return [autor for autor in Autor.objects.values('nome').annotate(count=Count('nome')).filter(count__gt=1)]
class ListarAutoresDuplicadosView(PermissionRequiredMixin, ListView):
model = get_user_model()
template_name = 'base/autores_duplicados.html'
context_object_name = 'autores_duplicados'
permission_required = ('base.list_appconfig',)
paginate_by = 10
def get_queryset(self):
return autores_duplicados()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
paginator = context['paginator']
page_obj = context['page_obj']
context['page_range'] = make_pagination(
page_obj.number, paginator.num_pages)
context[
'NO_ENTRIES_MSG'
] = 'Nenhum encontrado.'
return context
def parlamentares_filiacoes_intersecao():
intersecoes = []
for parlamentar in Parlamentar.objects.all().order_by('nome_parlamentar'):
filiacoes = parlamentar.filiacao_set.all()
combinacoes = itertools.combinations(filiacoes, 2)
for c in combinacoes:
data_filiacao1 = c[0].data
data_desfiliacao1 = c[0].data_desfiliacao if c[0].data_desfiliacao else timezone.now().date()
data_filiacao2 = c[1].data
data_desfiliacao2 = c[1].data_desfiliacao if c[1].data_desfiliacao else timezone.now().date()
if data_filiacao1 and data_filiacao2:
exists = intervalos_tem_intersecao(
data_filiacao1, data_desfiliacao1,
data_filiacao2, data_desfiliacao2)
if exists:
intersecoes.append((parlamentar, c[0], c[1]))
return intersecoes
class ListarParlFiliacoesIntersecaoView(PermissionRequiredMixin, ListView):
model = get_user_model()
template_name = 'base/parlamentares_filiacoes_intersecao.html'
context_object_name = 'parlamentares_filiacoes_intersecao'
permission_required = ('base.list_appconfig',)
paginate_by = 10
def get_queryset(self):
return parlamentares_filiacoes_intersecao()
def get_context_data(self, **kwargs):
context = super(
ListarParlFiliacoesIntersecaoView, self).get_context_data(**kwargs)
paginator = context['paginator']
page_obj = context['page_obj']
context['page_range'] = make_pagination(
page_obj.number, paginator.num_pages)
context[
'NO_ENTRIES_MSG'
] = 'Nenhum encontrado.'
return context
def parlamentares_mandatos_intersecao():
intersecoes = []
for parlamentar in Parlamentar.objects.all().order_by('nome_parlamentar'):
mandatos = parlamentar.mandato_set.all()
combinacoes = itertools.combinations(mandatos, 2)
for c in combinacoes:
data_inicio_mandato1 = c[0].data_inicio_mandato
data_fim_mandato1 = c[0].data_fim_mandato if c[0].data_fim_mandato else timezone.now().date()
data_inicio_mandato2 = c[1].data_inicio_mandato
data_fim_mandato2 = c[1].data_fim_mandato if c[1].data_fim_mandato else timezone.now().date()
if data_inicio_mandato1 and data_inicio_mandato2:
exists = intervalos_tem_intersecao(
data_inicio_mandato1, data_fim_mandato1,
data_inicio_mandato2, data_fim_mandato2)
if exists:
intersecoes.append((parlamentar, c[0], c[1]))
return intersecoes
class ListarParlMandatosIntersecaoView(PermissionRequiredMixin, ListView):
model = get_user_model()
template_name = 'base/parlamentares_mandatos_intersecao.html'
context_object_name = 'parlamentares_mandatos_intersecao'
permission_required = ('base.list_appconfig',)
paginate_by = 10
def get_queryset(self):
return parlamentares_mandatos_intersecao()
def get_context_data(self, **kwargs):
context = super(
ListarParlMandatosIntersecaoView, self).get_context_data(**kwargs)
paginator = context['paginator']
page_obj = context['page_obj']
context['page_range'] = make_pagination(
page_obj.number, paginator.num_pages)
context[
'NO_ENTRIES_MSG'
] = 'Nenhum encontrado.'
return context
def parlamentares_duplicados():
return [parlamentar.values() for parlamentar in Parlamentar.objects.values(
'nome_parlamentar').order_by('nome_parlamentar').annotate(count=Count(
'nome_parlamentar')).filter(count__gt=1)]
class ListarParlamentaresDuplicadosView(PermissionRequiredMixin, ListView):
model = get_user_model()
template_name = 'base/parlamentares_duplicados.html'
context_object_name = 'parlamentares_duplicados'
permission_required = ('base.list_appconfig',)
paginate_by = 10
def get_queryset(self):
return parlamentares_duplicados()
def get_context_data(self, **kwargs):
context = super(
ListarParlamentaresDuplicadosView, self).get_context_data(**kwargs)
paginator = context['paginator']
page_obj = context['page_obj']
context['page_range'] = make_pagination(
page_obj.number, paginator.num_pages)
context[
'NO_ENTRIES_MSG'
] = 'Nenhum encontrado.'
return context
def mandato_sem_data_inicio():
return Mandato.objects.filter(data_inicio_mandato__isnull=True).order_by('parlamentar')
def get_estatistica(request):
json_dict = {}
datas = [MateriaLegislativa.objects.all().
order_by('-data_ultima_atualizacao').
values_list('data_ultima_atualizacao', flat=True).
first(),
NormaJuridica.objects.all().
order_by('-data_ultima_atualizacao').
values_list('data_ultima_atualizacao', flat=True).
first()] # Retorna [None, None] se inexistem registros
max_data = ''
if datas[0] and datas[1]:
max_data = max(datas)
else:
max_data = next(iter([i for i in datas if i is not None]), '')
json_dict["data_ultima_atualizacao"] = max_data
json_dict["num_materias_legislativas"] = MateriaLegislativa.objects.all().count()
json_dict["num_normas_juridicas "] = NormaJuridica.objects.all().count()
json_dict["num_parlamentares"] = Parlamentar.objects.all().count()
json_dict["num_sessoes_plenarias"] = SessaoPlenaria.objects.all().count()
return JsonResponse(json_dict)
class ListarMandatoSemDataInicioView(PermissionRequiredMixin, ListView):
model = get_user_model()
template_name = 'base/mandato_sem_data_inicio.html'
context_object_name = 'mandato_sem_data_inicio'
permission_required = ('base.list_appconfig',)
paginate_by = 10
def get_queryset(self):
return mandato_sem_data_inicio()
def get_context_data(self, **kwargs):
context = super(
ListarMandatoSemDataInicioView, self
).get_context_data(**kwargs)
paginator = context['paginator']
page_obj = context['page_obj']
context['page_range'] = make_pagination(
page_obj.number, paginator.num_pages)
context[
'NO_ENTRIES_MSG'
] = 'Nenhum encontrado.'
return context
def filiacoes_sem_data_filiacao():
return Filiacao.objects.filter(data__isnull=True).order_by('parlamentar')
class ListarFiliacoesSemDataFiliacaoView(PermissionRequiredMixin, ListView):
model = get_user_model()
template_name = 'base/filiacoes_sem_data_filiacao.html'
context_object_name = 'filiacoes_sem_data_filiacao'
permission_required = ('base.list_appconfig',)
paginate_by = 10
def get_queryset(self):
return filiacoes_sem_data_filiacao()
def get_context_data(self, **kwargs):
context = super(
ListarFiliacoesSemDataFiliacaoView, self
).get_context_data(**kwargs)
paginator = context['paginator']
page_obj = context['page_obj']
context['page_range'] = make_pagination(
page_obj.number, paginator.num_pages)
context[
'NO_ENTRIES_MSG'
] = 'Nenhuma encontrada.'
return context
def materias_protocolo_inexistente():
materias = []
for materia in MateriaLegislativa.objects.filter(numero_protocolo__isnull=False).order_by('-ano', 'numero'):
exists = Protocolo.objects.filter(
ano=materia.ano, numero=materia.numero_protocolo).exists()
if not exists:
materias.append(
(materia, materia.ano, materia.numero_protocolo))
return materias
class ListarMatProtocoloInexistenteView(PermissionRequiredMixin, ListView):
model = get_user_model()
template_name = 'base/materias_protocolo_inexistente.html'
context_object_name = 'materias_protocolo_inexistente'
permission_required = ('base.list_appconfig',)
paginate_by = 10
def get_queryset(self):
return materias_protocolo_inexistente()
def get_context_data(self, **kwargs):
context = super(
ListarMatProtocoloInexistenteView, self
).get_context_data(**kwargs)
paginator = context['paginator']
page_obj = context['page_obj']
context['page_range'] = make_pagination(
page_obj.number, paginator.num_pages)
context[
'NO_ENTRIES_MSG'
] = 'Nenhuma encontrada.'
return context
def protocolos_com_materias():
protocolos = {}
for m in MateriaLegislativa.objects.filter(numero_protocolo__isnull=False).order_by('-ano', 'numero_protocolo'):
if Protocolo.objects.filter(numero=m.numero_protocolo, ano=m.ano).exists():
key = "{}/{}".format(m.numero_protocolo, m.ano)
val = protocolos.get(key, list())
val.append(m)
protocolos[key] = val
return [(v[0], len(v)) for (k, v) in protocolos.items() if len(v) > 1]
class ListarProtocolosComMateriasView(PermissionRequiredMixin, ListView):
model = get_user_model()
template_name = 'base/protocolos_com_materias.html'
context_object_name = 'protocolos_com_materias'
permission_required = ('base.list_appconfig',)
paginate_by = 10
def get_queryset(self):
return protocolos_com_materias()
def get_context_data(self, **kwargs):
context = super(
ListarProtocolosComMateriasView, self).get_context_data(**kwargs)
paginator = context['paginator']
page_obj = context['page_obj']
context['page_range'] = make_pagination(
page_obj.number, paginator.num_pages)
context[
'NO_ENTRIES_MSG'
] = 'Nenhum encontrado.'
return context
def protocolos_duplicados():
protocolos = {}
for p in Protocolo.objects.order_by('-ano', 'numero'):
key = "{}/{}".format(p.numero, p.ano)
val = protocolos.get(key, list())
val.append(p)
protocolos[key] = val
return [(v[0], len(v)) for (k, v) in protocolos.items() if len(v) > 1]
class ListarProtocolosDuplicadosView(PermissionRequiredMixin, ListView):
model = get_user_model()
template_name = 'auth/user_list.html'
context_object_name = 'user_list'
template_name = 'base/protocolos_duplicados.html'
context_object_name = 'protocolos_duplicados'
permission_required = ('base.list_appconfig',)
paginate_by = 10
def get_queryset(self):
qs = super(ListarUsuarioView, self).get_queryset()
return qs.order_by('username')
return protocolos_duplicados()
def get_context_data(self, **kwargs):
context = super(ListarUsuarioView, self).get_context_data(**kwargs)
context = super(
ListarProtocolosDuplicadosView, self).get_context_data(**kwargs)
paginator = context['paginator']
page_obj = context['page_obj']
context['page_range'] = make_pagination(
page_obj.number, paginator.num_pages)
context['NO_ENTRIES_MSG'] = 'Nenhum usuário cadastrado.'
context[
'NO_ENTRIES_MSG'
] = 'Nenhum encontrado.'
return context
class PesquisarUsuarioView(PermissionRequiredMixin, FilterView):
model = User
filterset_class = UsuarioFilterSet
permission_required = ('base.list_appconfig',)
paginate_by = 10
def get_filterset_kwargs(self, filterset_class):
super(PesquisarUsuarioView,
self).get_filterset_kwargs(filterset_class)
kwargs = {'data': self.request.GET or None}
qs = self.get_queryset().order_by('username').distinct()
kwargs.update({
'queryset': qs,
})
return kwargs
def get_context_data(self, **kwargs):
context = super(PesquisarUsuarioView,
self).get_context_data(**kwargs)
paginator = context['paginator']
page_obj = context['page_obj']
context['page_range'] = make_pagination(
page_obj.number, paginator.num_pages)
context['NO_ENTRIES_MSG'] = 'Nenhum usuário encontrado!'
context['title'] = _('Usuários')
return context
def get(self, request, *args, **kwargs):
super(PesquisarUsuarioView, self).get(request)
data = self.filterset.data
url = ''
if data:
url = "&" + str(self.request.META['QUERY_STRING'])
if url.startswith("&page"):
ponto_comeco = url.find('username=') - 1
url = url[ponto_comeco:]
context = self.get_context_data(filter=self.filterset,
object_list=self.object_list,
filter_url=url,
numero_res=len(self.object_list)
)
context['show_results'] = show_results_filter_set(
self.request.GET.copy())
return self.render_to_response(context)
class CreateUsuarioView(PermissionRequiredMixin, CreateView):
model = get_user_model()
form_class = UsuarioCreateForm
success_message = 'Usuário criado com sucesso'
success_message = 'Usuário criado com sucesso!'
fail_message = 'Usuário não criado!'
permission_required = ('base.add_appconfig',)
def get_success_url(self):
return reverse('sapl.base:user_list')
return reverse('sapl.base:usuario')
def form_valid(self, form):
data = form.cleaned_data
new_user = get_user_model().objects.create(
username=data['username'], email=data['email'])
username=data['username'],
email=data['email']
)
new_user.first_name = data['firstname']
new_user.last_name = data['lastname']
new_user.set_password(data['password1'])
@ -965,33 +1504,53 @@ class CreateUsuarioView(PermissionRequiredMixin, CreateView):
for g in groups:
g.user_set.add(new_user)
messages.success(self.request, self.success_message)
return HttpResponseRedirect(self.get_success_url())
def form_invalid(self, form):
messages.error(self.request, self.fail_message)
return super().form_invalid(form)
class DeleteUsuarioView(PermissionRequiredMixin, DeleteView):
class DeleteUsuarioView(PermissionRequiredMixin, DeleteView):
model = get_user_model()
template_name = "crud/confirm_delete.html"
permission_required = ('base.delete_appconfig',)
success_url = reverse_lazy('sapl.base:usuario')
success_message = "Usuário removido com sucesso!"
def get_success_url(self):
return reverse('sapl.base:user_list')
def get(self, request, *args, **kwargs):
return self.post(request, *args, **kwargs)
def delete(self, request, *args, **kwargs):
try:
super(DeleteUsuarioView, self).delete(request, *args, **kwargs)
except ProtectedError as exception:
error_url = reverse_lazy('sapl.base:user_delete', kwargs={'pk': self.kwargs['pk']})
error_message = "O usuário não pode ser removido, pois é referenciado por:<br><ul>"
for e in exception.protected_objects:
error_message += '<li>{} - {}</li>'.format(
e._meta.verbose_name, e
)
error_message += '</ul>'
messages.error(self.request, error_message)
return HttpResponseRedirect(error_url)
messages.success(self.request, self.success_message)
return HttpResponseRedirect(self.success_url)
def get_queryset(self):
qs = super(DeleteUsuarioView, self).get_queryset()
return qs.filter(id=self.kwargs['pk'])
@property
def cancel_url(self):
return reverse('sapl.base:user_edit',
kwargs={'pk': self.kwargs['pk']})
class EditUsuarioView(PermissionRequiredMixin, UpdateView):
model = get_user_model()
form_class = UsuarioEditForm
success_message = 'Usuário editado com sucesso'
success_message = 'Usuário editado com sucesso!'
permission_required = ('base.change_appconfig',)
def get_success_url(self):
return reverse('sapl.base:user_list')
return reverse('sapl.base:usuario')
def get_initial(self):
initial = super(EditUsuarioView, self).get_initial()
@ -1028,6 +1587,7 @@ class EditUsuarioView(PermissionRequiredMixin, UpdateView):
for g in groups:
g.user_set.add(user)
messages.success(self.request, self.success_message)
return super(EditUsuarioView, self).form_valid(form)
@ -1096,8 +1656,11 @@ class AppConfigCrud(CrudAux):
def gerar_hash(self, inst):
inst.save()
if inst.texto_original:
try:
inst.hash_code = gerar_hash_arquivo(
inst.texto_original.path, str(inst.pk))
except IOError:
raise ValidationError("Existem proposicoes com arquivos inexistentes.")
elif inst.texto_articulado.exists():
ta = inst.texto_articulado.first()
inst.hash_code = 'P' + ta.hash() + SEPARADOR_HASH_PROPOSICAO + str(inst.pk)

5
sapl/comissoes/forms.py

@ -12,6 +12,7 @@ from sapl.base.models import Autor, TipoAutor
from sapl.comissoes.models import (Comissao, Composicao, DocumentoAcessorio,
Participacao, Reuniao, Periodo)
from sapl.parlamentares.models import Legislatura, Mandato, Parlamentar
from sapl.utils import FileFieldCheckMixin
class ComposicaoForm(forms.ModelForm):
@ -382,7 +383,7 @@ class ReuniaoForm(ModelForm):
return self.cleaned_data
class DocumentoAcessorioCreateForm(forms.ModelForm):
class DocumentoAcessorioCreateForm(FileFieldCheckMixin, forms.ModelForm):
parent_pk = forms.CharField(required=False) # widget=forms.HiddenInput())
@ -404,7 +405,7 @@ class DocumentoAcessorioCreateForm(forms.ModelForm):
reuniao = Reuniao.objects.get(id=self.initial['parent_pk'])
class DocumentoAcessorioEditForm(forms.ModelForm):
class DocumentoAcessorioEditForm(FileFieldCheckMixin, forms.ModelForm):
parent_pk = forms.CharField(required=False) # widget=forms.HiddenInput())

3
sapl/comissoes/urls.py

@ -1,7 +1,7 @@
from django.conf.urls import include, url
from sapl.comissoes.views import (CargoCrud, ComissaoCrud, ComposicaoCrud,
DocumentoAcessorioCrud, MateriasTramitacaoListView, ParticipacaoCrud,
PeriodoComposicaoCrud, ReuniaoCrud, TipoComissaoCrud)
PeriodoComposicaoCrud, ReuniaoCrud, TipoComissaoCrud, get_participacoes_comissao)
from .apps import AppConfig
@ -21,4 +21,5 @@ urlpatterns = [
url(r'^sistema/comissao/periodo-composicao/',
include(PeriodoComposicaoCrud.get_urls())),
url(r'^sistema/comissao/tipo/', include(TipoComissaoCrud.get_urls())),
url(r'^sistema/comissao/recupera-participacoes', get_participacoes_comissao),
]

18
sapl/comissoes/views.py

@ -2,7 +2,7 @@ import logging
from django.core.urlresolvers import reverse
from django.db.models import F
from django.http.response import HttpResponseRedirect
from django.http.response import HttpResponseRedirect, JsonResponse
from django.views.decorators.clickjacking import xframe_options_exempt
from django.views.generic import ListView
from django.views.generic.base import RedirectView
@ -186,6 +186,7 @@ class MateriasTramitacaoListView(ListView):
context = super(
MateriasTramitacaoListView, self).get_context_data(**kwargs)
context['object'] = Comissao.objects.get(id=self.kwargs['pk'])
context['qtde'] = self.object_list.count()
return context
@ -196,7 +197,8 @@ class ReuniaoCrud(MasterDetailCrud):
public = [RP_LIST, RP_DETAIL, ]
class BaseMixin(MasterDetailCrud.BaseMixin):
list_field_names = ['data', 'nome', 'tema']
list_field_names = ['data', 'nome', 'tema', 'upload_ata']
ordering = '-data'
class ListView(MasterDetailCrud.ListView):
logger = logging.getLogger(__name__)
@ -277,3 +279,15 @@ class DocumentoAcessorioCrud(MasterDetailCrud):
return HttpResponseRedirect(
reverse('sapl.comissoes:reuniao_detail',
kwargs={'pk': obj.reuniao.pk}))
def get_participacoes_comissao(request):
parlamentares = []
composicao_id = request.GET.get('composicao_id')
if composicao_id:
parlamentares = [{'nome': p.parlamentar.nome_parlamentar, 'id': p.parlamentar.id} for p in
Participacao.objects.filter(composicao_id=composicao_id).order_by(
'parlamentar__nome_parlamentar')]
return JsonResponse(parlamentares, safe=False)

56
sapl/compilacao/forms.py

@ -3,7 +3,6 @@ from datetime import timedelta
from crispy_forms.bootstrap import (Alert, FieldWithButtons, FormActions,
InlineCheckboxes, InlineRadios,
StrictButton)
from crispy_forms.helper import FormHelper
from crispy_forms.layout import (HTML, Button, Column, Div, Field, Fieldset,
Layout, Row, Submit)
from django import forms
@ -23,10 +22,12 @@ from sapl.compilacao.models import (NOTAS_PUBLICIDADE_CHOICES,
TipoTextoArticulado, TipoVide,
VeiculoPublicacao, Vide)
from sapl.compilacao.utils import DISPOSITIVO_SELECT_RELATED
from sapl.crispy_layout_mixin import SaplFormHelper
from sapl.crispy_layout_mixin import SaplFormLayout, to_column, to_row,\
form_actions
from sapl.utils import YES_NO_CHOICES
error_messages = {
'required': _('Este campo é obrigatório'),
'invalid': _('URL inválida.')
@ -59,6 +60,13 @@ class TipoTaForm(ModelForm):
widget=forms.RadioSelect(),
required=True)
rodape_global = forms.CharField(
label=TipoTextoArticulado._meta.get_field(
'rodape_global').verbose_name,
widget=forms.Textarea(attrs={'id': 'texto-rico'}),
required=False
)
class Meta:
model = TipoTextoArticulado
fields = ['sigla',
@ -66,10 +74,12 @@ class TipoTaForm(ModelForm):
'content_type',
'participacao_social',
'publicacao_func',
'perfis'
'perfis',
'rodape_global'
]
widgets = {'perfis': widgets.CheckboxSelectMultiple()}
widgets = {'perfis': widgets.CheckboxSelectMultiple(),
'rodape_global': forms.Textarea}
def __init__(self, *args, **kwargs):
@ -84,12 +94,18 @@ class TipoTaForm(ModelForm):
('perfis', 12),
])
self.helper = FormHelper()
row3 = to_row([
('rodape_global', 12),
])
self.helper = SaplFormHelper()
self.helper.layout = SaplFormLayout(
Fieldset(_('Identificação Básica'),
row1, css_class="col-md-12"),
Fieldset(_('Funcionalidades'),
row2, css_class="col-md-12"))
row2, css_class="col-md-12"),
Fieldset(_('Nota de Rodapé Global'),
row3, css_class="col-md-12"))
super(TipoTaForm, self).__init__(*args, **kwargs)
@ -153,7 +169,7 @@ class TaForm(ModelForm):
('participacao_social', 3),
])
self.helper = FormHelper()
self.helper = SaplFormHelper()
self.helper.layout = SaplFormLayout(
Fieldset(_('Identificação Básica'), row1, css_class="col-md-12"),
Fieldset(
@ -204,7 +220,7 @@ class NotaForm(ModelForm):
publicacao = forms.DateField(
label=Nota._meta.get_field('publicacao').verbose_name,
input_formats=['%d/%m/%Y'],
input_formats=['%d/%m/%Y', '%d%m%Y'],
required=True,
widget=forms.DateInput(
format='%d/%m/%Y'),
@ -212,7 +228,7 @@ class NotaForm(ModelForm):
)
efetividade = forms.DateField(
label=Nota._meta.get_field('efetividade').verbose_name,
input_formats=['%d/%m/%Y'],
input_formats=['%d/%m/%Y', '%d%m%Y'],
required=True,
widget=forms.DateInput(
format='%d/%m/%Y'),
@ -268,7 +284,7 @@ class NotaForm(ModelForm):
css_class='form-group row justify-content-between mr-1 ml-1'
)
self.helper = FormHelper()
self.helper = SaplFormHelper()
self.helper.layout = Layout(
Div(
@ -363,7 +379,7 @@ class VideForm(ModelForm):
'texto',
placeholder=_('Texto Adicional ao Vide')), 12)))))
self.helper = FormHelper()
self.helper = SaplFormHelper()
self.helper.layout = Layout(
Div(
Div(HTML(_('Vides')), css_class='card-header bg-light'),
@ -471,7 +487,7 @@ class PublicacaoForm(ModelForm):
('url_externa', 8),
])
self.helper = FormHelper()
self.helper = SaplFormHelper()
self.helper.layout = SaplFormLayout(
Fieldset(Publicacao._meta.verbose_name,
row1, row2, row3, css_class="col-md-12"))
@ -659,7 +675,7 @@ class DispositivoEdicaoBasicaForm(ModelForm):
for f in fields:
self.base_fields.update({f: getattr(self, f)})
self.helper = FormHelper()
self.helper = SaplFormHelper()
if not editor_type:
cancel_label = _('Ir para o Editor Sequencial')
@ -790,7 +806,7 @@ class DispositivoSearchModalForm(Form):
)
)
self.helper = FormHelper()
self.helper = SaplFormHelper()
self.helper.layout = Layout(
fields_search,
Row(to_column((Div(css_class='result-busca-dispositivo'), 12))))
@ -903,7 +919,7 @@ class DispositivoEdicaoVigenciaForm(ModelForm):
row_vigencia,
css_class="col-md-12"))
self.helper = FormHelper()
self.helper = SaplFormHelper()
self.helper.layout = SaplFormLayout(
*layout,
cancel_label=_('Ir para o Editor Sequencial'))
@ -1023,7 +1039,7 @@ class DispositivoDefinidorVigenciaForm(Form):
row_vigencia,
css_class="col-md-12"))
self.helper = FormHelper()
self.helper = SaplFormHelper()
self.helper.layout = SaplFormLayout(
*layout,
cancel_label=_('Ir para o Editor Sequencial'))
@ -1162,7 +1178,7 @@ class DispositivoEdicaoAlteracaoForm(ModelForm):
if hasattr(self, f):
self.base_fields.update({f: getattr(self, f)})
self.helper = FormHelper()
self.helper = SaplFormHelper()
self.helper.layout = SaplFormLayout(
*layout,
cancel_label=_('Ir para o Editor Sequencial'))
@ -1328,7 +1344,7 @@ class TextNotificacoesForm(Form):
(Submit('submit-form', _('Filtrar'),
css_class='btn btn-primary float-right'), 2)])
self.helper = FormHelper()
self.helper = SaplFormHelper()
self.helper.layout = Layout(field_type_notificacoes)
super(TextNotificacoesForm, self).__init__(*args, **kwargs)
@ -1374,7 +1390,7 @@ class DispositivoRegistroAlteracaoForm(Form):
_fields = [Div(*layout, css_class="row")] + \
[to_row([(buttons, 12)])]
self.helper = FormHelper()
self.helper = SaplFormHelper()
self.helper.layout = Layout(*_fields)
super(DispositivoRegistroAlteracaoForm, self).__init__(*args, **kwargs)
@ -1431,7 +1447,7 @@ class DispositivoRegistroRevogacaoForm(Form):
_fields = [Div(*layout, css_class="row")] + \
[to_row([(buttons, 12)])]
self.helper = FormHelper()
self.helper = SaplFormHelper()
self.helper.layout = Layout(*_fields)
super(DispositivoRegistroRevogacaoForm, self).__init__(*args, **kwargs)
@ -1481,7 +1497,7 @@ class DispositivoRegistroInclusaoForm(Form):
_fields = [Div(*layout, css_class="row")] + \
[to_row([(buttons, 12)])]
self.helper = FormHelper()
self.helper = SaplFormHelper()
self.helper.layout = Layout(*_fields)
super(DispositivoRegistroInclusaoForm, self).__init__(*args, **kwargs)

20
sapl/compilacao/migrations/0011_tipotextoarticulado_rodape_global.py

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-03-26 18:59
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('compilacao', '0010_auto_20181004_1939'),
]
operations = [
migrations.AddField(
model_name='tipotextoarticulado',
name='rodape_global',
field=models.TextField(default='', help_text='A cada Tipo de Texto Articulado pode ser adicionado uma nota global de rodapé!', verbose_name='Rodapé Global'),
),
]

27
sapl/compilacao/migrations/0012_bug_auto_inserido.py

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.13 on 2018-03-19 13:41
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
def adjust_bug_auto_inserido(apps, schema_editor):
Dispositivo = apps.get_model('compilacao', 'Dispositivo')
Dispositivo.objects.filter(
tipo_dispositivo__class_css__startswith='caput',
dispositivo_pai__tipo_dispositivo__class_css__startswith='artigo',
auto_inserido=False
).update(auto_inserido=True)
class Migration(migrations.Migration):
dependencies = [
('compilacao', '0011_tipotextoarticulado_rodape_global'),
]
operations = [
migrations.RunPython(adjust_bug_auto_inserido),
]

7
sapl/compilacao/models.py

@ -149,6 +149,13 @@ class TipoTextoArticulado(models.Model):
em edição.
"""))
rodape_global = models.TextField(
verbose_name=_('Rodapé Global'),
help_text=_('A cada Tipo de Texto Articulado pode ser adicionado '
'uma nota global de rodapé!'),
default=''
)
class Meta:
verbose_name = _('Tipo de Texto Articulado')
verbose_name_plural = _('Tipos de Texto Articulados')

45
sapl/compilacao/views.py

@ -1545,7 +1545,7 @@ class ActionDeleteDispositivoMixin(ActionsCommonsMixin):
if not anterior:
self.logger.error("user=" + username + ". Não é possível excluir este Dispositivo (id={}) sem"
" excluir toda a sua estrutura!!!".format(base.ta_id))
" excluir toda a sua estrutura!!!".format(base.id))
raise Exception(
_('Não é possível excluir este Dispositivo sem'
' excluir toda a sua estrutura!!!'))
@ -1566,8 +1566,8 @@ class ActionDeleteDispositivoMixin(ActionsCommonsMixin):
for candidato in parents:
if candidato == base:
self.logger.error("user=" + username + ". Não é possível excluir este "
"Dispositivo ({}) sem "
"excluir toda a sua estrutura!!!".format(candidato))
"Dispositivo (id={}) sem "
"excluir toda a sua estrutura!!!".format(candidato.id))
raise Exception(
_('Não é possível excluir este '
'Dispositivo sem '
@ -1604,8 +1604,8 @@ class ActionDeleteDispositivoMixin(ActionsCommonsMixin):
break
else:
self.logger.error("user=" + username + ". Não é possível excluir este "
"Dispositivo ({}) sem excluir toda "
"a sua estrutura!!!".format(candidato))
"Dispositivo (id={}) sem excluir toda "
"a sua estrutura!!!".format(candidato.id))
raise Exception(
_('Não é possível excluir este '
'Dispositivo sem '
@ -1643,6 +1643,7 @@ class ActionDeleteDispositivoMixin(ActionsCommonsMixin):
# excluir e renumerar irmaos
profundidade_base = base.get_profundidade()
auto_inserido_base = base.auto_inserido
base.delete()
for irmao in irmaos_posteriores:
@ -1666,6 +1667,11 @@ class ActionDeleteDispositivoMixin(ActionsCommonsMixin):
i.set_numero_completo([0, 0, 0, 0, 0, 0, ])
i.rotulo = i.rotulo_padrao(local_insert=1)
i.save()
if not irmaos.exists() and \
auto_inserido_base and \
pai_base.nivel:
self.remover_dispositivo(pai_base, False)
else:
# Renumerar Dispostivos de Contagem Contínua
# de dentro da base se pai
@ -2224,9 +2230,15 @@ class ActionDispositivoCreateMixin(ActionsCommonsMixin):
dispositivo_pai=dp.dispositivo_pai).count()
if qtd_existente >= pp[0].quantidade_permitida:
data = {'pk': base.pk,
'pai': [base.dispositivo_pai.pk, ]}
self.set_message(data, 'warning',
data = {'pk': None
if base.dispositivo_pai else
base.pk,
'pai': [
base.dispositivo_pai.pk if
base.dispositivo_pai else
base.pk,
]}
self.set_message(data, 'danger',
_('Limite de inserções de '
'dispositivos deste tipo '
'foi excedido.'), time=6000)
@ -2512,7 +2524,7 @@ class ActionsEditMixin(ActionDragAndMoveDispositivoAlteradoMixin,
local_add=local_add,
create_auto_inserts=True)
if data:
if data and data['pk']:
ndp = Dispositivo.objects.get(pk=data['pk'])
@ -2539,6 +2551,9 @@ class ActionsEditMixin(ActionDragAndMoveDispositivoAlteradoMixin,
data.update({'pk': ndp.pk,
'pai': [bloco_alteracao.pk, ]})
else:
data.update({'pk': bloco_alteracao.pk,
'pai': [bloco_alteracao.pk, ]})
return data
@ -2933,6 +2948,7 @@ class DispositivoDinamicEditView(
class DispositivoSearchFragmentFormView(ListView):
template_name = 'compilacao/dispositivo_form_search_fragment.html'
logger = logging.getLogger(__name__)
def get(self, request, *args, **kwargs):
@ -2955,14 +2971,20 @@ class DispositivoSearchFragmentFormView(ListView):
messages.info(
request, _('Não foram encontrados resultados '
'com seus critérios de busca!'))
username = self.request.user.username
self.logger.error("user=" + username + ". Não foram encontrados "
"resultados com esses critérios de busca. "
"id_tipo_ta=".format(request.GET['tipo_ta']))
try:
r = response.render()
return response
except Exception as e:
messages.error(request, "Erro - %s" % e)
messages.error(request, "Erro - %s" % str(e))
context = {}
self.template_name = 'compilacao/messages.html'
username = self.request.user.username
self.logger.error("user=" + username + ". " + str(e))
return self.render_to_response(context)
def get_queryset(self):
@ -3134,7 +3156,8 @@ class DispositivoSearchFragmentFormView(ListView):
return r
except Exception as e:
print(e)
username = self.request.user.username
self.logger.error("user=" + username + ". " + str(e))
class DispositivoSearchModalView(FormView):

37
sapl/crispy_layout_mixin.py

@ -52,6 +52,35 @@ def form_actions(more=[Div(css_class='clearfix')],
)
class SaplFormHelper(FormHelper):
render_hidden_fields = True # default = False
"""
até a release 1.6.1 do django-crispy-forms, os fields em Meta.Fields eram
renderizados mesmo se não mencionados no helper.
Com esta mudança (https://github.com/django-crispy-forms/django-crispy-forms/commit/6b93e8a362422db8fe54aa731319c7cbc39990ba)
render_hidden_fields foi adicionado uma condição em que a cada
instância do Helper, fosse decidido se os fields não mencionados serião ou
não renderizados...
O Sapl até este commit: https://github.com/interlegis/sapl/commit/22b87f36ebc8659a6ecaf8831ab0f425206b0993
utilizou o django-crispy-forms na versão 1.6.1, ou seja,
sem a condição render_hidden_fields o que fazia o FormHelper, na 1.6.1
set comportar como se, agora, na 1.7.2 o default fosse True.
Como todos os Forms do Sapl foram construídos assumindo que fields
não incluídos explicitamente no Helper, o helper o incluiria implicitamente,
e assim o era, de acordo com commit acima do django-crispy-forms, então
cria-se essa classe:
class SaplFormHelper(FormHelper):
render_hidden_fields = True
onde torna o default, antes False, agora = True, o esperado pelos forms do sapl,
e substituí-se todos os FormHelper por SaplFormHelper dentro do projeto Sapl
esta explicação ficará aqui dentro do código, via commit, e na issue #2456.
"""
class SaplFormLayout(Layout):
def __init__(self, *fields, cancel_label=_('Cancelar'),
@ -137,6 +166,7 @@ def get_field_display(obj, fieldname):
value)
elif 'TextField' in str_type_from_field:
display = value.replace('\n', '<br/>')
display = '<div class="dont-break-out">{}</div>'.format(display)
else:
display = str(value)
return verbose_name, display
@ -188,8 +218,11 @@ class CrispyLayoutFormMixin:
pass
else:
if self.layout_key:
form.helper = FormHelper()
form.helper.layout = SaplFormLayout(*self.get_layout())
form.helper = SaplFormHelper()
layout = self.get_layout()
form.helper.layout = SaplFormLayout(*layout)
return form
@property

16
sapl/crud/base.py

@ -2,7 +2,6 @@ import logging
from braces.views import FormMessagesMixin
from crispy_forms.bootstrap import FieldWithButtons, StrictButton
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Field, Layout
from django import forms
from django.conf.urls import url
@ -25,6 +24,7 @@ from django.views.generic.base import ContextMixin
from django.views.generic.list import MultipleObjectMixin
from sapl.crispy_layout_mixin import CrispyLayoutFormMixin, get_field_display
from sapl.crispy_layout_mixin import SaplFormHelper
from sapl.rules.map_rules import (RP_ADD, RP_CHANGE, RP_DELETE, RP_DETAIL,
RP_LIST)
from sapl.settings import BASE_DIR
@ -150,7 +150,7 @@ class ListWithSearchForm(forms.Form):
def __init__(self, *args, **kwargs):
super(ListWithSearchForm, self).__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper = SaplFormHelper()
self.form_class = 'form-inline'
self.helper.form_method = 'GET'
self.helper.layout = Layout(
@ -449,18 +449,28 @@ class CrudListView(PermissionRequiredContainerCrudMixin, ListView):
if not n:
s += '<br>'
continue
m = obj
n = n.split('__')
for f in n[:-1]:
m = getattr(m, f)
if not m:
break
ss = ''
if m:
ss = get_field_display(m, n[-1])[1]
ss = (
('<br>' if '<ul>' in ss else ' - ') + ss)\
if ss and j != 0 and s else ss
hook = 'hook_{}'.format(''.join(n))
if hasattr(self, hook):
hs, url = getattr(self, hook)(obj, ss, url)
s += str(hs)
else:
s += ss
r.append((s, url))
return r
@ -571,6 +581,8 @@ class CrudListView(PermissionRequiredContainerCrudMixin, ListView):
rmo = rmo[0]
if not isinstance(rmo, str):
rmo = rmo[0]
if rmo.startswith('-'):
rmo = rmo[1:]
fo = '%s__%s' % (fo, rmo)
fo = desc + fo

4
sapl/legacy/test_renames.py

@ -13,9 +13,9 @@ from sapl.materia.models import (AcompanhamentoMateria, DocumentoAcessorio,
from sapl.norma.models import (AnexoNormaJuridica, NormaJuridica,
NormaRelacionada, TipoVinculoNormaJuridica)
from sapl.parlamentares.models import (Frente, Mandato, Parlamentar, Partido,
TipoAfastamento, Votante)
TipoAfastamento, Votante, Bloco)
from sapl.protocoloadm.models import DocumentoAdministrativo
from sapl.sessao.models import (Bancada, Bloco, CargoBancada,
from sapl.sessao.models import (Bancada, CargoBancada,
ExpedienteMateria, Orador, OradorExpediente,
OrdemDia, RegistroVotacao, ResumoOrdenacao,
SessaoPlenaria, TipoResultadoVotacao,

315
sapl/lexml/OAIServer.py

@ -0,0 +1,315 @@
import unicodedata
from datetime import datetime
import oaipmh
import oaipmh.error
import oaipmh.metadata
import oaipmh.server
from django.urls import reverse
from lxml import etree
from lxml.builder import ElementMaker
from sapl.base.models import AppConfig, CasaLegislativa
from sapl.lexml.models import LexmlPublicador, LexmlProvedor
from sapl.norma.models import NormaJuridica
from sapl.utils import LISTA_DE_UFS
class OAILEXML:
"""
Padrao OAI do LeXML
Esta registrado sobre o nome 'oai_lexml'
"""
def __init__(self, prefix):
self.prefix = prefix
self.ns = {'oai_lexml': 'http://www.lexml.gov.br/oai_lexml', }
self.schemas = {'oai_lexml': 'http://projeto.lexml.gov.br/esquemas/oai_lexml.xsd'}
def __call__(self, element, metadata):
data = metadata.record
if data.get('metadata'):
value = etree.XML(data['metadata'])
element.append(value)
class OAIServer:
"""
An OAI-2.0 compliant oai server.
Underlying code is based on pyoai's oaipmh.server'
"""
XSI_NS = 'http://www.w3.org/2001/XMLSchema-instance'
ns = {'lexml': 'http://www.lexml.gov.br/oai_lexml'}
schema = {'oai_lexml': 'http://projeto.lexml.gov.br/esquemas/oai_lexml.xsd'}
def __init__(self, config={}):
self.config = config
def identify(self):
result = oaipmh.common.Identify(
repositoryName=self.config['titulo'],
baseURL=self.config['base_url'],
protocolVersion='2.0',
adminEmails=self.config['email'],
earliestDatestamp=datetime(2001, 1, 1, 10, 00),
deletedRecord='transient',
granularity='YYYY-MM-DDThh:mm:ssZ',
compression=['identity'],
toolkit_description=False)
if self.config.get('descricao'):
result.add_description(self.config['descricao'])
return result
def create_header_and_metadata(self, record):
header = self.create_header(record)
metadata = oaipmh.common.Metadata(None, record['metadata'])
metadata.record = record
return header, metadata
def list_query(self, from_=None, until=None, offset=0, batch_size=10, identifier=None):
if identifier:
identifier = int(identifier.split('/')[-1]) # Get internal id
else:
identifier = ''
until = datetime.now() if not until or until > datetime.now() else until
return self.oai_query(offset=offset, batch_size=batch_size, from_=from_, until=until,
identifier=identifier)
def check_metadata_prefix(self, metadata_prefix):
if not metadata_prefix in self.config['metadata_prefixes']:
raise oaipmh.error.CannotDisseminateFormatError
def listRecords(self, metadataPrefix, from_=None, until=None, cursor=0, batch_size=10):
self.check_metadata_prefix(metadataPrefix)
for record in self.list_query(from_, until, cursor, batch_size):
header, metadata = self.create_header_and_metadata(record)
yield header, metadata, None # None?
def get_oai_id(self, internal_id):
return "oai:{}".format(internal_id)
def create_header(self, record):
oai_id = self.get_oai_id(record['record']['id'])
timestamp = record['record']['when_modified'] if record['record']['when_modified'] else datetime.now()
timestamp = timestamp.replace(tzinfo=None)
sets = []
deleted = record['record']['deleted']
return oaipmh.common.Header(None, oai_id, timestamp, sets, deleted)
def get_esfera_federacao(self):
appconfig = AppConfig.objects.first()
return appconfig.esfera_federacao
def recupera_norma(self, offset, batch_size, from_, until, identifier, esfera):
kwargs = {'data__lte': until}
if from_:
kwargs['data__gte'] = from_
if identifier:
kwargs['numero'] = identifier
if esfera:
kwargs['esfera_federacao'] = esfera
return NormaJuridica.objects.select_related('tipo').filter(**kwargs)[offset:offset + batch_size]
def monta_id(self, norma):
if norma:
num = len(casa.endereco_web.split('.'))
dominio = '.'.join(casa.endereco_web.split('.')[1:num])
prefixo_oai = '{}.{}:sapl/'.format(casa.sigla.lower(), dominio)
numero_interno = norma.numero
tipo_norma = norma.tipo.equivalente_lexml
ano_norma = norma.ano
identificador = '{}{};{};{}'.format(prefixo_oai, tipo_norma, ano_norma, numero_interno)
return identificador
else:
return None
@staticmethod
def remove_acentos(linha):
res = unicodedata.normalize('NFKD', linha).encode('ASCII', 'ignore')
res = res.decode("UTF-8")
remove_list = ["\'", "\"", "-"]
for i in remove_list:
res = res.replace(i, "")
return res
def monta_urn(self, norma, esfera):
if norma:
urn = 'urn:lex:br;'
esferas = {'M': 'municipal', 'E': 'estadual'}
municipio = self.remove_acentos(casa.municipio.lower())
uf_map = dict(LISTA_DE_UFS)
uf_desc = uf_map.get(casa.uf.upper(), '').lower()
uf_desc = self.remove_acentos(uf_desc)
for x in [' ', '.de.', '.da.', '.das.', '.do.', '.dos.']:
municipio = municipio.replace(x, '.')
uf_desc = uf_desc.replace(x, '.')
if esfera == 'M':
urn += '{};{}:'.format(uf_desc, municipio)
if norma.tipo.equivalente_lexml == 'regimento.interno' or norma.tipo.equivalente_lexml == 'resolucao':
urn += 'camara.'
urn += esferas[esfera] + ':'
elif esfera == 'E':
urn += '{}:{}:'.format(uf_desc, esferas[esfera])
else:
urn += ':'
if norma.tipo.equivalente_lexml:
urn += '{}:{};'.format(norma.tipo.equivalente_lexml, norma.data.isoformat())
else:
urn += '{};'.format(norma.data.isoformat())
if norma.tipo.equivalente_lexml == 'lei.organica' or norma.tipo.equivalente_lexml == 'constituicao':
urn += str(norma.ano)
else:
urn += str(norma.numero)
if norma.data_vigencia and norma.data_publicacao:
urn += '@{};publicacao;{}'.format(norma.data_vigencia.isoformat(), norma.data_publicacao.isoformat())
elif norma.data_publicacao:
urn += '@inicio.vigencia;publicacao;{}'.format(norma.data_publicacao.isoformat())
return urn
else:
return None
def data_por_extenso(self, data):
data = data.strftime('%d-%m-%Y')
if data != '':
meses = {1: 'Janeiro', 2: 'Fevereiro', 3: 'Março', 4: 'Abril', 5: 'Maio', 6: 'Junho', 7: 'Julho',
8: 'Agosto', 9: 'Setembro', 10: 'Outubro', 11: 'Novembro', 12: 'Dezembro'}
return '{} de {} de {}'.format(data[0:2], meses[int(data[3:5])], data[6:])
else:
return ''
def monta_xml(self, urn, norma):
BASE_URL_SAPL = self.config['base_url']
BASE_URL_SAPL = BASE_URL_SAPL[:BASE_URL_SAPL.find('/', 8)]
publicador = LexmlPublicador.objects.first()
if norma and publicador:
LEXML = ElementMaker(namespace=self.ns['lexml'], nsmap=self.ns)
oai_lexml = LEXML.LexML()
oai_lexml.attrib['{{{pre}}}schemaLocation'.format(pre=self.XSI_NS)] = '{} {}'.format(
'http://www.lexml.gov.br/oai_lexml', 'http://projeto.lexml.gov.br/esquemas/oai_lexml.xsd')
texto_integral = norma.texto_integral
mime_types = {'doc': 'application/msword',
'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'odt': 'application/vnd.oasis.opendocument.text',
'pdf': 'application/pdf',
'rtf': 'application/rtf'}
if texto_integral:
url_conteudo = BASE_URL_SAPL + texto_integral.url
extensao = texto_integral.url.split('.')[-1]
formato = mime_types.get(extensao, 'application/octet-stream')
else:
formato = 'text/html'
url_conteudo = BASE_URL_SAPL + reverse('sapl.norma:normajuridica_detail',
kwargs={'pk': norma.pk})
element_maker = ElementMaker()
id_publicador = str(publicador.id_publicador)
item_conteudo = element_maker.Item(url_conteudo, formato=formato, idPublicador=id_publicador,
tipo='conteudo')
oai_lexml.append(item_conteudo)
url = BASE_URL_SAPL + reverse('sapl.norma:normajuridica_detail', kwargs={'pk': norma.pk})
item_metadado = element_maker.Item(url, formato='text/html', idPublicador=id_publicador, tipo='metadado')
oai_lexml.append(item_metadado)
documento_individual = element_maker.DocumentoIndividual(urn)
oai_lexml.append(documento_individual)
if norma.tipo.equivalente_lexml == 'lei.organica':
epigrafe = '{} de {} - {}, de {}'.format(norma.tipo.descricao, casa.municipio,
casa.uf, norma.ano)
elif norma.tipo.equivalente_lexml == 'constituicao':
epigrafe = '{} do Estado de {}, de {}'.format(norma.tipo.descricao, casa.municipio,
norma.ano)
else:
epigrafe = '{}{}, de {}'.format(norma.tipo.descricao, norma.numero,
self.data_por_extenso(norma.data))
oai_lexml.append(element_maker.Epigrafe(epigrafe))
oai_lexml.append(element_maker.Ementa(norma.ementa))
indexacao = norma.indexacao
if indexacao:
oai_lexml.append(element_maker.Indexacao(indexacao))
return etree.tostring(oai_lexml)
else:
return None
def oai_query(self, offset=0, batch_size=10, from_=None, until=None, identifier=None):
esfera = self.get_esfera_federacao()
offset = 0 if offset < 0 else offset
batch_size = 10 if batch_size < 0 else batch_size
until = datetime.now() if not until or until > datetime.now() else until
normas = self.recupera_norma(offset, batch_size, from_, until, identifier, esfera)
for norma in normas:
resultado = {}
identificador = self.monta_id(norma)
urn = self.monta_urn(norma, esfera)
xml_lexml = self.monta_xml(urn, norma)
resultado['tx_metadado_xml'] = xml_lexml
resultado['cd_status'] = 'N'
resultado['id'] = identificador
resultado['when_modified'] = norma.timestamp
resultado['deleted'] = 0
yield {'record': resultado,
'metadata': resultado['tx_metadado_xml']}
def OAIServerFactory(config={}):
"""
Create a new OAI batching OAI Server given a config and a database
"""
for prefix in config['metadata_prefixes']:
metadata_registry = oaipmh.metadata.MetadataRegistry()
metadata_registry.registerWriter(prefix, OAILEXML(prefix))
return oaipmh.server.BatchingServer(
OAIServer(config),
metadata_registry=metadata_registry,
resumption_batch_size=config['batch_size']
)
casa = None
def casa_legislativa():
global casa
if not casa:
casa = CasaLegislativa.objects.first()
return casa if casa else CasaLegislativa() # retorna objeto dummy
def get_xml_provedor():
""" antigo get_descricao_casa() """
descricao = ''
provedor = LexmlProvedor.objects.first()
if provedor:
descricao = provedor.xml
if descricao:
descricao = descricao.encode('utf-8')
return descricao
def get_config(url, batch_size):
config = {'content_type': None,
'delay': 0,
'base_asset_path': None,
'metadata_prefixes': ['oai_lexml'],
'titulo': casa_legislativa().nome, # Inicializa variável global casa
'email': [casa.email], # lista de e-mails, antigo `def get_email()`
'base_url': url[:url.find('/', 8)] + reverse('sapl.lexml:lexml_endpoint')[:-4], # remove '/oai' suffix
'descricao': get_xml_provedor(),
'batch_size': batch_size
}
return config
if __name__ == '__main__':
"""
Para executar localmente (estando no diretório raiz):
$ ./manage.py shell_plus
Executar comando
%run sapl/lexml/OAIServer.py
"""
oai_server = OAIServerFactory(get_config('http://127.0.0.1:8000/', 10))
r = oai_server.handleRequest({'verb': 'ListRecords',
'metadataPrefix': 'oai_lexml'})
print(r.decode('UTF-8'))

6
sapl/lexml/models.py

@ -23,6 +23,12 @@ class LexmlProvedor(models.Model): # LexmlRegistroProvedor
blank=True,
verbose_name=_('XML fornecido pela equipe do LexML:'))
@property
def pretty_xml(self):
import html
safe_xml = html.escape(self.xml)
return safe_xml.replace('\n', '<br/>').replace(' ', '&nbsp;')
class Meta:
verbose_name = _('Provedor Lexml')
verbose_name_plural = _('Provedores Lexml')

5
sapl/lexml/urls.py

@ -1,6 +1,6 @@
from django.conf.urls import include, url
from sapl.lexml.views import LexmlProvedorCrud, LexmlPublicadorCrud
from sapl.lexml.views import LexmlProvedorCrud, LexmlPublicadorCrud, lexml_request, request_search
from .apps import AppConfig
@ -11,4 +11,7 @@ urlpatterns = [
include(LexmlProvedorCrud.get_urls())),
url(r'^sistema/lexml/publicador/',
include(LexmlPublicadorCrud.get_urls())),
url(r'^sistema/lexml/request_search/(?P<keyword>[\w\-]+)/', request_search, name='lexml_search'),
url(r'^sistema/lexml/oai', lexml_request, name='lexml_endpoint'),
]

33
sapl/lexml/views.py

@ -1,6 +1,35 @@
from sapl.crud.base import CrudAux
from django.http import HttpResponse
from django.shortcuts import render
from sapl.crud.base import CrudAux, Crud
from sapl.lexml.OAIServer import OAIServerFactory, get_config
from sapl.rules import RP_DETAIL, RP_LIST
from .models import LexmlProvedor, LexmlPublicador
LexmlProvedorCrud = CrudAux.build(LexmlProvedor, 'lexml_provedor')
LexmlPublicadorCrud = CrudAux.build(LexmlPublicador, 'lexml_publicador')
class LexmlProvedorCrud(Crud):
model = LexmlProvedor
help_topic = 'lexml_provedor'
public = [RP_LIST, RP_DETAIL]
class DetailView(Crud.DetailView):
layout_key = 'LexmlProvedorDetail'
def lexml_request(request):
request_dict = request.GET.copy()
if request_dict.get('batch_size'):
del request_dict['batch_size']
config = get_config(request.get_raw_uri(), int(request.GET.get('batch_size', '10')))
oai_server = OAIServerFactory(config)
r = oai_server.handleRequest(request_dict)
response = r.decode('UTF-8')
return HttpResponse(response, content_type='text/xml')
def request_search(request, keyword):
return render(request, "lexml/resultado-pesquisa.html", {"keyword": keyword})

457
sapl/materia/forms.py

@ -3,7 +3,6 @@ import logging
import os
from crispy_forms.bootstrap import Alert, InlineRadios
from crispy_forms.helper import FormHelper
from crispy_forms.layout import (HTML, Button, Column, Div, Field, Fieldset,
Layout, Row)
from django import forms
@ -26,26 +25,28 @@ import django_filters
import sapl
from sapl.base.models import AppConfig, Autor, TipoAutor
from sapl.comissoes.models import Comissao
from sapl.comissoes.models import Comissao, Participacao, Composicao
from sapl.compilacao.models import (STATUS_TA_IMMUTABLE_PUBLIC,
STATUS_TA_PRIVATE)
from sapl.crispy_layout_mixin import (SaplFormLayout, form_actions, to_column,
to_row)
from sapl.crispy_layout_mixin import SaplFormHelper
from sapl.materia.models import (AssuntoMateria, Autoria, MateriaAssunto,
MateriaLegislativa, Orgao, RegimeTramitacao,
TipoDocumento, TipoProposicao, StatusTramitacao,
UnidadeTramitacao)
from sapl.norma.models import (LegislacaoCitada, NormaJuridica,
TipoNormaJuridica)
from sapl.parlamentares.models import Legislatura, Partido
from sapl.protocoloadm.models import Protocolo, DocumentoAdministrativo
from sapl.parlamentares.models import Legislatura, Partido, Parlamentar
from sapl.protocoloadm.models import Protocolo, DocumentoAdministrativo, Anexado
from sapl.settings import MAX_DOC_UPLOAD_SIZE
from sapl.utils import (YES_NO_CHOICES, SEPARADOR_HASH_PROPOSICAO,
ChoiceWithoutValidationField,
MateriaPesquisaOrderingFilter, RangeWidgetOverride,
autor_label, autor_modal, gerar_hash_arquivo,
models_with_gr_for_model, qs_override_django_filter,
choice_anos_com_materias, FilterOverridesMetaMixin)
choice_anos_com_materias, FilterOverridesMetaMixin, FileFieldCheckMixin,
lista_anexados)
from .models import (AcompanhamentoMateria, Anexada, Autoria, DespachoInicial,
DocumentoAcessorio, Numeracao, Proposicao, Relatoria,
@ -76,7 +77,7 @@ class AdicionarVariasAutoriasFilterSet(django_filters.FilterSet):
row1 = to_row([('nome', 12)])
self.form.helper = FormHelper()
self.form.helper = SaplFormHelper()
self.form.helper.form_method = 'GET'
self.form.helper.layout = Layout(
Fieldset(_('Filtrar Autores'),
@ -112,7 +113,7 @@ class ReceberProposicaoForm(Form):
def __init__(self, *args, **kwargs):
row1 = to_row([('cod_hash', 12)])
self.helper = FormHelper()
self.helper = SaplFormHelper()
self.helper.layout = Layout(
Fieldset(
_('Incorporar Proposição'), row1,
@ -122,7 +123,7 @@ class ReceberProposicaoForm(Form):
super(ReceberProposicaoForm, self).__init__(*args, **kwargs)
class MateriaSimplificadaForm(ModelForm):
class MateriaSimplificadaForm(FileFieldCheckMixin, ModelForm):
logger = logging.getLogger(__name__)
@ -145,7 +146,7 @@ class MateriaSimplificadaForm(ModelForm):
row4 = to_row([('ementa', 12)])
row5 = to_row([('texto_original', 12)])
self.helper = FormHelper()
self.helper = SaplFormHelper()
self.helper.layout = Layout(
Fieldset(
_('Formulário Simplificado'),
@ -175,7 +176,7 @@ class MateriaSimplificadaForm(ModelForm):
return cleaned_data
class MateriaLegislativaForm(ModelForm):
class MateriaLegislativaForm(FileFieldCheckMixin, ModelForm):
logger = logging.getLogger(__name__)
@ -204,7 +205,7 @@ class MateriaLegislativaForm(ModelForm):
widget=forms.HiddenInput())
self.fields['autor'] = forms.CharField(required=False,
widget=forms.HiddenInput())
if kwargs['instance'].numero_protocolo:
if kwargs['instance'].numero_protocolo and Protocolo.objects.filter(numero=kwargs['instance'].numero_protocolo, ano=kwargs['instance'].ano).exists():
self.fields['numero_protocolo'].widget.attrs['readonly'] = True
def clean(self):
@ -340,22 +341,20 @@ class AcompanhamentoMateriaForm(ModelForm):
def __init__(self, *args, **kwargs):
row1 = to_row([('email', 10)])
row1 = to_row([('email', 12)])
row1.append(
Column(form_actions(label='Cadastrar'), css_class='col-md-2')
)
self.helper = FormHelper()
self.helper = SaplFormHelper()
self.helper.layout = Layout(
Fieldset(
_('Acompanhamento de Matéria por e-mail'), row1
_('Acompanhamento de Matéria por e-mail'),
row1,
form_actions(label='Cadastrar')
)
)
super(AcompanhamentoMateriaForm, self).__init__(*args, **kwargs)
class DocumentoAcessorioForm(ModelForm):
class DocumentoAcessorioForm(FileFieldCheckMixin, ModelForm):
data = forms.DateField(required=True)
class Meta:
@ -364,39 +363,83 @@ class DocumentoAcessorioForm(ModelForm):
class RelatoriaForm(ModelForm):
logger = logging.getLogger(__name__)
composicao = forms.ModelChoiceField(
required=True,
empty_label='---------',
queryset=Composicao.objects.all(),
label=_('Composição')
)
class Meta:
model = Relatoria
fields = ['data_designacao_relator', 'comissao', 'parlamentar',
'data_destituicao_relator', 'tipo_fim_relatoria']
fields = [
'comissao',
'data_designacao_relator',
'data_destituicao_relator',
'tipo_fim_relatoria',
'composicao',
'parlamentar'
]
widgets = {'comissao': forms.Select(attrs={'disabled': 'disabled'})}
def __init__(self, *args, **kwargs):
super(RelatoriaForm, self).__init__(*args, **kwargs)
row1 = to_row([('comissao', 12)])
row2 = to_row([('data_designacao_relator', 4),
('data_destituicao_relator', 4),
('tipo_fim_relatoria', 4)])
row3 = to_row([('composicao', 4),
('parlamentar', 8)])
def clean(self):
super(RelatoriaForm, self).clean()
self.helper = SaplFormHelper()
self.helper.layout = SaplFormLayout(
Fieldset(_('Relatoria'), row1, row2, row3))
if not self.is_valid():
return self.cleaned_data
super().__init__(*args, **kwargs)
comissao_pk = kwargs['initial']['comissao']
composicoes = Composicao.objects.filter(comissao_id=comissao_pk)
self.fields['composicao'].choices = [('', '---------')] + \
[(c.pk, c) for c in composicoes]
# UPDATE
if self.initial.get('composicao') and self.initial.get('parlamentar'):
parlamentares = [(p.parlamentar.id, p.parlamentar) for p in
Participacao.objects.filter(composicao__comissao_id=comissao_pk,
composicao_id=self.initial['composicao'])]
self.fields['parlamentar'].choices = [
('', '---------')] + parlamentares
# INSERT
else:
self.fields['parlamentar'].choices = [('', '---------')]
def clean(self):
super().clean()
cleaned_data = self.cleaned_data
if not self.is_valid():
return cleaned_data
try:
self.logger.debug("Tentando obter objeto Comissao.")
comissao = Comissao.objects.get(id=self.initial['comissao'])
except ObjectDoesNotExist as e:
self.logger.error("Objeto Comissao não encontrado com id={} "
".A localização atual deve ser uma comissão. "
.format(self.initial['comissao']) + str(e))
self.logger.error(
"Objeto Comissao não encontrado com id={}. A localização atual deve ser uma comissão. ".format(
self.initial['comissao']) + str(e))
msg = _('A localização atual deve ser uma comissão.')
raise ValidationError(msg)
else:
cleaned_data['comissao'] = comissao
if cleaned_data['data_designacao_relator'] < cleaned_data['composicao'].periodo.data_inicio \
or cleaned_data['data_designacao_relator'] > cleaned_data['composicao'].periodo.data_fim:
raise ValidationError(
_('Data de designação deve estar dentro do período da composição.'))
return cleaned_data
@ -419,11 +462,23 @@ class TramitacaoForm(ModelForm):
'unidade_tramitacao_destino',
'data_encaminhamento',
'data_fim_prazo',
'texto']
'texto',
'user',
'ip']
widgets = {'user': forms.HiddenInput(),
'ip': forms.HiddenInput()}
def __init__(self, *args, **kwargs):
super(TramitacaoForm, self).__init__(*args, **kwargs)
self.fields['data_tramitacao'].initial = timezone.now().date()
ust = UnidadeTramitacao.objects.select_related().all()
unidade_tramitacao_destino = [('', '---------')] + [(ut.pk, ut)
for ut in ust if ut.comissao and ut.comissao.ativa]
unidade_tramitacao_destino.extend(
[(ut.pk, ut) for ut in ust if ut.orgao])
unidade_tramitacao_destino.extend(
[(ut.pk, ut) for ut in ust if ut.parlamentar])
self.fields['unidade_tramitacao_destino'].choices = unidade_tramitacao_destino
def clean(self):
super(TramitacaoForm, self).clean()
@ -495,6 +550,49 @@ class TramitacaoForm(ModelForm):
return cleaned_data
@transaction.atomic
def save(self, commit=True):
tramitacao = super(TramitacaoForm, self).save(commit)
materia = tramitacao.materia
materia.em_tramitacao = False if tramitacao.status.indicador == "F" else True
materia.save()
lista_tramitacao = []
lista_anexadas = lista_anexados(materia)
for ma in lista_anexadas:
if not ma.tramitacao_set.all() \
or ma.tramitacao_set.last().unidade_tramitacao_destino == tramitacao.unidade_tramitacao_local:
ma.em_tramitacao = False if tramitacao.status.indicador == "F" else True
ma.save()
lista_tramitacao.append(Tramitacao(
status=tramitacao.status,
materia=ma,
data_tramitacao=tramitacao.data_tramitacao,
unidade_tramitacao_local=tramitacao.unidade_tramitacao_local,
data_encaminhamento=tramitacao.data_encaminhamento,
unidade_tramitacao_destino=tramitacao.unidade_tramitacao_destino,
urgente=tramitacao.urgente,
turno=tramitacao.turno,
texto=tramitacao.texto,
data_fim_prazo=tramitacao.data_fim_prazo,
user=tramitacao.user,
ip=tramitacao.ip
))
Tramitacao.objects.bulk_create(lista_tramitacao)
return tramitacao
# Compara se os campos de duas tramitações são iguais,
# exceto os campos id, documento_id e timestamp
def compara_tramitacoes_mat(tramitacao1, tramitacao2):
if not tramitacao1 or not tramitacao2:
return False
lst_items = ['id', 'materia_id', 'timestamp']
values = [(k,v) for k,v in tramitacao1.__dict__.items() if ((k not in lst_items) and (k[0] != '_'))]
other_values = [(k,v) for k,v in tramitacao2.__dict__.items() if (k not in lst_items and k[0] != '_')]
return values == other_values
class TramitacaoUpdateForm(TramitacaoForm):
unidade_tramitacao_local = forms.ModelChoiceField(
@ -516,11 +614,15 @@ class TramitacaoUpdateForm(TramitacaoForm):
'data_encaminhamento',
'data_fim_prazo',
'texto',
'user',
'ip'
]
widgets = {
'data_encaminhamento': forms.DateInput(format='%d/%m/%Y'),
'data_fim_prazo': forms.DateInput(format='%d/%m/%Y'),
'user': forms.HiddenInput(),
'ip': forms.HiddenInput()
}
def clean(self):
@ -529,33 +631,73 @@ class TramitacaoUpdateForm(TramitacaoForm):
if not self.is_valid():
return self.cleaned_data
cd = self.cleaned_data
obj = self.instance
ultima_tramitacao = Tramitacao.objects.filter(
materia_id=self.instance.materia_id).order_by(
materia_id=obj.materia_id).order_by(
'-data_tramitacao',
'-id').first()
# Se a Tramitação que está sendo editada não for a mais recente,
# ela não pode ter seu destino alterado.
if ultima_tramitacao != self.instance:
if self.cleaned_data['unidade_tramitacao_destino'] != \
self.instance.unidade_tramitacao_destino:
if ultima_tramitacao != obj:
if cd['unidade_tramitacao_destino'] != \
obj.unidade_tramitacao_destino:
self.logger.error("Você não pode mudar a Unidade de Destino desta "
"tramitação para {}, pois irá conflitar com a Unidade "
"Local da tramitação seguinte ({})."
.format(self.cleaned_data['unidade_tramitacao_destino'],
self.instance.unidade_tramitacao_destino))
.format(cd['unidade_tramitacao_destino'],
obj.unidade_tramitacao_destino))
raise ValidationError(
'Você não pode mudar a Unidade de Destino desta '
'tramitação, pois irá conflitar com a Unidade '
'Local da tramitação seguinte')
self.cleaned_data['data_tramitacao'] = \
self.instance.data_tramitacao
self.cleaned_data['unidade_tramitacao_local'] = \
self.instance.unidade_tramitacao_local
# Se não houve qualquer alteração em um dos dados, mantém o usuário e ip
if not (cd['data_tramitacao'] != obj.data_tramitacao or \
cd['unidade_tramitacao_destino'] != obj.unidade_tramitacao_destino or \
cd['status'] != obj.status or cd['texto'] != obj.texto or \
cd['data_encaminhamento'] != obj.data_encaminhamento or \
cd['data_fim_prazo'] != obj.data_fim_prazo or \
cd['urgente'] != obj.urgente or \
cd['turno'] != obj.turno):
cd['user'] = obj.user
cd['ip'] = obj.ip
return self.cleaned_data
cd['data_tramitacao'] = obj.data_tramitacao
cd['unidade_tramitacao_local'] = obj.unidade_tramitacao_local
return cd
@transaction.atomic
def save(self, commit=True):
ant_tram_principal = Tramitacao.objects.get(id=self.instance.id)
nova_tram_principal = super(TramitacaoUpdateForm, self).save(commit)
materia = nova_tram_principal.materia
materia.em_tramitacao = False if nova_tram_principal.status.indicador == "F" else True
materia.save()
lista_anexadas = lista_anexados(materia)
for ma in lista_anexadas:
tram_anexada = ma.tramitacao_set.last()
if compara_tramitacoes_mat(ant_tram_principal, tram_anexada):
tram_anexada.status = nova_tram_principal.status
tram_anexada.data_tramitacao = nova_tram_principal.data_tramitacao
tram_anexada.unidade_tramitacao_local = nova_tram_principal.unidade_tramitacao_local
tram_anexada.data_encaminhamento = nova_tram_principal.data_encaminhamento
tram_anexada.unidade_tramitacao_destino = nova_tram_principal.unidade_tramitacao_destino
tram_anexada.urgente = nova_tram_principal.urgente
tram_anexada.turno = nova_tram_principal.turno
tram_anexada.texto = nova_tram_principal.texto
tram_anexada.data_fim_prazo = nova_tram_principal.data_fim_prazo
tram_anexada.user = nova_tram_principal.user
tram_anexada.ip = nova_tram_principal.ip
tram_anexada.save()
ma.em_tramitacao = False if nova_tram_principal.status.indicador == "F" else True
ma.save()
return nova_tram_principal
class LegislacaoCitadaForm(ModelForm):
@ -710,7 +852,7 @@ class AnexadaForm(ModelForm):
empty_label='Selecione',
)
numero = forms.CharField(label='Número', required=True)
numero = forms.IntegerField(label='Número', required=True)
ano = forms.CharField(label='Ano', required=True)
@ -726,6 +868,13 @@ class AnexadaForm(ModelForm):
cleaned_data = self.cleaned_data
data_anexacao = cleaned_data['data_anexacao']
data_desanexacao = cleaned_data['data_desanexacao'] if cleaned_data['data_desanexacao'] else data_anexacao
if data_anexacao > data_desanexacao:
self.logger.error("Data de anexação posterior à data de desanexação.")
raise ValidationError(_("Data de anexação posterior à data de desanexação."))
try:
self.logger.info("Tentando obter objeto MateriaLegislativa (numero={}, ano={}, tipo={})."
.format(cleaned_data['numero'], cleaned_data['ano'], cleaned_data['tipo']))
@ -734,8 +883,8 @@ class AnexadaForm(ModelForm):
ano=cleaned_data['ano'],
tipo=cleaned_data['tipo'])
except ObjectDoesNotExist:
msg = _('A MateriaLegislativa a ser anexada (numero={}, ano={}, tipo={}) não existe no cadastro'
' de matérias legislativas.'.format(cleaned_data['numero'], cleaned_data['ano'], cleaned_data['tipo']))
msg = _('A {} {}/{} não existe no cadastro de matérias legislativas.'
.format(cleaned_data['tipo'], cleaned_data['numero'], cleaned_data['ano']))
self.logger.error("A matéria a ser anexada não existe no cadastro"
" de matérias legislativas.")
raise ValidationError(msg)
@ -745,13 +894,35 @@ class AnexadaForm(ModelForm):
self.logger.error("Matéria não pode ser anexada a si mesma.")
raise ValidationError(_('Matéria não pode ser anexada a si mesma'))
is_anexada = Anexada.objects.filter(materia_principal=materia_principal,
is_anexada = Anexada.objects.filter(
materia_principal=materia_principal,
materia_anexada=materia_anexada
).exists()
).exclude(pk=self.instance.pk).exists()
if is_anexada:
self.logger.error("Matéria já se encontra anexada.")
raise ValidationError(_('Matéria já se encontra anexada'))
ciclico = False
anexadas_anexada = Anexada.objects.filter(materia_principal=materia_anexada)
while anexadas_anexada and not ciclico:
anexadas = []
for anexa in anexadas_anexada:
if materia_principal == anexa.materia_anexada:
ciclico = True
else:
for a in Anexada.objects.filter(materia_principal=anexa.materia_anexada):
anexadas.append(a)
anexadas_anexada = anexadas
if ciclico:
self.logger.error("A matéria não pode ser anexada por uma de suas anexadas.")
raise ValidationError(_("A matéria não pode ser anexada por uma de suas anexadas."))
cleaned_data['materia_anexada'] = materia_anexada
return cleaned_data
@ -896,7 +1067,7 @@ class MateriaLegislativaFilterSet(django_filters.FilterSet):
('tipo_listagem', 4)
])
self.form.helper = FormHelper()
self.form.helper = SaplFormHelper()
self.form.helper.form_method = 'GET'
self.form.helper.layout = Layout(
Fieldset(_('Pesquisa Básica'),
@ -1016,7 +1187,7 @@ class AutoriaForm(ModelForm):
('autor', 4),
('primeiro_autor', 4)])
self.helper = FormHelper()
self.helper = SaplFormHelper()
self.helper.layout = Layout(
Fieldset(_('Autoria'),
row1, 'data_relativa', form_actions(label='Salvar')))
@ -1077,7 +1248,7 @@ class AutoriaMultiCreateForm(Form):
row2 = to_row([('autor', 12), ])
self.helper = FormHelper()
self.helper = SaplFormHelper()
self.helper.layout = Layout(
Fieldset(
_('Autorias'), row1, row2, 'data_relativa', 'autores',
@ -1117,13 +1288,35 @@ class AcessorioEmLoteFilterSet(django_filters.FilterSet):
row1 = to_row([('tipo', 12)])
row2 = to_row([('data_apresentacao', 12)])
self.form.helper = FormHelper()
self.form.helper = SaplFormHelper()
self.form.helper.form_method = 'GET'
self.form.helper.layout = Layout(
Fieldset(_('Documentos Acessórios em Lote'),
row1, row2, form_actions(label='Pesquisar')))
class AnexadaEmLoteFilterSet(django_filters.FilterSet):
class Meta(FilterOverridesMetaMixin):
model = MateriaLegislativa
fields = ['tipo', 'data_apresentacao']
def __init__(self, *args, **kwargs):
super(AnexadaEmLoteFilterSet, self).__init__(*args, **kwargs)
self.filters['tipo'].label = 'Tipo de Matéria'
self.filters['data_apresentacao'].label = 'Data (Inicial - Final)'
row1 = to_row([('tipo', 12)])
row2 = to_row([('data_apresentacao', 12)])
self.form.helper = SaplFormHelper()
self.form.helper.form_method = 'GET'
self.form.helper.layout = Layout(
Fieldset(_('Pesquisa de Matérias'),
row1, row2, form_actions(label='Pesquisar')))
class PrimeiraTramitacaoEmLoteFilterSet(django_filters.FilterSet):
class Meta(FilterOverridesMetaMixin):
@ -1142,7 +1335,7 @@ class PrimeiraTramitacaoEmLoteFilterSet(django_filters.FilterSet):
row1 = to_row([('tipo', 12)])
row2 = to_row([('data_apresentacao', 12)])
self.form.helper = FormHelper()
self.form.helper = SaplFormHelper()
self.form.helper.form_method = 'GET'
self.form.helper.layout = Layout(
Fieldset(_('Primeira Tramitação'),
@ -1177,7 +1370,7 @@ class TramitacaoEmLoteFilterSet(django_filters.FilterSet):
('tramitacao__status', 4)])
row2 = to_row([('data_apresentacao', 12)])
self.form.helper = FormHelper()
self.form.helper = SaplFormHelper()
self.form.helper.form_method = 'GET'
self.form.helper.layout = Layout(
Fieldset(_('Tramitação em Lote'),
@ -1241,7 +1434,7 @@ class TipoProposicaoForm(ModelForm):
)
)
self.helper = FormHelper()
self.helper = SaplFormHelper()
self.helper.layout = SaplFormLayout(tipo_select)
super(TipoProposicaoForm, self).__init__(*args, **kwargs)
@ -1331,7 +1524,7 @@ class TipoProposicaoSelect(Select):
return option
class ProposicaoForm(forms.ModelForm):
class ProposicaoForm(FileFieldCheckMixin, forms.ModelForm):
logger = logging.getLogger(__name__)
@ -1368,6 +1561,9 @@ class ProposicaoForm(forms.ModelForm):
widget=widgets.HiddenInput(),
required=False)
numero_materia_futuro = forms.IntegerField(
label='Número (Opcional)', required=False)
class Meta:
model = Proposicao
fields = ['tipo',
@ -1381,7 +1577,8 @@ class ProposicaoForm(forms.ModelForm):
'numero_materia',
'ano_materia',
'tipo_texto',
'hash_code']
'hash_code',
'numero_materia_futuro']
widgets = {
'descricao': widgets.Textarea(attrs={'rows': 4}),
@ -1389,10 +1586,10 @@ class ProposicaoForm(forms.ModelForm):
'hash_code': forms.HiddenInput(), }
def __init__(self, *args, **kwargs):
self.texto_articulado_proposicao = sapl.base.models.AppConfig.attr(
self.texto_articulado_proposicao = AppConfig.attr(
'texto_articulado_proposicao')
self.receber_recibo = sapl.base.models.AppConfig.attr(
self.receber_recibo = AppConfig.attr(
'receber_recibo_proposicao')
if not self.texto_articulado_proposicao:
@ -1414,6 +1611,12 @@ class ProposicaoForm(forms.ModelForm):
]
if AppConfig.objects.last().escolher_numero_materia_proposicao:
fields.append(to_column(('numero_materia_futuro', 12)),)
else:
if 'numero_materia_futuro' in self._meta.fields:
self._meta.fields.remove('numero_materia_futuro')
if self.texto_articulado_proposicao:
fields.append(
to_column((InlineRadios('tipo_texto'), 5)),)
@ -1431,7 +1634,7 @@ class ProposicaoForm(forms.ModelForm):
('ano_materia', 6)]),
), 12)),
)
self.helper = FormHelper()
self.helper = SaplFormHelper()
self.helper.layout = SaplFormLayout(*fields)
super(ProposicaoForm, self).__init__(*args, **kwargs)
@ -1490,6 +1693,15 @@ class ProposicaoForm(forms.ModelForm):
cd.get('ano_materia', ''),
cd.get('numero_materia', ''))
if cd['numero_materia_futuro'] and \
'tipo' in cd and \
MateriaLegislativa.objects.filter(tipo=cd['tipo'].tipo_conteudo_related,
ano=timezone.now().year,
numero=cd['numero_materia_futuro']):
raise ValidationError(_("A matéria {} {}/{} já existe.".format(cd['tipo'].tipo_conteudo_related.descricao,
cd['numero_materia_futuro'],
timezone.now().year)))
if tm and am and nm:
try:
self.logger.debug("Tentando obter objeto MateriaLegislativa (tipo_id={}, ano={}, numero={})."
@ -1532,9 +1744,14 @@ class ProposicaoForm(forms.ModelForm):
return super().save(commit)
inst.ano = timezone.now().year
sequencia_numeracao = AppConfig.attr('sequencia_numeracao_proposicao')
if sequencia_numeracao == 'A':
numero__max = Proposicao.objects.filter(
autor=inst.autor,
ano=timezone.now().year).aggregate(Max('numero_proposicao'))
elif sequencia_numeracao == 'B':
numero__max = Proposicao.objects.filter(
ano=timezone.now().year).aggregate(Max('numero_proposicao'))
numero__max = numero__max['numero_proposicao__max']
inst.numero_proposicao = (
numero__max + 1) if numero__max else 1
@ -1579,7 +1796,7 @@ class DevolverProposicaoForm(forms.ModelForm):
)
)
self.helper = FormHelper()
self.helper = SaplFormHelper()
self.helper.layout = Layout(*fields)
def clean(self):
@ -1634,7 +1851,7 @@ class ConfirmarProposicaoForm(ProposicaoForm):
required=False, widget=widgets.TextInput(
attrs={'readonly': 'readonly'}))
regime_tramitacao = forms.ModelChoiceField(
regime_tramitacao = forms.ModelChoiceField(label="Regime de tramitação",
required=False, queryset=RegimeTramitacao.objects.all())
gerar_protocolo = forms.ChoiceField(
@ -1654,21 +1871,20 @@ class ConfirmarProposicaoForm(ProposicaoForm):
'descricao',
'observacao',
'gerar_protocolo',
'numero_de_paginas'
'numero_de_paginas',
'numero_materia_futuro'
]
widgets = {
'descricao': widgets.Textarea(
attrs={'readonly': 'readonly', 'rows': 4}),
'data_envio': widgets.DateTimeInput(
attrs={'readonly': 'readonly'}),
}
def __init__(self, *args, **kwargs):
self.proposicao_incorporacao_obrigatoria = \
sapl.base.models.AppConfig.attr(
'proposicao_incorporacao_obrigatoria')
AppConfig.attr('proposicao_incorporacao_obrigatoria')
if self.proposicao_incorporacao_obrigatoria != 'C':
if 'gerar_protocolo' in self._meta.fields:
@ -1700,14 +1916,19 @@ class ConfirmarProposicaoForm(ProposicaoForm):
# esta chamada isola o __init__ de ProposicaoForm
super(ProposicaoForm, self).__init__(*args, **kwargs)
if self.instance.tipo.content_type.model_class() ==\
TipoMateriaLegislativa:
self.fields['regime_tramitacao'].required = True
fields = [
Fieldset(
_('Dados Básicos'),
to_row(
[
('tipo_readonly', 4),
('tipo_readonly', 3),
('data_envio', 3),
('autor_readonly', 5),
('autor_readonly', 3),
('numero_materia_futuro', 3),
('descricao', 12),
('observacao', 12)
]
@ -1715,6 +1936,11 @@ class ConfirmarProposicaoForm(ProposicaoForm):
)
]
if not AppConfig.objects.last().escolher_numero_materia_proposicao or \
not self.instance.numero_materia_futuro:
if 'numero_materia_futuro' in self._meta.fields:
del fields[0][0][3]
fields.append(
Fieldset(
_('Vinculado a Matéria Legislativa'),
@ -1762,11 +1988,13 @@ class ConfirmarProposicaoForm(ProposicaoForm):
fields.append(
Fieldset(_('Registro de Incorporação'), Row(*itens_incorporacao)))
self.helper = FormHelper()
self.helper = SaplFormHelper()
self.helper.layout = Layout(*fields)
self.fields['tipo_readonly'].initial = self.instance.tipo.descricao
self.fields['autor_readonly'].initial = str(self.instance.autor)
if self.instance.numero_materia_futuro:
self.fields['numero_materia_futuro'].initial = self.instance.numero_materia_futuro
if self.instance.materia_de_vinculo:
self.fields[
@ -1788,7 +2016,7 @@ class ConfirmarProposicaoForm(ProposicaoForm):
if not self.is_valid():
return self.cleaned_data
numeracao = sapl.base.models.AppConfig.attr('sequencia_numeracao')
numeracao = AppConfig.attr('sequencia_numeracao_proposicao')
if not numeracao:
self.logger.error("A sequência de numeração (por ano ou geral)"
@ -1865,8 +2093,8 @@ class ConfirmarProposicaoForm(ProposicaoForm):
try:
self.logger.debug(
"Tentando obter modelo de sequência de numeração.")
numeracao = sapl.base.models.AppConfig.objects.last(
).sequencia_numeracao
numeracao = AppConfig.objects.last(
).sequencia_numeracao_protocolo
except AttributeError as e:
self.logger.error("Erro ao obter modelo. " + str(e))
pass
@ -1892,10 +2120,14 @@ class ConfirmarProposicaoForm(ProposicaoForm):
elif numeracao == 'U':
numero = MateriaLegislativa.objects.filter(
tipo=tipo).aggregate(Max('numero'))
if numeracao is None:
numero['numero__max'] = 0
if cd['numero_materia_futuro'] and not MateriaLegislativa.objects.filter(tipo=tipo,
ano=ano,
numero=cd['numero_materia_futuro']):
max_numero = cd['numero_materia_futuro']
else:
max_numero = numero['numero__max'] + \
1 if numero['numero__max'] else 1
@ -2013,7 +2245,7 @@ class ConfirmarProposicaoForm(ProposicaoForm):
GenericForeignKey
"""
numeracao = sapl.base.models.AppConfig.attr('sequencia_numeracao')
numeracao = AppConfig.attr('sequencia_numeracao_protocolo')
if numeracao == 'A':
nm = Protocolo.objects.filter(
ano=timezone.now().year).aggregate(Max('numero'))
@ -2118,7 +2350,7 @@ class EtiquetaPesquisaForm(forms.Form):
[('processo_inicial', 6),
('processo_final', 6)])
self.helper = FormHelper()
self.helper = SaplFormHelper()
self.helper.layout = Layout(
Fieldset(
('Formulário de Etiqueta'),
@ -2203,7 +2435,7 @@ class FichaPesquisaForm(forms.Form):
('data_inicial', 3),
('data_final', 3)])
self.helper = FormHelper()
self.helper = SaplFormHelper()
self.helper.layout = Layout(
Fieldset(
('Formulário de Ficha'),
@ -2244,7 +2476,7 @@ class FichaSelecionaForm(forms.Form):
row1 = to_row(
[('materia', 12)])
self.helper = FormHelper()
self.helper = SaplFormHelper()
self.helper.layout = Layout(
Fieldset(
('Selecione a ficha que deseja imprimir'),
@ -2314,7 +2546,7 @@ class ExcluirTramitacaoEmLote(forms.Form):
[('unidade_tramitacao_local', 6),
('unidade_tramitacao_destino', 6)])
self.helper = FormHelper()
self.helper = SaplFormHelper()
self.helper.layout = Layout(
Fieldset(_('Dados das Tramitações'),
row1,
@ -2323,3 +2555,74 @@ class ExcluirTramitacaoEmLote(forms.Form):
form_actions(label='Excluir')
)
)
class MateriaPesquisaSimplesForm(forms.Form):
tipo_materia = forms.ModelChoiceField(
label=TipoMateriaLegislativa._meta.verbose_name,
queryset=TipoMateriaLegislativa.objects.all(),
required=False,
empty_label='Selecione')
data_inicial = forms.DateField(
label='Data Inicial',
required=False,
widget=forms.DateInput(format='%d/%m/%Y')
)
data_final = forms.DateField(
label='Data Final',
required=False,
widget=forms.DateInput(format='%d/%m/%Y')
)
titulo = forms.CharField(
label='Título do Relatório',
required=False,
max_length=150)
logger = logging.getLogger(__name__)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
row1 = to_row(
[('tipo_materia', 6),
('data_inicial', 3),
('data_final', 3)])
row2 = to_row(
[('titulo', 12)])
self.helper = SaplFormHelper()
self.helper.layout = Layout(
Fieldset(
'Índice de Materias',
row1, row2,
form_actions(label='Pesquisar')
)
)
def clean(self):
super().clean()
if not self.is_valid():
return self.cleaned_data
cleaned_data = self.cleaned_data
data_inicial = cleaned_data['data_inicial']
data_final = cleaned_data['data_final']
if data_inicial or data_final:
if not (data_inicial and data_final):
self.logger.error("Caso pesquise por data, os campos de Data Inicial e "
"Data Final devem ser preenchidos obrigatoriamente")
raise ValidationError(_('Caso pesquise por data, os campos de Data Inicial e '
'Data Final devem ser preenchidos obrigatoriamente'))
elif data_inicial > data_final:
self.logger.error("Data Final ({}) menor que a Data Inicial ({}).".format(
data_final, data_inicial))
raise ValidationError(
_('A Data Final não pode ser menor que a Data Inicial'))
return cleaned_data

20
sapl/materia/migrations/0041_proposicao_numero_materia_futuro.py

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-02-15 11:10
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('materia', '0040_auto_20190211_1602'),
]
operations = [
migrations.AddField(
model_name='proposicao',
name='numero_materia_futuro',
field=models.PositiveIntegerField(blank=True, null=True, verbose_name='Número Matéria'),
),
]

21
sapl/materia/migrations/0042_tipomaterialegislativa_sequencia_regimental.py

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-03-20 11:26
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('materia', '0041_proposicao_numero_materia_futuro'),
]
operations = [
migrations.AddField(
model_name='tipomaterialegislativa',
name='sequencia_regimental',
field=models.PositiveIntegerField(
default=0, help_text='A sequência regimental diz respeito ao que define o regimento da Casa Legislativa sobre qual a ordem de entrada das proposições nas Sessões Plenárias.', verbose_name='Sequência Regimental'),
),
]

19
sapl/materia/migrations/0043_auto_20190320_1749.py

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-03-20 20:49
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('materia', '0042_tipomaterialegislativa_sequencia_regimental'),
]
operations = [
migrations.AlterModelOptions(
name='tipomaterialegislativa',
options={'ordering': ['sequencia_regimental', 'descricao'], 'verbose_name': 'Tipo de Matéria Legislativa', 'verbose_name_plural': 'Tipos de Matérias Legislativas'},
),
]

22
sapl/materia/migrations/0044_auto_20190327_1409.py

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-03-27 17:09
from __future__ import unicode_literals
from django.db import migrations, models
import sapl.materia.models
import sapl.utils
class Migration(migrations.Migration):
dependencies = [
('materia', '0043_auto_20190320_1749'),
]
operations = [
migrations.AlterField(
model_name='documentoacessorio',
name='arquivo',
field=models.FileField(blank=True, max_length=255, null=True, upload_to=sapl.materia.models.anexo_upload_path, validators=[sapl.utils.restringe_tipos_de_arquivo_txt], verbose_name='Texto Integral'),
),
]

20
sapl/materia/migrations/0045_auto_20190415_1050.py

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-04-15 13:50
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('materia', '0044_auto_20190327_1409'),
]
operations = [
migrations.AlterField(
model_name='tipomaterialegislativa',
name='sequencia_numeracao',
field=models.CharField(blank=True, choices=[('A', 'Sequencial por ano para cada autor'), ('B', 'Sequencial por ano indepententemente do autor'), ('L', 'Sequencial por legislatura'), ('U', 'Sequencial único')], max_length=1, verbose_name='Sequência de numeração'),
),
]

20
sapl/materia/migrations/0046_auto_20190417_0941.py

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-04-17 12:41
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('materia', '0045_auto_20190415_1050'),
]
operations = [
migrations.AlterField(
model_name='tipomaterialegislativa',
name='sequencia_numeracao',
field=models.CharField(blank=True, choices=[('A', 'Sequencial por ano para cada autor'), ('L', 'Sequencial por legislatura'), ('U', 'Sequencial único')], max_length=1, verbose_name='Sequência de numeração'),
),
]

28
sapl/materia/migrations/0046_auto_20190417_1212.py

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-04-17 15:12
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('materia', '0045_auto_20190415_1050'),
]
operations = [
migrations.AddField(
model_name='tramitacao',
name='ip',
field=models.CharField(blank=True, default='', max_length=30, verbose_name='IP'),
),
migrations.AddField(
model_name='tramitacao',
name='user',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='Usuário'),
),
]

20
sapl/materia/migrations/0047_auto_20190417_1432.py

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-04-17 17:32
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('materia', '0046_auto_20190417_0941'),
]
operations = [
migrations.AlterField(
model_name='tipomaterialegislativa',
name='sequencia_numeracao',
field=models.CharField(blank=True, choices=[('A', 'Sequencial por ano'), ('L', 'Sequencial por legislatura'), ('U', 'Sequencial único')], max_length=1, verbose_name='Sequência de numeração'),
),
]

16
sapl/materia/migrations/0048_merge_20190426_0828.py

@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-04-26 11:28
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('materia', '0046_auto_20190417_1212'),
('materia', '0047_auto_20190417_1432'),
]
operations = [
]

67
sapl/materia/models.py

@ -10,7 +10,7 @@ from django.utils.translation import ugettext_lazy as _
from model_utils import Choices
import reversion
from sapl.base.models import SEQUENCIA_NUMERACAO, Autor
from sapl.base.models import SEQUENCIA_NUMERACAO_PROTOCOLO, Autor
from sapl.comissoes.models import Comissao
from sapl.compilacao.models import (PerfilEstruturalTextoArticulado,
TextoArticulado)
@ -18,7 +18,7 @@ from sapl.parlamentares.models import Parlamentar
#from sapl.protocoloadm.models import Protocolo
from sapl.utils import (RANGE_ANOS, YES_NO_CHOICES, SaplGenericForeignKey,
SaplGenericRelation, restringe_tipos_de_arquivo_txt,
texto_upload_path)
texto_upload_path, get_settings_auth_user_model)
EM_TRAMITACAO = [(1, 'Sim'),
@ -78,8 +78,37 @@ class TipoProposicao(models.Model):
return self.descricao
class TipoMateriaManager(models.Manager):
def reordene(self, exclude_pk=None):
tipos = self.get_queryset()
if exclude_pk:
tipos = tipos.exclude(pk=exclude_pk)
for sr, t in enumerate(tipos, 1):
t.sequencia_regimental = sr
t.save()
def reposicione(self, pk, idx):
tipos = self.reordene(exclude_pk=pk)
self.get_queryset(
).filter(
sequencia_regimental__gte=idx
).update(
sequencia_regimental=models.F('sequencia_regimental') + 1
)
self.get_queryset(
).filter(
pk=pk
).update(
sequencia_regimental=idx
)
@reversion.register()
class TipoMateriaLegislativa(models.Model):
objects = TipoMateriaManager()
sigla = models.CharField(max_length=5, verbose_name=_('Sigla'))
descricao = models.CharField(max_length=50, verbose_name=_('Descrição '))
# XXX o que é isso ?
@ -99,12 +128,19 @@ class TipoMateriaLegislativa(models.Model):
max_length=1,
blank=True,
verbose_name=_('Sequência de numeração'),
choices=SEQUENCIA_NUMERACAO)
choices=SEQUENCIA_NUMERACAO_PROTOCOLO)
sequencia_regimental = models.PositiveIntegerField(
default=0,
verbose_name=_('Sequência Regimental'),
help_text=_('A sequência regimental diz respeito ao que define '
'o regimento da Casa Legislativa sobre qual a ordem '
'de entrada das proposições nas Sessões Plenárias.'))
class Meta:
verbose_name = _('Tipo de Matéria Legislativa')
verbose_name_plural = _('Tipos de Matérias Legislativas')
ordering = ['descricao']
ordering = ['sequencia_regimental', 'descricao']
def __str__(self):
return self.descricao
@ -461,6 +497,7 @@ class DocumentoAcessorio(models.Model):
arquivo = models.FileField(
blank=True,
null=True,
max_length=255,
upload_to=anexo_upload_path,
verbose_name=_('Texto Integral'),
validators=[restringe_tipos_de_arquivo_txt])
@ -630,10 +667,15 @@ class Relatoria(models.Model):
verbose_name_plural = _('Relatorias')
def __str__(self):
if self.tipo_fim_relatoria:
return _('%(materia)s - %(tipo)s - %(data)s') % {
'materia': self.materia,
'tipo': self.tipo_fim_relatoria,
'data': self.data_designacao_relator}
'data': self.data_designacao_relator.strftime("%d/%m/%Y")}
else:
return _('%(materia)s - %(data)s') % {
'materia': self.materia,
'data': self.data_designacao_relator.strftime("%d/%m/%Y")}
@reversion.register()
@ -687,6 +729,9 @@ class Proposicao(models.Model):
numero_proposicao = models.PositiveIntegerField(
blank=True, null=True, verbose_name=_('Número'))
numero_materia_futuro = models.PositiveIntegerField(
blank=True, null=True, verbose_name=_('Número Matéria'))
hash_code = models.CharField(verbose_name=_('Código do Documento'),
max_length=200,
blank=True)
@ -917,7 +962,8 @@ class Tramitacao(models.Model):
('B', 'primeira_votacao', _('1ª Votação')),
('C', 'segunda_terceira_votacao', _('2ª e 3ª Votação')),
('D', 'deliberacao', _('Deliberação')),
('E', 'primeira_segunda_votacao_urgencia', _('1ª e 2ª votações em regime de urgência'))
('E', 'primeira_segunda_votacao_urgencia', _(
'1ª e 2ª votações em regime de urgência'))
)
@ -957,6 +1003,15 @@ class Tramitacao(models.Model):
texto = models.TextField(verbose_name=_('Texto da Ação'))
data_fim_prazo = models.DateField(
blank=True, null=True, verbose_name=_('Data Fim Prazo'))
user = models.ForeignKey(get_settings_auth_user_model(),
verbose_name=_('Usuário'),
on_delete=models.PROTECT,
null=True,
blank=True)
ip = models.CharField(verbose_name=_('IP'),
max_length=30,
blank=True,
default='')
class Meta:
verbose_name = _('Tramitação')

235
sapl/materia/tests/test_materia.py

@ -1,3 +1,4 @@
from datetime import date
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.core.files.uploadedfile import SimpleUploadedFile
@ -14,10 +15,67 @@ from sapl.materia.models import (Anexada, Autoria, DespachoInicial,
StatusTramitacao, TipoDocumento,
TipoMateriaLegislativa, TipoProposicao,
Tramitacao, UnidadeTramitacao)
from sapl.materia.forms import (TramitacaoForm, compara_tramitacoes_mat,
TramitacaoUpdateForm)
from sapl.norma.models import (LegislacaoCitada, NormaJuridica,
TipoNormaJuridica)
from sapl.parlamentares.models import Legislatura
from sapl.utils import models_with_gr_for_model
from sapl.utils import models_with_gr_for_model, lista_anexados
@pytest.mark.django_db(transaction=False)
def test_lista_materias_anexadas():
tipo_materia = mommy.make(
TipoMateriaLegislativa,
descricao="Tipo_Teste"
)
regime_tramitacao = mommy.make(
RegimeTramitacao,
descricao="Regime_Teste"
)
materia_principal = mommy.make(
MateriaLegislativa,
numero=20,
ano=2018,
data_apresentacao="2018-01-04",
regime_tramitacao=regime_tramitacao,
tipo=tipo_materia
)
materia_anexada = mommy.make(
MateriaLegislativa,
numero=21,
ano=2019,
data_apresentacao="2019-05-04",
regime_tramitacao=regime_tramitacao,
tipo=tipo_materia
)
materia_anexada_anexada = mommy.make(
MateriaLegislativa,
numero=22,
ano=2020,
data_apresentacao="2020-01-05",
regime_tramitacao=regime_tramitacao,
tipo=tipo_materia
)
mommy.make(
Anexada,
materia_principal=materia_principal,
materia_anexada=materia_anexada,
data_anexacao="2019-05-11"
)
mommy.make(
Anexada,
materia_principal=materia_anexada,
materia_anexada=materia_anexada_anexada,
data_anexacao="2020-11-05"
)
lista = lista_anexados(materia_principal)
assert len(lista) == 2
assert lista[0] == materia_anexada
assert lista[1] == materia_anexada_anexada
@pytest.mark.django_db(transaction=False)
@ -581,3 +639,178 @@ def test_numeracao_materia_legislativa_por_ano(admin_client):
response_content = eval(response.content.decode('ascii'))
esperado_outro_ano = eval('{"ano": "2010", "numero": 1}')
assert response_content['numero'] == esperado_outro_ano['numero']
@pytest.mark.django_db(transaction=False)
def test_tramitacoes_materias_anexadas(admin_client):
tipo_materia = mommy.make(
TipoMateriaLegislativa,
descricao="Tipo_Teste"
)
materia_principal = mommy.make(
MateriaLegislativa,
ano=2018,
data_apresentacao="2018-01-04",
tipo=tipo_materia
)
materia_anexada = mommy.make(
MateriaLegislativa,
ano=2019,
data_apresentacao="2019-05-04",
tipo=tipo_materia
)
materia_anexada_anexada = mommy.make(
MateriaLegislativa,
ano=2020,
data_apresentacao="2020-01-05",
tipo=tipo_materia
)
mommy.make(
Anexada,
materia_principal=materia_principal,
materia_anexada=materia_anexada,
data_anexacao="2019-05-11"
)
mommy.make(
Anexada,
materia_principal=materia_anexada,
materia_anexada=materia_anexada_anexada,
data_anexacao="2020-11-05"
)
unidade_tramitacao_local_1 = make_unidade_tramitacao(descricao="Teste 1")
unidade_tramitacao_destino_1 = make_unidade_tramitacao(descricao="Teste 2")
unidade_tramitacao_destino_2 = make_unidade_tramitacao(descricao="Teste 3")
status = mommy.make(
StatusTramitacao,
indicador='R')
# Teste criação de Tramitacao
form = TramitacaoForm(data={})
form.data = {'data_tramitacao':date(2019, 5, 6),
'unidade_tramitacao_local':unidade_tramitacao_local_1.pk,
'unidade_tramitacao_destino':unidade_tramitacao_destino_1.pk,
'status':status.pk,
'urgente': False,
'texto': "Texto de teste"}
form.instance.materia_id=materia_principal.pk
assert form.is_valid()
tramitacao_principal = form.save()
tramitacao_anexada = materia_anexada.tramitacao_set.last()
tramitacao_anexada_anexada = materia_anexada_anexada.tramitacao_set.last()
# Verifica se foram criadas as tramitações para as matérias anexadas e anexadas às anexadas
assert materia_principal.tramitacao_set.last() == tramitacao_principal
assert tramitacao_principal.materia.em_tramitacao == (tramitacao_principal.status.indicador != "F")
assert compara_tramitacoes_mat(tramitacao_principal, tramitacao_anexada)
assert MateriaLegislativa.objects.get(id=materia_anexada.pk).em_tramitacao \
== (tramitacao_anexada.status.indicador != "F")
assert compara_tramitacoes_mat(tramitacao_anexada_anexada, tramitacao_principal)
assert MateriaLegislativa.objects.get(id=materia_anexada_anexada.pk).em_tramitacao \
== (tramitacao_anexada_anexada.status.indicador != "F")
# Teste Edição de Tramitacao
form = TramitacaoUpdateForm(data={})
# Alterando unidade_tramitacao_destino
form.data = {'data_tramitacao':tramitacao_principal.data_tramitacao,
'unidade_tramitacao_local':tramitacao_principal.unidade_tramitacao_local.pk,
'unidade_tramitacao_destino':unidade_tramitacao_destino_2.pk,
'status':tramitacao_principal.status.pk,
'urgente': tramitacao_principal.urgente,
'texto': tramitacao_principal.texto}
form.instance = tramitacao_principal
assert form.is_valid()
tramitacao_principal = form.save()
tramitacao_anexada = materia_anexada.tramitacao_set.last()
tramitacao_anexada_anexada = materia_anexada_anexada.tramitacao_set.last()
assert tramitacao_principal.unidade_tramitacao_destino == unidade_tramitacao_destino_2
assert tramitacao_anexada.unidade_tramitacao_destino == unidade_tramitacao_destino_2
assert tramitacao_anexada_anexada.unidade_tramitacao_destino == unidade_tramitacao_destino_2
# Teste Remoção de Tramitacao
url = reverse('sapl.materia:tramitacao_delete',
kwargs={'pk': tramitacao_principal.pk})
response = admin_client.post(url, {'confirmar':'confirmar'} ,follow=True)
assert Tramitacao.objects.filter(id=tramitacao_principal.pk).count() == 0
assert Tramitacao.objects.filter(id=tramitacao_anexada.pk).count() == 0
assert Tramitacao.objects.filter(id=tramitacao_anexada_anexada.pk).count() == 0
# Testes para quando as tramitações das anexadas divergem
form = TramitacaoForm(data={})
form.data = {'data_tramitacao':date(2019, 5, 6),
'unidade_tramitacao_local':unidade_tramitacao_local_1.pk,
'unidade_tramitacao_destino':unidade_tramitacao_destino_1.pk,
'status':status.pk,
'urgente': False,
'texto': "Texto de teste"}
form.instance.materia_id=materia_principal.pk
assert form.is_valid()
tramitacao_principal = form.save()
tramitacao_anexada = materia_anexada.tramitacao_set.last()
tramitacao_anexada_anexada = materia_anexada_anexada.tramitacao_set.last()
form = TramitacaoUpdateForm(data={})
# Alterando unidade_tramitacao_destino
form.data = {'data_tramitacao':tramitacao_anexada.data_tramitacao,
'unidade_tramitacao_local':tramitacao_anexada.unidade_tramitacao_local.pk,
'unidade_tramitacao_destino':unidade_tramitacao_destino_2.pk,
'status':tramitacao_anexada.status.pk,
'urgente': tramitacao_anexada.urgente,
'texto': tramitacao_anexada.texto}
form.instance = tramitacao_anexada
assert form.is_valid()
tramitacao_anexada = form.save()
tramitacao_anexada_anexada = materia_anexada_anexada.tramitacao_set.last()
assert tramitacao_principal.unidade_tramitacao_destino == unidade_tramitacao_destino_1
assert tramitacao_anexada.unidade_tramitacao_destino == unidade_tramitacao_destino_2
assert tramitacao_anexada_anexada.unidade_tramitacao_destino == unidade_tramitacao_destino_2
# Editando a tramitação principal, as tramitações anexadas não devem ser editadas
form = TramitacaoUpdateForm(data={})
# Alterando o texto
form.data = {'data_tramitacao':tramitacao_principal.data_tramitacao,
'unidade_tramitacao_local':tramitacao_principal.unidade_tramitacao_local.pk,
'unidade_tramitacao_destino':tramitacao_principal.unidade_tramitacao_destino.pk,
'status':tramitacao_principal.status.pk,
'urgente': tramitacao_principal.urgente,
'texto': "Testando a alteração"}
form.instance = tramitacao_principal
assert form.is_valid()
tramitacao_principal = form.save()
tramitacao_anexada = materia_anexada.tramitacao_set.last()
tramitacao_anexada_anexada = materia_anexada_anexada.tramitacao_set.last()
assert tramitacao_principal.texto == "Testando a alteração"
assert not tramitacao_anexada.texto == "Testando a alteração"
assert not tramitacao_anexada_anexada.texto == "Testando a alteração"
# Removendo a tramitação pricipal, as tramitações anexadas não devem ser removidas, pois divergiram
url = reverse('sapl.materia:tramitacao_delete',
kwargs={'pk': tramitacao_principal.pk})
response = admin_client.post(url, {'confirmar':'confirmar'} ,follow=True)
assert Tramitacao.objects.filter(id=tramitacao_principal.pk).count() == 0
assert Tramitacao.objects.filter(id=tramitacao_anexada.pk).count() == 1
assert Tramitacao.objects.filter(id=tramitacao_anexada_anexada.pk).count() == 1
# Removendo a tramitação anexada, a tramitação anexada à anexada deve ser removida
url = reverse('sapl.materia:tramitacao_delete',
kwargs={'pk': tramitacao_anexada.pk})
response = admin_client.post(url, {'confirmar':'confirmar'} ,follow=True)
assert Tramitacao.objects.filter(id=tramitacao_anexada.pk).count() == 0
assert Tramitacao.objects.filter(id=tramitacao_anexada_anexada.pk).count() == 0

13
sapl/materia/tests/test_materia_form.py

@ -2,6 +2,7 @@ import pytest
from django.utils.translation import ugettext as _
from model_mommy import mommy
from sapl.comissoes.models import Comissao, TipoComissao
from sapl.materia import forms
from sapl.materia.models import MateriaLegislativa, TipoMateriaLegislativa
@ -172,15 +173,23 @@ def test_valida_campos_obrigatorios_devolver_proposicao_form():
@pytest.mark.django_db(transaction=False)
def test_valida_campos_obrigatorios_relatoria_form():
form = forms.RelatoriaForm(data={})
tipo_comissao = mommy.make(TipoComissao)
comissao = mommy.make(Comissao,
tipo=tipo_comissao,
nome='Comissao Teste',
sigla='T',
data_criacao='2016-03-21')
form = forms.RelatoriaForm(initial={'comissao':comissao}, data={})
assert not form.is_valid()
errors = form.errors
assert errors['parlamentar'] == [_('Este campo é obrigatório.')]
assert errors['data_designacao_relator'] == [_('Este campo é obrigatório.')]
assert errors['composicao'] == [_('Este campo é obrigatório.')]
assert len(errors) == 2
assert len(errors) == 3
@pytest.mark.django_db(transaction=False)

9
sapl/materia/urls.py

@ -8,6 +8,7 @@ from sapl.materia.views import (AcompanhamentoConfirmarView,
CriarProtocoloMateriaView, DespachoInicialCrud,
DocumentoAcessorioCrud,
DocumentoAcessorioEmLoteView,
MateriaAnexadaEmLoteView,
EtiquetaPesquisaView, FichaPesquisaView,
FichaSelecionaView, ImpressosView,
LegislacaoCitadaCrud, MateriaAssuntoCrud,
@ -24,7 +25,8 @@ from sapl.materia.views import (AcompanhamentoConfirmarView,
TipoProposicaoCrud, TramitacaoCrud,
TramitacaoEmLoteView, UnidadeTramitacaoCrud,
proposicao_texto, recuperar_materia,
ExcluirTramitacaoEmLoteView, RetornarProposicao)
ExcluirTramitacaoEmLoteView, RetornarProposicao,
MateriaPesquisaSimplesView)
from sapl.norma.views import NormaPesquisaSimplesView
from sapl.protocoloadm.views import (FichaPesquisaAdmView, FichaSelecionaAdmView)
@ -48,6 +50,9 @@ urlpatterns_impressos = [
url(r'^materia/impressos/norma-pesquisa/$',
NormaPesquisaSimplesView.as_view(),
name='impressos_norma_pesquisa'),
url(r'^materia/impressos/materia-pesquisa/$',
MateriaPesquisaSimplesView.as_view(),
name='impressos_materia_pesquisa'),
url(r'^materia/impressos/ficha-pesquisa-adm/$',
FichaPesquisaAdmView.as_view(),
name= 'impressos_ficha_pesquisa_adm'),
@ -93,6 +98,8 @@ urlpatterns_materia = [
url(r'^materia/acessorio-em-lote', DocumentoAcessorioEmLoteView.as_view(),
name='acessorio_em_lote'),
url(r'^materia/(?P<pk>\d+)/anexada-em-lote', MateriaAnexadaEmLoteView.as_view(),
name='anexada_em_lote'),
url(r'^materia/primeira-tramitacao-em-lote',
PrimeiraTramitacaoEmLoteView.as_view(),
name='primeira_tramitacao_em_lote'),

511
sapl/materia/views.py

@ -1,17 +1,22 @@
from datetime import datetime
import logging
import os
import shutil
import tempfile
import weasyprint
import itertools
from datetime import datetime
from random import choice
from string import ascii_letters, digits
from crispy_forms.helper import FormHelper
from crispy_forms.layout import HTML
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import permission_required
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned, ValidationError
from django.core.urlresolvers import reverse
from django.db.models import Max
from django.db.models import Max, Q
from django.http import HttpResponse, JsonResponse
from django.http.response import Http404, HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect
@ -28,10 +33,11 @@ import sapl
from sapl.base.email_utils import do_envia_email_confirmacao
from sapl.base.models import Autor, CasaLegislativa, AppConfig as BaseAppConfig
from sapl.base.signals import tramitacao_signal
from sapl.comissoes.models import Comissao, Participacao
from sapl.comissoes.models import Comissao, Participacao, Composicao
from sapl.compilacao.models import (STATUS_TA_IMMUTABLE_RESTRICT,
STATUS_TA_PRIVATE)
from sapl.compilacao.views import IntegracaoTaView
from sapl.crispy_layout_mixin import SaplFormHelper
from sapl.crispy_layout_mixin import SaplFormLayout, form_actions
from sapl.crud.base import (RP_DETAIL, RP_LIST, Crud, CrudAux,
MasterDetailCrud,
@ -41,16 +47,18 @@ from sapl.materia.forms import (AnexadaForm, AutoriaForm,
ConfirmarProposicaoForm,
DevolverProposicaoForm, LegislacaoCitadaForm,
OrgaoForm, ProposicaoForm, TipoProposicaoForm,
TramitacaoForm, TramitacaoUpdateForm)
TramitacaoForm, TramitacaoUpdateForm, MateriaPesquisaSimplesForm)
from sapl.norma.models import LegislacaoCitada
from sapl.parlamentares.models import Legislatura
from sapl.protocoloadm.models import Protocolo
from sapl.settings import MEDIA_ROOT
from sapl.utils import (YES_NO_CHOICES, autor_label, autor_modal, SEPARADOR_HASH_PROPOSICAO,
gerar_hash_arquivo, get_base_url,
gerar_hash_arquivo, get_base_url, get_client_ip,
get_mime_type_from_file_extension, montar_row_autor,
show_results_filter_set, mail_service_configured)
show_results_filter_set, mail_service_configured, lista_anexados)
from .forms import (AcessorioEmLoteFilterSet, AcompanhamentoMateriaForm,
AnexadaEmLoteFilterSet,
AdicionarVariasAutoriasFilterSet, DespachoInicialForm,
DocumentoAcessorioForm, EtiquetaPesquisaForm,
FichaPesquisaForm, FichaSelecionaForm, MateriaAssuntoForm,
@ -61,7 +69,7 @@ from .forms import (AcessorioEmLoteFilterSet, AcompanhamentoMateriaForm,
filtra_tramitacao_destino,
filtra_tramitacao_destino_and_status,
filtra_tramitacao_status,
ExcluirTramitacaoEmLote)
ExcluirTramitacaoEmLote, compara_tramitacoes_mat)
from .models import (AcompanhamentoMateria, Anexada, AssuntoMateria, Autoria,
DespachoInicial, DocumentoAcessorio, MateriaAssunto,
MateriaLegislativa, Numeracao, Orgao, Origem, Proposicao,
@ -74,9 +82,6 @@ AssuntoMateriaCrud = CrudAux.build(AssuntoMateria, 'assunto_materia')
OrigemCrud = CrudAux.build(Origem, '')
TipoMateriaCrud = CrudAux.build(
TipoMateriaLegislativa, 'tipo_materia_legislativa')
RegimeTramitacaoCrud = CrudAux.build(
RegimeTramitacao, 'regime_tramitacao')
@ -213,7 +218,8 @@ class CriarProtocoloMateriaView(CreateView):
context['form'].fields['ano'].initial = protocolo.ano
if protocolo:
if protocolo.timestamp:
context['form'].fields['data_apresentacao'].initial = protocolo.timestamp.date()
context['form'].fields['data_apresentacao'].initial = protocolo.timestamp.date(
)
elif protocolo.timestamp_data_hora_manual:
context['form'].fields['data_apresentacao'].initial = protocolo.timestamp_data_hora_manual.date()
elif protocolo.data:
@ -327,7 +333,7 @@ def recuperar_materia(request):
logger.debug("user=" + username +
". Tentando obter numeração da matéria.")
numeracao = sapl.base.models.AppConfig.objects.last(
).sequencia_numeracao
).sequencia_numeracao_proposicao
except AttributeError as e:
logger.error("user=" + username + ". " + str(e) +
" Numeracao da matéria definida como None.")
@ -812,6 +818,10 @@ class ProposicaoCrud(Crud):
self.logger.debug("user=" + username + ". Tentando obter número do objeto MateriaLegislativa com "
"atributos tipo={} e ano={}."
.format(p.tipo.tipo_conteudo_related, p.ano))
if p.numero_materia_futuro:
numero = p.numero_materia_futuro
else:
numero = MateriaLegislativa.objects.filter(tipo=p.tipo.tipo_conteudo_related,
ano=p.ano).last().numero + 1
messages.success(request, _(
@ -1101,45 +1111,13 @@ class RelatoriaCrud(MasterDetailCrud):
class CreateView(MasterDetailCrud.CreateView):
form_class = RelatoriaForm
layout_key = None
logger = logging.getLogger(__name__)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
username = self.request.user.username
try:
self.logger.debug("user=" + username + ". Tentando obter objeto Comissao de pk={}.".format(
context['form'].initial['comissao']))
comissao = Comissao.objects.get(
pk=context['form'].initial['comissao'])
except:
self.logger.error("user=" + username + ". Objeto Comissão de pk={} não encontrado.".format(
context['form'].initial['comissao']))
pass
else:
self.logger.info("user=" + username + ". Objeto Comissao de pk={} obtido com sucesso.".format(
context['form'].initial['comissao']))
composicao = comissao.composicao_set.order_by(
'-periodo__data_inicio').first()
participacao = Participacao.objects.filter(
composicao=composicao)
parlamentares = []
parlamentares.append(['', '---------'])
for p in participacao:
if p.titular:
parlamentares.append(
[p.parlamentar.id, p.parlamentar.nome_parlamentar])
context['form'].fields['parlamentar'].choices = parlamentares
return context
def get_initial(self):
materia = MateriaLegislativa.objects.get(id=self.kwargs['pk'])
loc_atual = Tramitacao.objects.filter(
materia=materia).last()
loc_atual = Tramitacao.objects.filter(materia=materia).last()
if loc_atual is None:
localizacao = 0
@ -1154,37 +1132,28 @@ class RelatoriaCrud(MasterDetailCrud):
class UpdateView(MasterDetailCrud.UpdateView):
form_class = RelatoriaForm
layout_key = None
logger = logging.getLogger(__name__)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
username = self.request.user.username
try:
self.logger.debug("user=" + username + ". Tentando obter objeto Comissao de pk={}.".format(
context['form'].initial['comissao']))
comissao = Comissao.objects.get(
pk=context['form'].initial['comissao'])
except ObjectDoesNotExist:
self.logger.error("user=" + username + ". Objeto Comissão de pk={} não encontrado.".format(
context['form'].initial['comissao']))
pass
else:
self.logger.info("user=" + username + ". Objeto Comissao de pk={} obtido com sucesso.".format(
context['form'].initial['comissao']))
composicao = comissao.composicao_set.order_by(
'-periodo__data_inicio').first()
participacao = Participacao.objects.filter(
composicao=composicao)
parlamentares = [[p.parlamentar.id, p.parlamentar.nome_parlamentar] for
p in participacao if p.titular]
context['form'].fields['parlamentar'].choices = parlamentares
return context
def get_initial(self):
relatoria = Relatoria.objects.get(id=self.kwargs['pk'])
parlamentar = relatoria.parlamentar
comissao = relatoria.comissao
composicoes = [p.composicao for p in
Participacao.objects.filter(
parlamentar=parlamentar,
composicao__comissao=comissao)]
data_designacao = relatoria.data_designacao_relator
composicao = ''
for c in composicoes:
data_inicial = c.periodo.data_inicio
data_fim = c.periodo.data_fim if c.periodo.data_fim else timezone.now().date()
if data_inicial <= data_designacao <= data_fim:
composicao = c.id
break
return {'comissao': relatoria.comissao.id,
'parlamentar': relatoria.parlamentar.id,
'composicao': composicao}
class TramitacaoCrud(MasterDetailCrud):
@ -1220,6 +1189,8 @@ class TramitacaoCrud(MasterDetailCrud):
else:
initial['unidade_tramitacao_local'] = ''
initial['data_tramitacao'] = timezone.now().date()
initial['ip'] = get_client_ip(self.request)
initial['user'] = self.request.user
return initial
def get_context_data(self, **kwargs):
@ -1232,6 +1203,7 @@ class TramitacaoCrud(MasterDetailCrud):
'-timestamp',
'-id').first()
#TODO: Esta checagem foi inserida na issue #2027, mas é mesmo necessária?
if ultima_tramitacao:
if ultima_tramitacao.unidade_tramitacao_destino:
context['form'].fields[
@ -1245,6 +1217,15 @@ class TramitacaoCrud(MasterDetailCrud):
' da última tramitação não pode ser vazia!')
messages.add_message(self.request, messages.ERROR, msg)
primeira_tramitacao = not(Tramitacao.objects.filter(
materia_id=int(kwargs['root_pk'])).exists())
# Se não for a primeira tramitação daquela matéria, o campo
# não pode ser modificado
if not primeira_tramitacao:
context['form'].fields[
'unidade_tramitacao_local'].widget.attrs['disabled'] = True
return context
def form_valid(self, form):
@ -1252,12 +1233,6 @@ class TramitacaoCrud(MasterDetailCrud):
self.object = form.save()
username = self.request.user.username
if form.instance.status.indicador == 'F':
form.instance.materia.em_tramitacao = False
else:
form.instance.materia.em_tramitacao = True
form.instance.materia.save()
try:
self.logger.debug("user=" + username + ". Tentando enviar Tramitacao (sender={}, post={}, request={})."
.format(Tramitacao, self.object, self.request))
@ -1265,7 +1240,6 @@ class TramitacaoCrud(MasterDetailCrud):
post=self.object,
request=self.request)
except Exception as e:
# TODO log error
msg = _('Tramitação criada, mas e-mail de acompanhamento '
'de matéria não enviado. Há problemas na configuração '
'do e-mail.')
@ -1282,16 +1256,16 @@ class TramitacaoCrud(MasterDetailCrud):
layout_key = 'TramitacaoUpdate'
def get_initial(self):
initial = super(UpdateView, self).get_initial()
initial['ip'] = get_client_ip(self.request)
initial['user'] = self.request.user
return initial
def form_valid(self, form):
self.object = form.save()
username = self.request.user.username
if form.instance.status.indicador == 'F':
form.instance.materia.em_tramitacao = False
else:
form.instance.materia.em_tramitacao = True
form.instance.materia.save()
try:
self.logger.debug("user=" + username + ". Tentando enviar Tramitacao (sender={}, post={}, request={}"
.format(Tramitacao, self.object, self.request))
@ -1299,7 +1273,6 @@ class TramitacaoCrud(MasterDetailCrud):
post=self.object,
request=self.request)
except Exception:
# TODO log error
msg = _('Tramitação atualizada, mas e-mail de acompanhamento '
'de matéria não enviado. Há problemas na configuração '
'do e-mail.')
@ -1325,18 +1298,17 @@ class TramitacaoCrud(MasterDetailCrud):
def delete(self, request, *args, **kwargs):
tramitacao = Tramitacao.objects.get(id=self.kwargs['pk'])
materia = MateriaLegislativa.objects.get(id=tramitacao.materia.id)
materia = tramitacao.materia
url = reverse('sapl.materia:tramitacao_list',
kwargs={'pk': tramitacao.materia.id})
kwargs={'pk': materia.id})
ultima_tramitacao = materia.tramitacao_set.order_by(
'-data_tramitacao',
'-timestamp',
'-id').first()
username = request.user.username
if tramitacao.pk != ultima_tramitacao.pk:
username = request.user.username
self.logger.error("user=" + username + ". Não é possível deletar a tramitação de pk={}. "
"Somente a última tramitação (pk={}) pode ser deletada!."
.format(tramitacao.pk, ultima_tramitacao.pk))
@ -1344,13 +1316,28 @@ class TramitacaoCrud(MasterDetailCrud):
messages.add_message(request, messages.ERROR, msg)
return HttpResponseRedirect(url)
else:
tramitacao.delete()
tramitacoes_deletar = [tramitacao.id]
mat_anexadas = lista_anexados(materia)
for ma in mat_anexadas:
tram_anexada = ma.tramitacao_set.last()
if compara_tramitacoes_mat(tram_anexada, tramitacao):
tramitacoes_deletar.append(tram_anexada.id)
Tramitacao.objects.filter(id__in=tramitacoes_deletar).delete()
return HttpResponseRedirect(url)
class DetailView(MasterDetailCrud.DetailView):
template_name = "materia/tramitacao_detail.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['user'] = self.request.user
return context
def montar_helper_documento_acessorio(self):
autor_row = montar_row_autor('autor')
self.helper = FormHelper()
self.helper = SaplFormHelper()
self.helper.layout = SaplFormLayout(*self.get_layout())
# Adiciona o novo campo 'autor' e mecanismo de busca
@ -1617,6 +1604,20 @@ class MateriaLegislativaCrud(Crud):
form_class = MateriaLegislativaForm
def form_valid(self, form):
self.object = form.save()
username = self.request.user.username
if Anexada.objects.filter(materia_principal=self.kwargs['pk']).exists():
materia = MateriaLegislativa.objects.get(pk=self.kwargs['pk'])
anexadas = lista_anexados(materia)
for anexada in anexadas:
anexada.em_tramitacao = True if form.instance.em_tramitacao else False
anexada.save()
return super().form_valid(form)
@property
def cancel_url(self):
return self.search_url
@ -1917,19 +1918,50 @@ class AcompanhamentoMateriaView(CreateView):
confirmar o acompanhamento desta matéria.')
messages.add_message(request, messages.SUCCESS, msg)
# Se o elemento existir e o email não foi confirmado:
# gerar novo hash e reenviar mensagem de email
elif not acompanhar[0].confirmado:
acompanhar = acompanhar[0]
acompanhar.hash = hash_txt
acompanhar.save()
base_url = get_base_url(request)
destinatario = AcompanhamentoMateria.objects.get(
materia=materia,
email=email,
confirmado=False
)
casa = CasaLegislativa.objects.first()
do_envia_email_confirmacao(base_url,
casa,
"materia",
materia,
destinatario)
self.logger.debug('user=' + usuario.username + '. Foi enviado um e-mail de confirmação. Confira sua caixa \
de mensagens e clique no link que nós enviamos para \
confirmar o acompanhamento desta matéria.')
msg = _('Foi enviado um e-mail de confirmação. Confira sua caixa \
de mensagens e clique no link que nós enviamos para \
confirmar o acompanhamento desta matéria.')
messages.add_message(request, messages.SUCCESS, msg)
# Caso esse Acompanhamento já exista
# avisa ao usuário que essa matéria já está sendo acompanhada
else:
self.logger.debug("user=" + usuario.username +
". Este e-mail já está acompanhando essa matéria.")
msg = _('Este e-mail já está acompanhando essa matéria.')
messages.add_message(request, messages.INFO, msg)
messages.add_message(request, messages.ERROR, msg)
return self.render_to_response(
{'form': form,
'materia': materia,
'error': _('Essa matéria já está\
sendo acompanhada por este e-mail.')})
'materia': materia
})
return HttpResponseRedirect(self.get_success_url())
else:
return self.render_to_response(
@ -1945,6 +1977,7 @@ class DocumentoAcessorioEmLoteView(PermissionRequiredMixin, FilterView):
filterset_class = AcessorioEmLoteFilterSet
template_name = 'materia/em_lote/acessorio.html'
permission_required = ('materia.add_documentoacessorio',)
logger = logging.getLogger(__name__)
def get_context_data(self, **kwargs):
context = super(DocumentoAcessorioEmLoteView,
@ -1966,6 +1999,7 @@ class DocumentoAcessorioEmLoteView(PermissionRequiredMixin, FilterView):
return context
def post(self, request, *args, **kwargs):
username = request.user.username
marcadas = request.POST.getlist('materia_id')
if len(marcadas) == 0:
@ -1977,22 +2011,182 @@ class DocumentoAcessorioEmLoteView(PermissionRequiredMixin, FilterView):
tz = timezone.get_current_timezone()
if len(request.POST['autor']) > 50:
msg = _('Autor tem que ter menos do que 50 caracteres.')
messages.add_message(request, messages.ERROR, msg)
return self.get(request, self.kwargs)
tmp_name = os.path.join(MEDIA_ROOT, request.FILES['arquivo'].name)
with open(tmp_name, 'wb') as destination:
for chunk in request.FILES['arquivo'].chunks():
destination.write(chunk)
try:
doc_data = tz.localize(datetime.strptime(
request.POST['data'], "%d/%m/%Y"))
except Exception as e:
msg = _('Formato da data incorreto. O formato deve ser da forma dd/mm/aaaa.')
messages.add_message(request, messages.ERROR, msg)
self.logger.error("User={}. {}. Data inserida: {}".format(username, str(msg), request.POST['data']))
os.remove(tmp_name)
return self.get(request, self.kwargs)
for materia_id in marcadas:
doc = DocumentoAcessorio()
doc.materia_id = materia_id
doc.tipo = tipo
doc.arquivo = request.FILES['arquivo']
doc.nome = request.POST['nome']
doc.data = tz.localize(datetime.strptime(
request.POST['data'], "%d/%m/%Y"))
doc.data = doc_data
doc.autor = request.POST['autor']
doc.ementa = request.POST['ementa']
doc.arquivo.name = tmp_name
try:
doc.clean_fields()
except ValidationError as e:
for m in [ '%s: %s' % (DocumentoAcessorio()._meta.get_field(k).verbose_name, '</br>'.join(v))
for k,v in e.message_dict.items() ]:
# Insere as mensagens de erro no formato:
# 'verbose_name do nome do campo': 'mensagem de erro'
messages.add_message(request, messages.ERROR, m)
self.logger.error("User={}. {}. Nome do arquivo: {}.".format(username, str(msg), request.FILES['arquivo'].name))
os.remove(tmp_name)
return self.get(request, self.kwargs)
doc.save()
diretorio = os.path.join(MEDIA_ROOT,
'sapl/public/documentoacessorio',
str(doc_data.year),
str(doc.id))
if not os.path.exists(diretorio):
os.makedirs(diretorio)
file_path = os.path.join(diretorio,
request.FILES['arquivo'].name)
shutil.copy2(tmp_name, file_path)
doc.arquivo.name = file_path.split(MEDIA_ROOT + "/")[1] # Retira MEDIA_ROOT do nome
doc.save()
os.remove(tmp_name)
msg = _('Documento(s) criado(s).')
messages.add_message(request, messages.SUCCESS, msg)
return self.get(request, self.kwargs)
class MateriaAnexadaEmLoteView(PermissionRequiredMixin, FilterView):
filterset_class = AnexadaEmLoteFilterSet
template_name = 'materia/em_lote/anexada.html'
permission_required = ('materia.add_documentoacessorio',)
def get_context_data(self, **kwargs):
context = super(MateriaAnexadaEmLoteView,
self).get_context_data(**kwargs)
context['root_pk'] = self.kwargs['pk']
context['subnav_template_name'] = 'materia/subnav.yaml'
context['title'] = _('Matérias Anexadas em Lote')
# Verifica se os campos foram preenchidos
if not self.request.GET.get('tipo', " "):
msg =_('Por favor, selecione um tipo de matéria.')
messages.add_message(self.request, messages.ERROR, msg)
if not self.request.GET.get('data_apresentacao_0', " ") or not self.request.GET.get('data_apresentacao_1', " "):
msg =_('Por favor, preencha as datas.')
messages.add_message(self.request, messages.ERROR, msg)
return context
if not self.request.GET.get('data_apresentacao_0', " ") or not self.request.GET.get('data_apresentacao_1', " "):
msg =_('Por favor, preencha as datas.')
messages.add_message(self.request, messages.ERROR, msg)
return context
qr = self.request.GET.copy()
context['object_list'] = context['object_list'].order_by(
'numero', '-ano')
principal = MateriaLegislativa.objects.get(pk=self.kwargs['pk'])
not_list = [self.kwargs['pk']] + \
[m for m in principal.materia_principal_set.all().values_list('materia_anexada_id', flat=True)]
context['object_list'] = context['object_list'].exclude(pk__in=not_list)
context['temp_object_list'] = context['object_list']
context['object_list'] = []
for obj in context['temp_object_list']:
materia_anexada = obj
ciclico = False
anexadas_anexada = Anexada.objects.filter(
materia_principal = materia_anexada
)
while anexadas_anexada and not ciclico:
anexadas = []
for anexa in anexadas_anexada:
if principal == anexa.materia_anexada:
ciclico = True
else:
for a in Anexada.objects.filter(materia_principal=anexa.materia_anexada):
anexadas.append(a)
anexadas_anexada = anexadas
if not ciclico:
context['object_list'].append(obj)
context['numero_res'] = len(context['object_list'])
context['filter_url'] = ('&' + qr.urlencode()) if len(qr) > 0 else ''
context['show_results'] = show_results_filter_set(qr)
return context
def post(self, request, *args, **kwargs):
marcadas = request.POST.getlist('materia_id')
data_anexacao = datetime.strptime(
request.POST['data_anexacao'], "%d/%m/%Y").date()
if request.POST['data_desanexacao'] == '':
data_desanexacao = None
v_data_desanexacao = data_anexacao
else:
data_desanexacao = datetime.strptime(
request.POST['data_desanexacao'], "%d/%m/%Y").date()
v_data_desanexacao = data_desanexacao
if len(marcadas) == 0:
msg = _('Nenhuma máteria foi selecionada.')
messages.add_message(request, messages.ERROR, msg)
if data_anexacao > v_data_desanexacao:
msg = _('Data de anexação posterior à data de desanexação.')
messages.add_message(request, messages.ERROR, msg)
return self.get(request, self.kwargs)
if data_anexacao > v_data_desanexacao:
msg = _('Data de anexação posterior à data de desanexação.')
messages.add_message(request, messages.ERROR, msg)
return self.get(request, self.kwargs)
principal = MateriaLegislativa.objects.get(pk=kwargs['pk'])
for materia in MateriaLegislativa.objects.filter(id__in=marcadas):
anexada = Anexada()
anexada.materia_principal = principal
anexada.materia_anexada = materia
anexada.data_anexacao = data_anexacao
anexada.data_desanexacao = data_desanexacao
anexada.save()
msg = _('Matéria(s) anexada(s).')
messages.add_message(request, messages.SUCCESS, msg)
sucess_url = reverse('sapl_index') + 'materia/' + kwargs['pk'] + '/anexada'
return HttpResponseRedirect(sucess_url)
class PrimeiraTramitacaoEmLoteView(PermissionRequiredMixin, FilterView):
filterset_class = PrimeiraTramitacaoEmLoteFilterSet
template_name = 'materia/em_lote/tramitacao.html'
@ -2092,7 +2286,17 @@ class PrimeiraTramitacaoEmLoteView(PermissionRequiredMixin, FilterView):
# TODO: usar Form
urgente = request.POST['urgente'] == 'True'
flag_error = False
for materia_id in marcadas:
materias_principais = [m for m in MateriaLegislativa.objects.filter(id__in=marcadas)]
materias_anexadas = [m.anexadas.all() for m in MateriaLegislativa.objects.filter(id__in=marcadas) if m.anexadas.all()]
materias_anexadas = list(itertools.chain.from_iterable(materias_anexadas))
tramitacao_local = int(request.POST['unidade_tramitacao_local'])
materias_anexadas = list(filter(lambda ma : not ma.tramitacao_set.all() or \
ma.tramitacao_set.last().unidade_tramitacao_destino.id == tramitacao_local,
materias_anexadas))
materias = set(materias_principais + materias_anexadas)
for materia in materias:
try:
data_tramitacao = tz.localize(datetime.strptime(
request.POST['data_tramitacao'], "%d/%m/%Y"))
@ -2101,8 +2305,10 @@ class PrimeiraTramitacaoEmLoteView(PermissionRequiredMixin, FilterView):
messages.add_message(request, messages.ERROR, msg)
return self.get(request, self.kwargs)
user = request.user
ip = get_client_ip(request)
t = Tramitacao(
materia_id=materia_id,
materia=materia,
data_tramitacao=data_tramitacao,
data_encaminhamento=data_encaminhamento,
data_fim_prazo=data_fim_prazo,
@ -2113,7 +2319,9 @@ class PrimeiraTramitacaoEmLoteView(PermissionRequiredMixin, FilterView):
urgente=urgente,
status_id=request.POST['status'],
turno=request.POST['turno'],
texto=request.POST['texto']
texto=request.POST['texto'],
user=user,
ip=ip
)
t.save()
try:
@ -2136,7 +2344,7 @@ class PrimeiraTramitacaoEmLoteView(PermissionRequiredMixin, FilterView):
status = StatusTramitacao.objects.get(id=request.POST['status'])
for materia in MateriaLegislativa.objects.filter(id__in=marcadas):
for materia in materias:
if status.indicador == 'F':
materia.em_tramitacao = False
elif self.primeira_tramitacao:
@ -2183,13 +2391,11 @@ class ImpressosView(PermissionRequiredMixin, TemplateView):
def gerar_pdf_impressos(request, context, template_name):
template = loader.get_template(template_name)
html = template.render(context, request)
pdf = weasyprint.HTML(string=html, base_url=request.build_absolute_uri()
).write_pdf()
pdf = weasyprint.HTML(
string=html, base_url=request.build_absolute_uri()).write_pdf()
response = HttpResponse(pdf, content_type='application/pdf')
response['Content-Disposition'] = (
'inline; filename="relatorio_impressos.pdf"')
response['Content-Disposition'] = 'inline; filename="relatorio_impressos.pdf"'
response['Content-Transfer-Encoding'] = 'binary'
return response
@ -2197,7 +2403,7 @@ def gerar_pdf_impressos(request, context, template_name):
class EtiquetaPesquisaView(PermissionRequiredMixin, FormView):
form_class = EtiquetaPesquisaForm
template_name = 'materia/impressos/etiqueta.html'
template_name = 'materia/impressos/impressos_form.html'
permission_required = ('materia.can_access_impressos', )
def form_valid(self, form):
@ -2238,7 +2444,7 @@ class EtiquetaPesquisaView(PermissionRequiredMixin, FormView):
class FichaPesquisaView(PermissionRequiredMixin, FormView):
form_class = FichaPesquisaForm
template_name = 'materia/impressos/ficha.html'
template_name = 'materia/impressos/impressos_form.html'
permission_required = ('materia.can_access_impressos', )
def form_valid(self, form):
@ -2256,7 +2462,7 @@ class FichaPesquisaView(PermissionRequiredMixin, FormView):
class FichaSelecionaView(PermissionRequiredMixin, FormView):
logger = logging.getLogger(__name__)
form_class = FichaSelecionaForm
template_name = 'materia/impressos/ficha_seleciona.html'
template_name = 'materia/impressos/impressos_form.html'
permission_required = ('materia.can_access_impressos', )
def get_context_data(self, **kwargs):
@ -2353,3 +2559,76 @@ class ExcluirTramitacaoEmLoteView(PermissionRequiredMixin, FormView):
tramitacao.delete()
return redirect(self.get_success_url())
class MateriaPesquisaSimplesView(PermissionRequiredMixin, FormView):
form_class = MateriaPesquisaSimplesForm
template_name = 'materia/impressos/impressos_form.html'
permission_required = ('materia.can_access_impressos', )
def form_valid(self, form):
template_materia = 'materia/impressos/materias_pdf.html'
kwargs = {}
if form.cleaned_data.get('tipo_materia'):
kwargs.update({'tipo': form.cleaned_data['tipo_materia']})
if form.cleaned_data.get('data_inicial'):
kwargs.update({'data__gte': form.cleaned_data['data_inicial'],
'data__lte': form.cleaned_data['data_final']})
materias = MateriaLegislativa.objects.filter(
**kwargs).order_by('-numero', 'ano')
quantidade_materias = materias.count()
materias = materias[:2000] if quantidade_materias > 2000 else materias
context = {'quantidade': quantidade_materias,
'titulo': form.cleaned_data['titulo'],
'materias': materias}
return gerar_pdf_impressos(self.request, context, template_materia)
class TipoMateriaCrud(CrudAux):
model = TipoMateriaLegislativa
class DetailView(CrudAux.DetailView):
layout_key = 'TipoMateriaLegislativaDetail'
class DeleteView(CrudAux.DeleteView):
def delete(self, request, *args, **kwargs):
d = CrudAux.DeleteView.delete(self, request, *args, **kwargs)
TipoMateriaLegislativa.objects.reordene()
return d
class ListView(CrudAux.ListView):
paginate_by = None
layout_key = 'TipoMateriaLegislativaDetail'
template_name = "materia/tipomaterialegislativa_list.html"
def hook_sigla(self, obj, default, url):
return '<a href="{}" pk="{}">{}</a>'.format(
url, obj.id, obj.sigla), ''
def get(self, request, *args, **kwargs):
if TipoMateriaLegislativa.objects.filter(
sequencia_regimental=0).exists():
TipoMateriaLegislativa.objects.reordene()
return CrudAux.ListView.get(self, request, *args, **kwargs)
class CreateView(CrudAux.CreateView):
def form_valid(self, form):
fv = super().form_valid(form)
if not TipoMateriaLegislativa.objects.exclude(
sequencia_regimental=0).exists():
TipoMateriaLegislativa.objects.reordene()
else:
sr__max = TipoMateriaLegislativa.objects.all().aggregate(
Max('sequencia_regimental'))
self.object.sequencia_regimental = sr__max['sequencia_regimental__max'] + 1
self.object.save()
return fv

45
sapl/norma/forms.py

@ -1,7 +1,7 @@
import logging
from crispy_forms.helper import FormHelper
from sapl.crispy_layout_mixin import SaplFormHelper
from crispy_forms.layout import Fieldset, Layout
from django import forms
from django.core.exceptions import ObjectDoesNotExist, ValidationError
@ -17,8 +17,8 @@ from sapl.crispy_layout_mixin import form_actions, to_row
from sapl.materia.forms import choice_anos_com_materias
from sapl.materia.models import MateriaLegislativa, TipoMateriaLegislativa
from sapl.settings import MAX_DOC_UPLOAD_SIZE
from sapl.utils import NormaPesquisaOrderingFilter, RangeWidgetOverride,\
choice_anos_com_normas, FilterOverridesMetaMixin
from sapl.utils import NormaPesquisaOrderingFilter, RangeWidgetOverride, \
choice_anos_com_normas, FilterOverridesMetaMixin, FileFieldCheckMixin
from .models import (AnexoNormaJuridica, AssuntoNorma, NormaJuridica, NormaRelacionada,
TipoNormaJuridica, AutoriaNorma)
@ -71,7 +71,7 @@ class NormaFilterSet(django_filters.FilterSet):
row4 = to_row([('data_vigencia', 12)])
row5 = to_row([('o', 6), ('indexacao', 6)])
self.form.helper = FormHelper()
self.form.helper = SaplFormHelper()
self.form.helper.form_method = 'GET'
self.form.helper.layout = Layout(
Fieldset(_('Pesquisa de Norma'),
@ -88,7 +88,7 @@ class NormaFilterSet(django_filters.FilterSet):
return queryset.filter(q)
class NormaJuridicaForm(ModelForm):
class NormaJuridicaForm(FileFieldCheckMixin, ModelForm):
# Campos de MateriaLegislativa
tipo_materia = forms.ModelChoiceField(
@ -200,6 +200,8 @@ class NormaJuridicaForm(ModelForm):
return cleaned_data
def clean_texto_integral(self):
super(NormaJuridicaForm, self).clean()
texto_integral = self.cleaned_data.get('texto_integral', False)
if texto_integral and texto_integral.size > MAX_DOC_UPLOAD_SIZE:
max_size = str(MAX_DOC_UPLOAD_SIZE / (1024 * 1024))
@ -238,7 +240,7 @@ class AutoriaNormaForm(ModelForm):
('autor', 4),
('primeiro_autor', 4)])
self.helper = FormHelper()
self.helper = SaplFormHelper()
self.helper.layout = Layout(
Fieldset(_('Autoria'),
row1, 'data_relativa', form_actions(label='Salvar')))
@ -269,7 +271,7 @@ class AutoriaNormaForm(ModelForm):
return cd
class AnexoNormaJuridicaForm(ModelForm):
class AnexoNormaJuridicaForm(FileFieldCheckMixin, ModelForm):
class Meta:
model = AnexoNormaJuridica
fields = ['norma', 'anexo_arquivo', 'assunto_anexo']
@ -296,11 +298,10 @@ class AnexoNormaJuridicaForm(ModelForm):
def save(self, commit=False):
anexo = self.instance
anexo.ano = self.cleaned_data['norma'].ano
anexo = super(AnexoNormaJuridicaForm, self).save(commit=True)
anexo.norma = self.cleaned_data['norma']
anexo.assunto_anexo = self.cleaned_data['assunto_anexo']
anexo.anexo_arquivo = self.cleaned_data['anexo_arquivo']
anexo.save()
anexo = super(AnexoNormaJuridicaForm, self).save(commit=True)
return anexo
@ -390,7 +391,7 @@ class NormaPesquisaSimplesForm(forms.Form):
logger = logging.getLogger(__name__)
def __init__(self, *args, **kwargs):
super(NormaPesquisaSimplesForm, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
row1 = to_row(
[('tipo_norma', 6),
@ -400,39 +401,33 @@ class NormaPesquisaSimplesForm(forms.Form):
row2 = to_row(
[('titulo', 12)])
self.helper = FormHelper()
self.helper = SaplFormHelper()
self.helper.layout = Layout(
Fieldset(
('Índice de Normas'),
'Índice de Normas',
row1, row2,
form_actions(label='Pesquisar')
)
)
def clean(self):
super(NormaPesquisaSimplesForm, self).clean()
super().clean()
if not self.is_valid():
return self.cleaned_data
cleaned_data = self.cleaned_data
data_inicial = cleaned_data['data_inicial']
data_final = cleaned_data['data_final']
if (data_inicial and data_final and
data_inicial > data_final):
self.logger.error("Data Final ({}) menor que a Data Inicial ({}).".format(
data_final, data_inicial))
raise ValidationError(_(
'A Data Final não pode ser menor que a Data Inicial'))
else:
condicao1 = data_inicial and not data_final
condicao2 = not data_inicial and data_final
if condicao1 or condicao2:
if data_inicial or data_final:
if not(data_inicial and data_final):
self.logger.error("Caso pesquise por data, os campos de Data Inicial e "
"Data Final devem ser preenchidos obrigatoriamente")
raise ValidationError(_('Caso pesquise por data, os campos de Data Inicial e ' +
raise ValidationError(_('Caso pesquise por data, os campos de Data Inicial e '
'Data Final devem ser preenchidos obrigatoriamente'))
elif data_inicial > data_final:
self.logger.error("Data Final ({}) menor que a Data Inicial ({}).".format(data_final, data_inicial))
raise ValidationError(_('A Data Final não pode ser menor que a Data Inicial'))
return cleaned_data

19
sapl/norma/migrations/0023_auto_20190219_1535.py

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-02-19 18:35
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('norma', '0022_auto_20190108_1606'),
]
operations = [
migrations.AlterModelOptions(
name='normarelacionada',
options={'ordering': ('norma_principal__ano', 'norma_relacionada__ano'), 'verbose_name': 'Norma Relacionada', 'verbose_name_plural': 'Normas Relacionadas'},
),
]

19
sapl/norma/migrations/0024_auto_20190425_0917.py

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-04-25 12:17
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('norma', '0023_auto_20190219_1535'),
]
operations = [
migrations.AlterModelOptions(
name='normarelacionada',
options={'ordering': ('norma_principal__data', 'norma_relacionada__data'), 'verbose_name': 'Norma Relacionada', 'verbose_name_plural': 'Normas Relacionadas'},
),
]

24
sapl/norma/models.py

@ -145,9 +145,11 @@ class NormaJuridica(models.Model):
def get_normas_relacionadas(self):
principais = NormaRelacionada.objects.filter(
norma_principal=self.id)
norma_principal=self.id).order_by('norma_principal__data',
'norma_relacionada__data')
relacionadas = NormaRelacionada.objects.filter(
norma_relacionada=self.id)
norma_relacionada=self.id).order_by('norma_principal__data',
'norma_relacionada__data')
return (principais, relacionadas)
def get_anexos_norma_juridica(self):
@ -311,6 +313,7 @@ class NormaRelacionada(models.Model):
class Meta:
verbose_name = _('Norma Relacionada')
verbose_name_plural = _('Normas Relacionadas')
ordering = ('norma_principal__data', 'norma_relacionada__data')
def __str__(self):
return _('Principal: %(norma_principal)s'
@ -348,3 +351,20 @@ class AnexoNormaJuridica(models.Model):
def __str__(self):
return _('Anexo: %(anexo)s da norma %(norma)s') % {
'anexo': self.anexo_arquivo, 'norma': self.norma}
def save(self, force_insert=False, force_update=False, using=None,
update_fields=None):
if not self.pk and self.anexo_arquivo:
anexo_arquivo = self.anexo_arquivo
self.anexo_arquivo = None
models.Model.save(self, force_insert=force_insert,
force_update=force_update,
using=using,
update_fields=update_fields)
self.anexo_arquivo = anexo_arquivo
return models.Model.save(self, force_insert=force_insert,
force_update=force_update,
using=using,
update_fields=update_fields)

31
sapl/norma/views.py

@ -345,12 +345,10 @@ class ImpressosView(PermissionRequiredMixin, TemplateView):
def gerar_pdf_impressos(request, context, template_name):
template = loader.get_template(template_name)
html = template.render(context, request)
pdf = weasyprint.HTML(string=html, base_url=request.build_absolute_uri()
).write_pdf()
pdf = weasyprint.HTML(string=html, base_url=request.build_absolute_uri()).write_pdf()
response = HttpResponse(pdf, content_type='application/pdf')
response['Content-Disposition'] = (
'inline; filename="relatorio_impressos.pdf"')
response['Content-Disposition'] = 'inline; filename="relatorio_impressos.pdf"'
response['Content-Transfer-Encoding'] = 'binary'
return response
@ -358,29 +356,28 @@ def gerar_pdf_impressos(request, context, template_name):
class NormaPesquisaSimplesView(PermissionRequiredMixin, FormView):
form_class = NormaPesquisaSimplesForm
template_name = 'materia/impressos/norma.html'
template_name = 'materia/impressos/impressos_form.html'
permission_required = ('materia.can_access_impressos', )
def form_valid(self, form):
normas = NormaJuridica.objects.all().order_by(
'numero')
template_norma = 'materia/impressos/normas_pdf.html'
titulo = form.cleaned_data['titulo']
if form.cleaned_data['tipo_norma']:
normas = normas.filter(tipo=form.cleaned_data['tipo_norma'])
kwargs = {}
if form.cleaned_data.get('tipo_norma'):
kwargs.update({'tipo': form.cleaned_data['tipo_norma']})
if form.cleaned_data['data_inicial']:
normas = normas.filter(
data__gte=form.cleaned_data['data_inicial'],
data__lte=form.cleaned_data['data_final'])
if form.cleaned_data.get('data_inicial'):
kwargs.update({'data__gte': form.cleaned_data['data_inicial'],
'data__lte': form.cleaned_data['data_final']})
qtd_resultados = len(normas)
if qtd_resultados > 2000:
normas = normas[:2000]
normas = NormaJuridica.objects.filter(**kwargs).order_by('-numero', 'ano')
context = {'quantidade': qtd_resultados,
quantidade_normas = normas.count()
normas = normas[:2000] if quantidade_normas > 2000 else normas
context = {'quantidade': quantidade_normas,
'titulo': titulo,
'normas': normas}

160
sapl/parlamentares/forms.py

@ -1,7 +1,7 @@
from datetime import timedelta
import logging
from crispy_forms.helper import FormHelper
from sapl.crispy_layout_mixin import SaplFormHelper
from crispy_forms.layout import Fieldset, Layout
from django import forms
from django.contrib.auth import get_user_model
@ -15,13 +15,15 @@ from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from floppyforms.widgets import ClearableFileInput
from image_cropping.widgets import CropWidget, ImageCropWidget
from sapl.utils import FileFieldCheckMixin
from sapl.base.models import Autor, TipoAutor
from sapl.crispy_layout_mixin import form_actions, to_row
from sapl.rules import SAPL_GROUP_VOTANTE
import django_filters
from .models import (ComposicaoColigacao, Filiacao, Frente, Legislatura,
Mandato, Parlamentar, Votante)
Mandato, Parlamentar, Votante, Bloco)
class ImageThumbnailFileInput(ClearableFileInput):
@ -196,7 +198,7 @@ class LegislaturaForm(ModelForm):
return data
class ParlamentarForm(ModelForm):
class ParlamentarForm(FileFieldCheckMixin, ModelForm):
class Meta:
model = Parlamentar
@ -209,20 +211,31 @@ class ParlamentarForm(ModelForm):
attrs={'id': 'texto-rico'})}
class ParlamentarCreateForm(ParlamentarForm):
class ParlamentarFilterSet(django_filters.FilterSet):
nome_parlamentar = django_filters.CharFilter(
label=_('Nome do Parlamentar'),
lookup_expr='icontains')
legislatura = forms.ModelChoiceField(
label=_('Legislatura'),
required=True,
queryset=Legislatura.objects.all().order_by('-data_inicio'),
empty_label='----------',
)
class Meta:
model = Parlamentar
fields = ['nome_parlamentar']
data_expedicao_diploma = forms.DateField(
label=_('Expedição do Diploma'),
required=True,
def __init__(self, *args, **kwargs):
super(ParlamentarFilterSet, self).__init__(*args, **kwargs)
row0 = to_row([('nome_parlamentar', 12)])
self.form.helper = SaplFormHelper()
self.form.helper.form_method = 'GET'
self.form.helper.layout = Layout(
Fieldset(_('Pesquisa de Parlamentar'),
row0,
form_actions(label='Pesquisar'))
)
class ParlamentarCreateForm(ParlamentarForm):
class Meta(ParlamentarForm.Meta):
widgets = {
'fotografia': forms.ClearableFileInput(),
@ -230,16 +243,24 @@ class ParlamentarCreateForm(ParlamentarForm):
attrs={'id': 'texto-rico'})
}
def clean(self):
super().clean()
if not self.is_valid():
return self.cleaned_data
cleaned_data = self.cleaned_data
parlamentar = Parlamentar.objects.filter(nome_parlamentar=cleaned_data['nome_parlamentar']).exists()
if parlamentar:
self.logger.error('Parlamentar já cadastrado.')
raise ValidationError('Parlamentar já cadastrado.')
return cleaned_data
@transaction.atomic
def save(self, commit=True):
parlamentar = super(ParlamentarCreateForm, self).save(commit)
legislatura = self.cleaned_data['legislatura']
Mandato.objects.create(
parlamentar=parlamentar,
legislatura=legislatura,
data_inicio_mandato=legislatura.data_inicio,
data_fim_mandato=legislatura.data_fim,
data_expedicao_diploma=self.cleaned_data['data_expedicao_diploma'])
content_type = ContentType.objects.get_for_model(Parlamentar)
object_id = parlamentar.pk
tipo = TipoAutor.objects.get(content_type=content_type)
@ -446,7 +467,7 @@ class VotanteForm(ModelForm):
def __init__(self, *args, **kwargs):
row1 = to_row([('username', 4)])
self.helper = FormHelper()
self.helper = SaplFormHelper()
self.helper.layout = Layout(
Fieldset(_('Votante'),
row1, form_actions(label='Salvar'))
@ -500,3 +521,100 @@ class VotanteForm(ModelForm):
votante.user = u
votante.save()
return votante
class VincularParlamentarForm(forms.Form):
logger = logging.getLogger(__name__)
parlamentar = forms.ModelChoiceField(
label=Parlamentar._meta.verbose_name,
queryset=Parlamentar.objects.filter(ativo=True),
required=True,
empty_label='Selecione'
)
legislatura = forms.ModelChoiceField(
label=Legislatura._meta.verbose_name,
queryset=Legislatura.objects.all(),
required=True,
empty_label='Selecione'
)
data_expedicao_diploma = forms.DateField(
label='Data de Expedição do Diploma',
required=False,
widget=forms.DateInput(format='%d/%m/%Y')
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
row1 = to_row([
('parlamentar', 6),
('legislatura', 3),
('data_expedicao_diploma', 3)
])
self.helper = SaplFormHelper()
self.helper.layout = Layout(
Fieldset(
'Vincular Parlamentar',
row1,
form_actions(label='Vincular')
)
)
def clean(self):
super().clean()
if not self.is_valid():
return self.cleaned_data
cleaned_data = self.cleaned_data
parlamentar = cleaned_data['parlamentar']
legislatura = cleaned_data['legislatura']
data_expedicao_diploma = cleaned_data['data_expedicao_diploma']
if parlamentar.mandato_set.filter(legislatura=legislatura):
self.logger.error('Parlamentar já está vinculado a legislatura informada.')
raise ValidationError(_('Parlamentar já está vinculado a legislatura informada.'))
elif data_expedicao_diploma and legislatura.data_inicio <= data_expedicao_diploma:
self.logger.error('Data da Expedição do Diploma deve ser anterior a data de início da Legislatura.')
raise ValidationError(_('Data da Expedição do Diploma deve ser anterior a data de início da Legislatura.'))
return cleaned_data
class BlocoForm(ModelForm):
class Meta:
model = Bloco
fields = ['nome', 'partidos', 'data_criacao',
'data_extincao', 'descricao']
def clean(self):
super(BlocoForm, self).clean()
if not self.is_valid():
return self.cleaned_data
if self.cleaned_data['data_extincao']:
if (self.cleaned_data['data_extincao'] <
self.cleaned_data['data_criacao']):
msg = _('Data de extinção não pode ser menor que a de criação')
raise ValidationError(msg)
return self.cleaned_data
@transaction.atomic
def save(self, commit=True):
bloco = super(BlocoForm, self).save(commit)
content_type = ContentType.objects.get_for_model(Bloco)
object_id = bloco.pk
tipo = TipoAutor.objects.get(content_type=content_type)
Autor.objects.create(
content_type=content_type,
object_id=object_id,
tipo=tipo,
nome=bloco.nome
)
return bloco

37
sapl/parlamentares/migrations/0026_bloco.py

@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-04-30 11:28
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('parlamentares', '0025_auto_20180924_1724'),
('sessao', '0039_auto_20190430_0825')
]
state_operations = [
migrations.CreateModel(
name='Bloco',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('nome', models.CharField(max_length=80, verbose_name='Nome do Bloco')),
('data_criacao', models.DateField(null=True, verbose_name='Data Criação')),
('data_extincao', models.DateField(blank=True, null=True, verbose_name='Data Dissolução')),
('descricao', models.TextField(blank=True, verbose_name='Descrição')),
('partidos', models.ManyToManyField(blank=True, to='parlamentares.Partido', verbose_name='Partidos')),
],
options={
'db_table': 'parlamentares_bloco',
'verbose_name': 'Bloco Parlamentar',
'verbose_name_plural': 'Blocos Parlamentares',
},
bases=(models.Model,),
),
]
operations = [
migrations.SeparateDatabaseAndState(state_operations=state_operations)
]

19
sapl/parlamentares/migrations/0027_auto_20190430_0839.py

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-04-30 11:39
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('parlamentares', '0026_bloco'),
]
operations = [
migrations.AlterModelTable(
name='bloco',
table=None,
),
]

34
sapl/parlamentares/models.py

@ -568,3 +568,37 @@ class Votante(models.Model):
def __str__(self):
return self.user.username
@reversion.register()
class Bloco(models.Model):
'''
* blocos podem existir por mais de uma legislatura
'''
nome = models.CharField(
max_length=80, verbose_name=_('Nome do Bloco'))
partidos = models.ManyToManyField(
Partido, blank=True, verbose_name=_('Partidos'))
data_criacao = models.DateField(
blank=False, null=True, verbose_name=_('Data Criação'))
data_extincao = models.DateField(
blank=True, null=True, verbose_name=_('Data Dissolução'))
descricao = models.TextField(blank=True, verbose_name=_('Descrição'))
# campo conceitual de reversão genérica para o model Autor que dá a
# o meio possível de localização de tipos de autores.
autor = SaplGenericRelation(Autor,
related_query_name='bloco_set',
fields_search=(
('nome', '__icontains'),
('descricao', '__icontains'),
('partidos__sigla', '__icontains'),
('partidos__nome', '__icontains'),
))
class Meta:
verbose_name = _('Bloco Parlamentar')
verbose_name_plural = _('Blocos Parlamentares')
def __str__(self):
return self.nome

15
sapl/parlamentares/tests/test_parlamentares.py

@ -11,7 +11,6 @@ from sapl.parlamentares.models import (Dependente, Filiacao, Legislatura,
@pytest.mark.django_db(transaction=False)
def test_cadastro_parlamentar(admin_client):
legislatura = mommy.make(Legislatura)
url = reverse('sapl.parlamentares:parlamentar_create')
response = admin_client.get(url)
@ -20,21 +19,13 @@ def test_cadastro_parlamentar(admin_client):
response = admin_client.post(url, {'nome_completo': 'Teresa Barbosa',
'nome_parlamentar': 'Terezinha',
'sexo': 'F',
'ativo': 'True',
'legislatura': legislatura.id,
'data_expedicao_diploma': '2001-01-01'},
'ativo': 'True'},
follow=True)
[parlamentar] = Parlamentar.objects.all()
assert parlamentar.nome_parlamentar == 'Terezinha'
assert parlamentar.sexo == 'F'
assert parlamentar.ativo is True
# o primeiro mandato é criado
[mandato] = Mandato.objects.all()
assert mandato.parlamentar == parlamentar
assert str(mandato.data_expedicao_diploma) == '2001-01-01'
assert mandato.legislatura == legislatura
assert mandato.data_fim_mandato == legislatura.data_fim
@pytest.mark.django_db(transaction=False)
@ -42,9 +33,7 @@ def test_incluir_parlamentar_errors(admin_client):
url = reverse('sapl.parlamentares:parlamentar_create')
response = admin_client.post(url)
erros_esperados = {campo: ['Este campo é obrigatório.']
for campo in ['legislatura',
'data_expedicao_diploma',
'nome_parlamentar',
for campo in ['nome_parlamentar',
'nome_completo',
'sexo',
]}

15
sapl/parlamentares/urls.py

@ -17,7 +17,9 @@ from sapl.parlamentares.views import (CargoMesaCrud, ColigacaoCrud,
frente_atualiza_lista_parlamentares,
insere_parlamentar_composicao,
parlamentares_frente_selected,
remove_parlamentar_composicao)
remove_parlamentar_composicao,
parlamentares_filiados, BlocoCrud,
PesquisarParlamentarView, VincularParlamentarView)
from .apps import AppConfig
@ -33,13 +35,20 @@ urlpatterns = [
VotanteView.get_urls()
)),
url(r'^parlamentar/pesquisar-parlamentar/',
PesquisarParlamentarView.as_view(), name='pesquisar_parlamentar'),
url(r'^parlamentar/(?P<pk>\d+)/materias$',
ParlamentarMateriasView.as_view(), name='parlamentar_materias'),
url(r'^parlamentar/vincular-parlamentar/$',
VincularParlamentarView.as_view(), name='vincular_parlamentar'),
url(r'^sistema/coligacao/',
include(ColigacaoCrud.get_urls() +
ComposicaoColigacaoCrud.get_urls())),
url(r'^sistema/bloco/',
include(BlocoCrud.get_urls())),
url(r'^sistema/frente/',
include(FrenteCrud.get_urls())),
url(r'^sistema/frente/atualiza-lista-parlamentares',
@ -60,6 +69,7 @@ urlpatterns = [
url(r'^sistema/parlamentar/tipo-militar/',
include(TipoMilitarCrud.get_urls())),
url(r'^sistema/parlamentar/partido/', include(PartidoCrud.get_urls())),
url(r'^sistema/parlamentar/partido/(?P<pk>\d+)/filiados$', parlamentares_filiados, name='parlamentares_filiados'),
url(r'^sistema/mesa-diretora/sessao-legislativa/',
include(SessaoLegislativaCrud.get_urls())),
@ -80,4 +90,5 @@ urlpatterns = [
url(r'^mesa-diretora/remove-parlamentar-composicao/$',
remove_parlamentar_composicao, name='remove_parlamentar_composicao'),
]

160
sapl/parlamentares/views.py

@ -3,6 +3,7 @@ import json
import logging
from django.contrib import messages
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
from django.core.urlresolvers import reverse, reverse_lazy
@ -10,6 +11,7 @@ from django.db.models import F, Q
from django.db.models.aggregates import Count
from django.http import JsonResponse
from django.http.response import HttpResponseRedirect
from django.shortcuts import render
from django.templatetags.static import static
from django.utils import timezone
from django.utils.datastructures import MultiValueDictKeyError
@ -17,27 +19,33 @@ from django.utils.translation import ugettext_lazy as _
from django.views.decorators.clickjacking import xframe_options_exempt
from django.views.generic import FormView
from django.views.generic.edit import UpdateView
from django_filters.views import FilterView
from image_cropping.utils import get_backend
from sapl.base.forms import SessaoLegislativaForm
from sapl.base.forms import SessaoLegislativaForm, PartidoForm
from sapl.base.models import Autor
from sapl.comissoes.models import Participacao
from sapl.crud.base import (RP_CHANGE, RP_DETAIL, RP_LIST, Crud, CrudAux,
CrudBaseForListAndDetailExternalAppView,
MasterDetailCrud)
MasterDetailCrud, make_pagination)
from sapl.materia.models import Autoria, Proposicao, Relatoria
from sapl.parlamentares.apps import AppConfig
from sapl.utils import parlamentares_ativos
from sapl.utils import (parlamentares_ativos, show_results_filter_set)
from .forms import (FiliacaoForm, FrenteForm, LegislaturaForm, MandatoForm,
ParlamentarCreateForm, ParlamentarForm, VotanteForm)
ParlamentarCreateForm, ParlamentarForm, VotanteForm,
ParlamentarFilterSet, VincularParlamentarForm,
BlocoForm)
from .models import (CargoMesa, Coligacao, ComposicaoColigacao, ComposicaoMesa,
Dependente, Filiacao, Frente, Legislatura, Mandato,
NivelInstrucao, Parlamentar, Partido, SessaoLegislativa,
SituacaoMilitar, TipoAfastamento, TipoDependente, Votante)
SituacaoMilitar, TipoAfastamento, TipoDependente, Votante,
Bloco)
CargoMesaCrud = CrudAux.build(CargoMesa, 'cargo_mesa')
PartidoCrud = CrudAux.build(Partido, 'partidos')
TipoDependenteCrud = CrudAux.build(TipoDependente, 'tipo_dependente')
NivelInstrucaoCrud = CrudAux.build(NivelInstrucao, 'nivel_instrucao')
TipoAfastamentoCrud = CrudAux.build(TipoAfastamento, 'tipo_afastamento')
@ -57,6 +65,16 @@ class SessaoLegislativaCrud(CrudAux):
form_class = SessaoLegislativaForm
class PartidoCrud(CrudAux):
model = Partido
class CreateView(CrudAux.CreateView):
form_class = PartidoForm
class UpdateView(CrudAux.UpdateView):
form_class = PartidoForm
class VotanteView(MasterDetailCrud):
model = Votante
parent_field = 'parlamentar'
@ -154,6 +172,63 @@ class ProposicaoParlamentarCrud(CrudBaseForListAndDetailExternalAppView):
_('Texto Eletrônico'))
class PesquisarParlamentarView(FilterView):
model = Parlamentar
filterset_class = ParlamentarFilterSet
paginate_by = 10
def get_filterset_kwargs(self, filterset_class):
super(PesquisarParlamentarView,
self).get_filterset_kwargs(filterset_class)
kwargs = {'data': self.request.GET or None}
qs = self.get_queryset().order_by('nome_parlamentar').distinct()
kwargs.update({
'queryset': qs,
})
return kwargs
def get_context_data(self, **kwargs):
context = super(PesquisarParlamentarView,
self).get_context_data(**kwargs)
paginator = context['paginator']
page_obj = context['page_obj']
context['page_range'] = make_pagination(
page_obj.number, paginator.num_pages)
context['NO_ENTRIES_MSG'] = 'Nenhum parlamentar encontrado!'
context['title'] = _('Parlamentares')
return context
def get(self, request, *args, **kwargs):
super(PesquisarParlamentarView, self).get(request)
data = self.filterset.data
url = ''
if data:
url = "&" + str(self.request.META['QUERY_STRING'])
if url.startswith("&page"):
ponto_comeco = url.find('nome_parlamentar=') - 1
url = url[ponto_comeco:]
context = self.get_context_data(filter=self.filterset,
object_list=self.object_list,
filter_url=url,
numero_res=len(self.object_list)
)
context['show_results'] = show_results_filter_set(
self.request.GET.copy())
return self.render_to_response(context)
class ParticipacaoParlamentarCrud(CrudBaseForListAndDetailExternalAppView):
model = Participacao
parent_field = 'parlamentar'
@ -178,7 +253,6 @@ class ParticipacaoParlamentarCrud(CrudBaseForListAndDetailExternalAppView):
comissoes = []
for p in object_list:
if p.cargo.nome != 'Relator':
comissao = [
(p.composicao.comissao.nome, reverse(
'sapl.comissoes:comissao_detail', kwargs={
@ -593,7 +667,7 @@ class ParlamentarCrud(Crud):
.format(legislatura.data_fim, legislatura.data_fim, legislatura.data_fim))
row[1] = (
'O Parlamentar possui duas filiações conflitantes',
None)
None, None)
# Caso encontre UMA filiação nessas condições
else:
@ -678,6 +752,19 @@ class ParlamentarMateriasView(FormView):
})
def get_data_filicao(parlamentar):
return parlamentar.filiacao_set.order_by('-data').first().data.strftime('%d/%m/%Y')
def parlamentares_filiados(request, pk):
template_name = 'parlamentares/partido_filiados.html'
parlamentares = Parlamentar.objects.all()
partido = Partido.objects.get(pk=pk)
parlamentares_filiados = [(parlamentar, get_data_filicao(parlamentar)) for parlamentar in parlamentares if
parlamentar.filiacao_atual == partido.sigla]
return render(request, template_name, {'partido': partido, 'parlamentares': parlamentares_filiados})
class MesaDiretoraView(FormView):
template_name = 'parlamentares/composicaomesa_form.html'
success_url = reverse_lazy('sapl.parlamentares:mesa_diretora')
@ -738,9 +825,9 @@ class MesaDiretoraView(FormView):
parlamentares_ocupados = [m.parlamentar for m in mesa]
parlamentares_vagos = list(
set(
[p.parlamentar for p in parlamentares]) - set(
[p.parlamentar for p in parlamentares if p.parlamentar.ativo]) - set(
parlamentares_ocupados))
parlamentares_vagos.sort(key=lambda x: x.nome_parlamentar)
# Se todos os cargos estiverem ocupados, a listagem de parlamentares
# deve ser renderizada vazia
if not cargos_vagos:
@ -808,6 +895,7 @@ def altera_field_mesa(request):
[p.parlamentar for p in parlamentares]) - set(
parlamentares_ocupados))
parlamentares_vagos.sort(key=lambda x: x.nome_parlamentar)
lista_sessoes = [(s.id, s.__str__()) for s in sessoes]
lista_composicao = [(c.id, c.parlamentar.__str__(),
c.cargo.__str__()) for c in composicao_mesa]
@ -1027,7 +1115,20 @@ def altera_field_mesa_public_view(request):
partido_parlamentar_sessao_legislativa(sessao,
parlamentar))
if parlamentar.fotografia:
lista_fotos.append(parlamentar.fotografia.url)
try:
thumbnail_url = get_backend().get_thumbnail_url(
parlamentar.fotografia,
{
'size': (128, 128),
'box': parlamentar.cropping,
'crop': True,
'detail': True,
}
)
lista_fotos.append(thumbnail_url)
except Exception as e:
logger.error(e)
logger.error('erro processando arquivo: %s' % parlamentar.fotografia.path)
else:
lista_fotos.append(None)
@ -1039,3 +1140,40 @@ def altera_field_mesa_public_view(request):
'lista_fotos': lista_fotos,
'sessao_selecionada': sessao_selecionada,
'msg': ('', 1)})
class VincularParlamentarView(PermissionRequiredMixin, FormView):
logger = logging.getLogger(__name__)
form_class = VincularParlamentarForm
template_name = 'parlamentares/vincular_parlamentar.html'
permission_required = ('parlamentares.add_parlamentar', )
def get_success_url(self):
return reverse('sapl.parlamentares:parlamentar_list')
def form_valid(self, form):
kwargs = {
'parlamentar': form.cleaned_data['parlamentar'],
'legislatura': form.cleaned_data['legislatura'],
'data_inicio_mandato': form.cleaned_data['legislatura'].data_inicio,
'data_fim_mandato': form.cleaned_data['legislatura'].data_fim
}
data_expedicao_diploma = form.cleaned_data.get('data_expedicao_diploma')
if data_expedicao_diploma:
kwargs.update({'data_expedicao_diploma': data_expedicao_diploma})
mandato = Mandato.objects.create(**kwargs)
mandato.save()
return HttpResponseRedirect(self.get_success_url())
class BlocoCrud(CrudAux):
model = Bloco
class CreateView(CrudAux.CreateView):
form_class = BlocoForm
def get_success_url(self):
return reverse('sapl.parlamentares:bloco_list')

335
sapl/protocoloadm/forms.py

@ -2,19 +2,19 @@
import logging
from crispy_forms.bootstrap import InlineRadios, Alert
from crispy_forms.helper import FormHelper
from sapl.crispy_layout_mixin import SaplFormHelper
from crispy_forms.layout import HTML, Button, Column, Fieldset, Layout, Div
from django import forms
from django.core.exceptions import (MultipleObjectsReturned,
ObjectDoesNotExist, ValidationError)
from django.db import models
from django.db import models, transaction
from django.db.models import Max
from django.forms import ModelForm
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
import django_filters
from sapl.base.models import Autor, TipoAutor
from sapl.base.models import Autor, TipoAutor, AppConfig
from sapl.crispy_layout_mixin import SaplFormLayout, form_actions, to_row
from sapl.materia.models import (MateriaLegislativa, TipoMateriaLegislativa,
UnidadeTramitacao)
@ -23,12 +23,13 @@ from sapl.utils import (RANGE_ANOS, YES_NO_CHOICES, AnoNumeroOrderingFilter,
RangeWidgetOverride, autor_label, autor_modal,
choice_anos_com_protocolo, choice_force_optional,
choice_anos_com_documentoadministrativo,
FilterOverridesMetaMixin, choice_anos_com_materias)
FilterOverridesMetaMixin, choice_anos_com_materias,
FileFieldCheckMixin, lista_anexados)
from .models import (AcompanhamentoDocumento, DocumentoAcessorioAdministrativo,
DocumentoAdministrativo,
Protocolo, TipoDocumentoAdministrativo,
TramitacaoAdministrativo)
TramitacaoAdministrativo, Anexado)
TIPOS_PROTOCOLO = [('0', 'Recebido'), ('1', 'Enviado'),
@ -51,16 +52,14 @@ class AcompanhamentoDocumentoForm(ModelForm):
def __init__(self, *args, **kwargs):
row1 = to_row([('email', 10)])
row1 = to_row([('email', 12)])
row1.append(
Column(form_actions(label='Cadastrar'), css_class='col-md-2')
)
self.helper = FormHelper()
self.helper = SaplFormHelper()
self.helper.layout = Layout(
Fieldset(
_('Acompanhamento de Documento por e-mail'), row1
_('Acompanhamento de Documento por e-mail'),
row1,
form_actions(label='Cadastrar')
)
)
super(AcompanhamentoDocumentoForm, self).__init__(*args, **kwargs)
@ -136,7 +135,7 @@ class ProtocoloFilterSet(django_filters.FilterSet):
row5 = to_row(
[('tipo_processo', 6), ('o', 6)])
self.form.helper = FormHelper()
self.form.helper = SaplFormHelper()
self.form.helper.form_method = 'GET'
self.form.helper.layout = Layout(
Fieldset(_('Pesquisar Protocolo'),
@ -212,7 +211,7 @@ class DocumentoAdministrativoFilterSet(django_filters.FilterSet):
('tramitacaoadministrativo__unidade_tramitacao_destino', 5),
])
self.form.helper = FormHelper()
self.form.helper = SaplFormHelper()
self.form.helper.form_method = 'GET'
self.form.helper.layout = Layout(
Fieldset(_('Pesquisar Documento'),
@ -222,7 +221,7 @@ class DocumentoAdministrativoFilterSet(django_filters.FilterSet):
)
class AnularProcoloAdmForm(ModelForm):
class AnularProtocoloAdmForm(ModelForm):
logger = logging.getLogger(__name__)
@ -241,7 +240,7 @@ class AnularProcoloAdmForm(ModelForm):
widget=forms.Textarea)
def clean(self):
super(AnularProcoloAdmForm, self).clean()
super(AnularProtocoloAdmForm, self).clean()
cleaned_data = self.cleaned_data
@ -305,7 +304,7 @@ class AnularProcoloAdmForm(ModelForm):
row2 = to_row(
[('justificativa_anulacao', 12)])
self.helper = FormHelper()
self.helper = SaplFormHelper()
self.helper.layout = Layout(
Fieldset(_('Identificação do Protocolo'),
row1,
@ -314,7 +313,7 @@ class AnularProcoloAdmForm(ModelForm):
form_actions(label='Anular')
)
)
super(AnularProcoloAdmForm, self).__init__(
super(AnularProtocoloAdmForm, self).__init__(
*args, **kwargs)
@ -404,14 +403,21 @@ class ProtocoloDocumentForm(ModelForm):
row7 = to_row(
[('numero', 12)])
self.helper = FormHelper()
fieldset = Fieldset(_('Protocolo com data e hora informados manualmente'),
row3,
css_id='protocolo_data_hora_manual')
config = AppConfig.objects.first()
if not config.protocolo_manual:
row3 = to_row([(HTML("&nbsp;"), 12)])
fieldset = row3
self.helper = SaplFormHelper()
self.helper.layout = Layout(
Fieldset(_('Identificação de Documento'),
row1,
row2),
Fieldset(_('Protocolo com data e hora informados manualmente'),
row3,
css_id='protocolo_data_hora_manual'),
fieldset,
row4,
row5,
HTML("&nbsp;"),
@ -425,6 +431,10 @@ class ProtocoloDocumentForm(ModelForm):
super(ProtocoloDocumentForm, self).__init__(
*args, **kwargs)
if not config.protocolo_manual:
self.fields['data_hora_manual'].widget = forms.HiddenInput()
class ProtocoloMateriaForm(ModelForm):
@ -583,14 +593,21 @@ class ProtocoloMateriaForm(ModelForm):
row6 = to_row(
[('numero', 12)])
self.helper = FormHelper()
fieldset = Fieldset(_('Protocolo com data e hora informados manualmente'),
row3,
css_id='protocolo_data_hora_manual')
config = AppConfig.objects.first()
if not config.protocolo_manual:
row3 = to_row([(HTML("&nbsp;"), 12)])
fieldset = row3
self.helper = SaplFormHelper()
self.helper.layout = Layout(
Fieldset(_('Identificação da Matéria'),
row1,
row2),
Fieldset(_('Protocolo com data e hora informados manualmente'),
row3,
css_id='protocolo_data_hora_manual'),
fieldset,
row4,
row5,
HTML("&nbsp;"),
@ -604,8 +621,11 @@ class ProtocoloMateriaForm(ModelForm):
super(ProtocoloMateriaForm, self).__init__(
*args, **kwargs)
if not config.protocolo_manual:
self.fields['data_hora_manual'].widget = forms.HiddenInput()
class DocumentoAcessorioAdministrativoForm(ModelForm):
class DocumentoAcessorioAdministrativoForm(FileFieldCheckMixin, ModelForm):
class Meta:
model = DocumentoAcessorioAdministrativo
@ -630,11 +650,29 @@ class TramitacaoAdmForm(ModelForm):
fields = ['data_tramitacao',
'unidade_tramitacao_local',
'status',
'urgente',
'unidade_tramitacao_destino',
'data_encaminhamento',
'data_fim_prazo',
'texto',
]
'user',
'ip']
widgets = {'user': forms.HiddenInput(),
'ip': forms.HiddenInput()}
def __init__(self, *args, **kwargs):
super(TramitacaoAdmForm, self).__init__(*args, **kwargs)
self.fields['data_tramitacao'].initial = timezone.now().date()
ust = UnidadeTramitacao.objects.select_related().all()
unidade_tramitacao_destino = [('', '---------')] + [(ut.pk, ut)
for ut in ust if ut.comissao and ut.comissao.ativa]
unidade_tramitacao_destino.extend(
[(ut.pk, ut) for ut in ust if ut.orgao])
unidade_tramitacao_destino.extend(
[(ut.pk, ut) for ut in ust if ut.parlamentar])
self.fields['unidade_tramitacao_destino'].choices = unidade_tramitacao_destino
self.fields['urgente'].label = "Urgente? *"
def clean(self):
cleaned_data = super(TramitacaoAdmForm, self).clean()
@ -707,6 +745,51 @@ class TramitacaoAdmForm(ModelForm):
return self.cleaned_data
@transaction.atomic
def save(self, commit=True):
tramitacao = super(TramitacaoAdmForm, self).save(commit)
documento = tramitacao.documento
documento.tramitacao = False if tramitacao.status.indicador == "F" else True
documento.save()
lista_tramitacao = []
list_anexados = lista_anexados(documento, False)
for da in list_anexados:
if not da.tramitacaoadministrativo_set.all() \
or da.tramitacaoadministrativo_set.last() \
.unidade_tramitacao_destino == tramitacao.unidade_tramitacao_local:
da.tramitacao = False if tramitacao.status.indicador == "F" else True
da.save()
lista_tramitacao.append(TramitacaoAdministrativo(
status=tramitacao.status,
documento=da,
data_tramitacao=tramitacao.data_tramitacao,
unidade_tramitacao_local=tramitacao.unidade_tramitacao_local,
data_encaminhamento=tramitacao.data_encaminhamento,
unidade_tramitacao_destino=tramitacao.unidade_tramitacao_destino,
urgente=tramitacao.urgente,
texto=tramitacao.texto,
data_fim_prazo=tramitacao.data_fim_prazo,
user=tramitacao.user,
ip=tramitacao.ip
))
TramitacaoAdministrativo.objects.bulk_create(lista_tramitacao)
return tramitacao
# Compara se os campos de duas tramitações são iguais,
# exceto os campos id, documento_id e timestamp
def compara_tramitacoes_doc(tramitacao1, tramitacao2):
if not tramitacao1 or not tramitacao2:
return False
lst_items = ['id', 'documento_id', 'timestamp']
values = [(k,v) for k,v in tramitacao1.__dict__.items() if ((k not in lst_items) and (k[0] != '_'))]
other_values = [(k,v) for k,v in tramitacao2.__dict__.items() if (k not in lst_items and k[0] != '_')]
return values == other_values
class TramitacaoAdmEditForm(TramitacaoAdmForm):
@ -723,11 +806,15 @@ class TramitacaoAdmEditForm(TramitacaoAdmForm):
fields = ['data_tramitacao',
'unidade_tramitacao_local',
'status',
'urgente',
'unidade_tramitacao_destino',
'data_encaminhamento',
'data_fim_prazo',
'texto',
]
'user',
'ip']
widgets = {'user': forms.HiddenInput(),
'ip': forms.HiddenInput()}
def clean(self):
super(TramitacaoAdmEditForm, self).clean()
@ -735,33 +822,193 @@ class TramitacaoAdmEditForm(TramitacaoAdmForm):
if not self.is_valid():
return self.cleaned_data
cd = self.cleaned_data
obj = self.instance
ultima_tramitacao = TramitacaoAdministrativo.objects.filter(
documento_id=self.instance.documento_id).order_by(
documento_id=obj.documento_id).order_by(
'-data_tramitacao',
'-id').first()
# Se a Tramitação que está sendo editada não for a mais recente,
# ela não pode ter seu destino alterado.
if ultima_tramitacao != self.instance:
if self.cleaned_data['unidade_tramitacao_destino'] != \
self.instance.unidade_tramitacao_destino:
if ultima_tramitacao != obj:
if cd['unidade_tramitacao_destino'] != \
obj.unidade_tramitacao_destino:
self.logger.error('Você não pode mudar a Unidade de Destino desta '
'tramitação (id={}), pois irá conflitar com a Unidade '
'Local da tramitação seguinte'.format(self.instance.documento_id))
'Local da tramitação seguinte'.format(obj.documento_id))
raise ValidationError(
'Você não pode mudar a Unidade de Destino desta '
'tramitação, pois irá conflitar com a Unidade '
'Local da tramitação seguinte')
self.cleaned_data['data_tramitacao'] = \
self.instance.data_tramitacao
self.cleaned_data['unidade_tramitacao_local'] = \
self.instance.unidade_tramitacao_local
# Se não houve qualquer alteração em um dos dados, mantém o usuário e ip
if not (cd['data_tramitacao'] != obj.data_tramitacao or \
cd['unidade_tramitacao_destino'] != obj.unidade_tramitacao_destino or \
cd['status'] != obj.status or cd['texto'] != obj.texto or \
cd['data_encaminhamento'] != obj.data_encaminhamento or \
cd['data_fim_prazo'] != obj.data_fim_prazo):
cd['user'] = obj.user
cd['ip'] = obj.ip
cd['data_tramitacao'] = obj.data_tramitacao
cd['unidade_tramitacao_local'] = obj.unidade_tramitacao_local
return cd
@transaction.atomic
def save(self, commit=True):
# tram_principal = super(TramitacaoAdmEditForm, self).save(commit)
ant_tram_principal = TramitacaoAdministrativo.objects.get(id=self.instance.id)
nova_tram_principal = super(TramitacaoAdmEditForm, self).save(commit)
documento = nova_tram_principal.documento
documento.tramitacao = False if nova_tram_principal.status.indicador == "F" else True
documento.save()
list_anexados = lista_anexados(documento, False)
for da in list_anexados:
tram_anexada = da.tramitacaoadministrativo_set.last()
if compara_tramitacoes_doc(ant_tram_principal, tram_anexada):
tram_anexada.status = nova_tram_principal.status
tram_anexada.data_tramitacao = nova_tram_principal.data_tramitacao
tram_anexada.unidade_tramitacao_local = nova_tram_principal.unidade_tramitacao_local
tram_anexada.data_encaminhamento = nova_tram_principal.data_encaminhamento
tram_anexada.unidade_tramitacao_destino = nova_tram_principal.unidade_tramitacao_destino
tram_anexada.urgente = nova_tram_principal.urgente
tram_anexada.texto = nova_tram_principal.texto
tram_anexada.data_fim_prazo = nova_tram_principal.data_fim_prazo
tram_anexada.user = nova_tram_principal.user
tram_anexada.ip = nova_tram_principal.ip
tram_anexada.save()
da.tramitacao = False if nova_tram_principal.status.indicador == "F" else True
da.save()
return nova_tram_principal
class AnexadoForm(ModelForm):
logger = logging.getLogger(__name__)
tipo = forms.ModelChoiceField(
label='Tipo',
required=True,
queryset=TipoDocumentoAdministrativo.objects.all(),
empty_label='Selecione'
)
numero = forms.CharField(label='Número', required=True)
ano = forms.CharField(label='Ano', required=True)
def __init__(self, *args, **kwargs):
return super(AnexadoForm, self).__init__(*args, **kwargs)
def clean(self):
super(AnexadoForm, self).clean()
if not self.is_valid():
return self.cleaned_data
cleaned_data = self.cleaned_data
data_anexacao = cleaned_data['data_anexacao']
data_desanexacao = cleaned_data['data_desanexacao'] if cleaned_data['data_desanexacao'] else data_anexacao
if data_anexacao > data_desanexacao:
self.logger.error("Data de anexação posterior à data de desanexação.")
raise ValidationError(_("Data de anexação posterior à data de desanexação."))
try:
self.logger.info(
"Tentando obter objeto DocumentoAdministrativo (numero={}, ano={}, tipo={})."
.format(cleaned_data['numero'], cleaned_data['ano'], cleaned_data['tipo'])
)
documento_anexado = DocumentoAdministrativo.objects.get(
numero=cleaned_data['numero'],
ano=cleaned_data['ano'],
tipo=cleaned_data['tipo']
)
except ObjectDoesNotExist:
msg = _('O {} {}/{} não existe no cadastro de documentos administrativos.'
.format(cleaned_data['tipo'], cleaned_data['numero'], cleaned_data['ano']))
self.logger.error("O documento a ser anexado não existe no cadastro"
" de documentos administrativos")
raise ValidationError(msg)
documento_principal = self.instance.documento_principal
if documento_principal == documento_anexado:
self.logger.error("O documento não pode ser anexado a si mesmo.")
raise ValidationError(_("O documento não pode ser anexado a si mesmo"))
is_anexado = Anexado.objects.filter(documento_principal=documento_principal,
documento_anexado=documento_anexado
).exclude(pk=self.instance.pk).exists()
if is_anexado:
self.logger.error("Documento já se encontra anexado.")
raise ValidationError(_('Documento já se encontra anexado'))
ciclico = False
anexados_anexado = Anexado.objects.filter(documento_principal=documento_anexado)
while(anexados_anexado and not ciclico):
anexados = []
for anexo in anexados_anexado:
if documento_principal == anexo.documento_anexado:
ciclico = True
else:
for a in Anexado.objects.filter(documento_principal=anexo.documento_anexado):
anexados.append(a)
anexados_anexado = anexados
if ciclico:
self.logger.error("O documento não pode ser anexado por um de seus anexados.")
raise ValidationError(_('O documento não pode ser anexado por um de seus anexados'))
cleaned_data['documento_anexado'] = documento_anexado
return cleaned_data
def save(self, commit=False):
anexado = super(AnexadoForm, self).save(commit)
anexado.documento_anexado = self.cleaned_data['documento_anexado']
anexado.save()
return anexado
class Meta:
model = Anexado
fields = ['tipo', 'numero', 'ano', 'data_anexacao', 'data_desanexacao']
class AnexadoEmLoteFilterSet(django_filters.FilterSet):
class Meta(FilterOverridesMetaMixin):
model = DocumentoAdministrativo
fields = ['tipo', 'data']
def __init__(self, *args, **kwargs):
super(AnexadoEmLoteFilterSet, self).__init__(*args, **kwargs)
self.filters['tipo'].label = 'Tipo de Documento*'
self.filters['data'].label = 'Data (Inicial - Final)*'
row1 = to_row([('tipo', 12)])
row2 = to_row([('data', 12)])
self.form.helper = SaplFormHelper()
self.form.helper.form_method = 'GET'
self.form.helper.layout = Layout(
Fieldset(_('Pesquisa de Documentos'),
row1, row2, form_actions(label='Pesquisar'))
)
class DocumentoAdministrativoForm(ModelForm):
class DocumentoAdministrativoForm(FileFieldCheckMixin, ModelForm):
logger = logging.getLogger(__name__)
@ -910,7 +1157,7 @@ class DocumentoAdministrativoForm(ModelForm):
row7 = to_row(
[('observacao', 12)])
self.helper = FormHelper()
self.helper = SaplFormHelper()
self.helper.layout = SaplFormLayout(
Fieldset(_('Identificação Básica'),
row1, row2, row3, row4, row5),
@ -978,7 +1225,7 @@ class DesvincularDocumentoForm(ModelForm):
('ano', 4),
('tipo', 4)])
self.helper = FormHelper()
self.helper = SaplFormHelper()
self.helper.layout = Layout(
Fieldset(_('Identificação do Documento'),
row1,
@ -1043,7 +1290,7 @@ class DesvincularMateriaForm(forms.Form):
('ano', 4),
('tipo', 4)])
self.helper = FormHelper()
self.helper = SaplFormHelper()
self.helper.layout = Layout(
Fieldset(_('Identificação da Matéria'),
row1,
@ -1111,7 +1358,7 @@ class FichaPesquisaAdmForm(forms.Form):
('data_inicial', 3),
('data_final', 3)])
self.helper = FormHelper()
self.helper = SaplFormHelper()
self.helper.layout = Layout(
Fieldset(
('Formulário de Ficha'),
@ -1152,7 +1399,7 @@ class FichaSelecionaAdmForm(forms.Form):
row1 = to_row(
[('documento', 12)])
self.helper = FormHelper()
self.helper = SaplFormHelper()
self.helper.layout = Layout(
Fieldset(
('Selecione a ficha que deseja imprimir'),

35
sapl/protocoloadm/migrations/0018_auto_20190314_1532.py

@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-03-14 18:32
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('protocoloadm', '0017_merge_20190121_1552'),
]
operations = [
migrations.CreateModel(
name='Anexado',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('data_anexacao', models.DateField(verbose_name='Data Anexação')),
('data_desanexacao', models.DateField(blank=True, null=True, verbose_name='Data Desanexação')),
('documento_anexado', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documento_anexado_set', to='protocoloadm.DocumentoAdministrativo', verbose_name='Documento Anexado')),
('documento_principal', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documento_principal_set', to='protocoloadm.DocumentoAdministrativo', verbose_name='Documento Principal')),
],
options={
'verbose_name': 'Anexado',
'verbose_name_plural': 'Anexados',
},
),
migrations.AddField(
model_name='documentoadministrativo',
name='anexados',
field=models.ManyToManyField(blank=True, related_name='anexo_de', through='protocoloadm.Anexado', to='protocoloadm.DocumentoAdministrativo'),
),
]

28
sapl/protocoloadm/migrations/0019_auto_20190426_0833.py

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-04-26 11:33
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('protocoloadm', '0018_auto_20190314_1532'),
]
operations = [
migrations.AddField(
model_name='tramitacaoadministrativo',
name='ip',
field=models.CharField(blank=True, default='', max_length=30, verbose_name='IP'),
),
migrations.AddField(
model_name='tramitacaoadministrativo',
name='user',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='Usuário'),
),
]

25
sapl/protocoloadm/migrations/0019_auto_20190429_0828.py

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-04-29 11:28
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('protocoloadm', '0018_auto_20190314_1532'),
]
operations = [
migrations.AddField(
model_name='tramitacaoadministrativo',
name='urgente',
field=models.BooleanField(choices=[(True, 'Sim'), (False, 'Não')], default=False, verbose_name='Urgente ?'),
),
migrations.AlterField(
model_name='tramitacaoadministrativo',
name='texto',
field=models.TextField(verbose_name='Texto da Ação'),
),
]

21
sapl/protocoloadm/migrations/0020_tramitacaoadministrativo_timestamp.py

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-04-29 12:15
from __future__ import unicode_literals
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('protocoloadm', '0019_auto_20190429_0828'),
]
operations = [
migrations.AddField(
model_name='tramitacaoadministrativo',
name='timestamp',
field=models.DateTimeField(default=django.utils.timezone.now),
),
]

16
sapl/protocoloadm/migrations/0021_merge_20190429_1531.py

@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-04-29 18:31
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('protocoloadm', '0020_tramitacaoadministrativo_timestamp'),
('protocoloadm', '0019_auto_20190426_0833'),
]
operations = [
]

61
sapl/protocoloadm/models.py

@ -6,7 +6,8 @@ import reversion
from sapl.base.models import Autor
from sapl.materia.models import TipoMateriaLegislativa, UnidadeTramitacao
from sapl.utils import RANGE_ANOS, YES_NO_CHOICES, texto_upload_path
from sapl.utils import (RANGE_ANOS, YES_NO_CHOICES, texto_upload_path,
get_settings_auth_user_model)
@reversion.register()
@ -170,6 +171,18 @@ class DocumentoAdministrativo(models.Model):
verbose_name=_('Acesso Restrito'),
blank=True)
anexados = models.ManyToManyField(
'self',
blank=True,
through='Anexado',
symmetrical=False,
related_name='anexo_de',
through_fields=(
'documento_principal',
'documento_anexado'
)
)
class Meta:
verbose_name = _('Documento Administrativo')
verbose_name_plural = _('Documentos Administrativos')
@ -288,6 +301,7 @@ class TramitacaoAdministrativo(models.Model):
verbose_name=_('Status'))
documento = models.ForeignKey(DocumentoAdministrativo,
on_delete=models.PROTECT)
timestamp = models.DateTimeField(default=timezone.now)
data_tramitacao = models.DateField(
verbose_name=_('Data Tramitação'))
unidade_tramitacao_local = models.ForeignKey(
@ -302,10 +316,21 @@ class TramitacaoAdministrativo(models.Model):
related_name='adm_tramitacoes_destino',
on_delete=models.PROTECT,
verbose_name=_('Unidade Destino'))
texto = models.TextField(
blank=True, verbose_name=_('Texto da Ação'))
urgente = models.BooleanField(verbose_name=_('Urgente ?'),
choices=YES_NO_CHOICES,
default=False)
texto = models.TextField(verbose_name=_('Texto da Ação'))
data_fim_prazo = models.DateField(
blank=True, null=True, verbose_name=_('Data Fim do Prazo'))
user = models.ForeignKey(get_settings_auth_user_model(),
verbose_name=_('Usuário'),
on_delete=models.PROTECT,
null=True,
blank=True)
ip = models.CharField(verbose_name=_('IP'),
max_length=30,
blank=True,
default='')
class Meta:
verbose_name = _('Tramitação de Documento Administrativo')
@ -317,6 +342,36 @@ class TramitacaoAdministrativo(models.Model):
}
@reversion.register()
class Anexado(models.Model):
documento_principal = models.ForeignKey(
DocumentoAdministrativo, related_name='documento_principal_set',
on_delete = models.CASCADE,
verbose_name=_('Documento Principal')
)
documento_anexado = models.ForeignKey(
DocumentoAdministrativo, related_name='documento_anexado_set',
on_delete = models.CASCADE,
verbose_name=_('Documento Anexado')
)
data_anexacao = models.DateField(verbose_name=_('Data Anexação'))
data_desanexacao = models.DateField(
blank=True, null=True, verbose_name=_('Data Desanexação')
)
class Meta:
verbose_name = _('Anexado')
verbose_name_plural = _('Anexados')
def __str__(self):
return _('Anexado: %(documento_anexado_tipo)s %(documento_anexado_numero)s'
'/%(documento_anexado_ano)s\n') % {
'documento_anexado_tipo': self.documento_anexado.tipo,
'documento_anexado_numero': self.documento_anexado.numero,
'documento_anexado_ano': self.documento_anexado.ano
}
@reversion.register()
class AcompanhamentoDocumento(models.Model):
usuario = models.CharField(max_length=50)

295
sapl/protocoloadm/tests/test_protocoloadm.py

@ -7,16 +7,21 @@ from django.utils.translation import ugettext_lazy as _
from model_mommy import mommy
import pytest
from sapl.base.models import AppConfig
from sapl.comissoes.models import Comissao, TipoComissao
from sapl.materia.models import UnidadeTramitacao
from sapl.protocoloadm.forms import (AnularProcoloAdmForm,
from sapl.protocoloadm.forms import (AnularProtocoloAdmForm,
DocumentoAdministrativoForm,
MateriaLegislativa, ProtocoloDocumentForm,
ProtocoloMateriaForm)
ProtocoloMateriaForm, TramitacaoAdmForm,
TramitacaoAdmEditForm,
compara_tramitacoes_doc)
from sapl.protocoloadm.models import (DocumentoAdministrativo, Protocolo,
StatusTramitacaoAdministrativo,
TipoDocumentoAdministrativo,
TipoMateriaLegislativa,
TipoMateriaLegislativa, Anexado,
TramitacaoAdministrativo)
from sapl.utils import lista_anexados
@pytest.mark.django_db(transaction=False)
@ -50,7 +55,7 @@ def test_anular_protocolo_submit(admin_client):
@pytest.mark.django_db(transaction=False)
def test_form_anular_protocolo_inexistente():
form = AnularProcoloAdmForm({'numero': '1',
form = AnularProtocoloAdmForm({'numero': '1',
'ano': '2016',
'justificativa_anulacao': 'TESTE'})
@ -63,7 +68,7 @@ def test_form_anular_protocolo_inexistente():
@pytest.mark.django_db(transaction=False)
def test_form_anular_protocolo_valido():
mommy.make(Protocolo, numero='1', ano='2016', anulado=False)
form = AnularProcoloAdmForm({'numero': '1',
form = AnularProtocoloAdmForm({'numero': '1',
'ano': '2016',
'justificativa_anulacao': 'TESTE'})
if not form.is_valid():
@ -73,7 +78,7 @@ def test_form_anular_protocolo_valido():
@pytest.mark.django_db(transaction=False)
def test_form_anular_protocolo_anulado():
mommy.make(Protocolo, numero='1', ano='2016', anulado=True)
form = AnularProcoloAdmForm({'numero': '1',
form = AnularProtocoloAdmForm({'numero': '1',
'ano': '2016',
'justificativa_anulacao': 'TESTE'})
assert form.errors['__all__'] == \
@ -87,7 +92,7 @@ def test_form_anular_protocolo_campos_obrigatorios():
# TODO: generalizar para diminuir o tamanho deste método
# numero ausente
form = AnularProcoloAdmForm({'numero': '',
form = AnularProtocoloAdmForm({'numero': '',
'ano': '2016',
'justificativa_anulacao': 'TESTE'})
if form.is_valid():
@ -97,7 +102,7 @@ def test_form_anular_protocolo_campos_obrigatorios():
assert form.errors['numero'] == [_('Este campo é obrigatório.')]
# ano ausente
form = AnularProcoloAdmForm({'numero': '1',
form = AnularProtocoloAdmForm({'numero': '1',
'ano': '',
'justificativa_anulacao': 'TESTE'})
if form.is_valid():
@ -107,7 +112,7 @@ def test_form_anular_protocolo_campos_obrigatorios():
assert form.errors['ano'] == [_('Este campo é obrigatório.')]
# justificativa_anulacao ausente
form = AnularProcoloAdmForm({'numero': '1',
form = AnularProtocoloAdmForm({'numero': '1',
'ano': '2016',
'justificativa_anulacao': ''})
if form.is_valid():
@ -156,6 +161,8 @@ def test_create_tramitacao(admin_client):
'unidade_tramitacao_destino': unidade_tramitacao_local_1.pk,
'documento': documento_adm.pk,
'status': status.pk,
'urgente': False,
'texto': 'teste',
'data_tramitacao': date(2016, 8, 21)},
follow=True)
@ -174,6 +181,8 @@ def test_create_tramitacao(admin_client):
'unidade_tramitacao_destino': unidade_tramitacao_destino_2.pk,
'documento': documento_adm.pk,
'status': status.pk,
'urgente': False,
'texto': 'teste',
'data_tramitacao': date(2016, 8, 20)},
follow=True)
@ -192,6 +201,8 @@ def test_create_tramitacao(admin_client):
'unidade_tramitacao_destino': unidade_tramitacao_destino_2.pk,
'documento': documento_adm.pk,
'status': status.pk,
'urgente': False,
'texto': 'teste',
'data_tramitacao': timezone.now().date() + timedelta(
days=1)},
follow=True)
@ -211,6 +222,8 @@ def test_create_tramitacao(admin_client):
'unidade_tramitacao_destino': unidade_tramitacao_destino_2.pk,
'documento': documento_adm.pk,
'status': status.pk,
'urgente': False,
'texto': 'teste',
'data_tramitacao': date(2016, 8, 21),
'data_encaminhamento': date(2016, 8, 20)},
follow=True)
@ -230,6 +243,8 @@ def test_create_tramitacao(admin_client):
'unidade_tramitacao_destino': unidade_tramitacao_destino_2.pk,
'documento': documento_adm.pk,
'status': status.pk,
'urgente': False,
'texto': 'teste',
'data_tramitacao': date(2016, 8, 21),
'data_fim_prazo': date(2016, 8, 20)},
follow=True)
@ -249,6 +264,8 @@ def test_create_tramitacao(admin_client):
'unidade_tramitacao_destino': unidade_tramitacao_destino_2.pk,
'documento': documento_adm.pk,
'status': status.pk,
'urgente': False,
'texto': 'teste',
'data_tramitacao': date(2016, 8, 21)},
follow=True)
@ -260,7 +277,7 @@ def test_create_tramitacao(admin_client):
@pytest.mark.django_db(transaction=False)
def test_anular_protocolo_dados_invalidos():
form = AnularProcoloAdmForm(data={})
form = AnularProtocoloAdmForm(data={})
assert not form.is_valid()
@ -275,7 +292,7 @@ def test_anular_protocolo_dados_invalidos():
@pytest.mark.django_db(transaction=False)
def test_anular_protocolo_form_anula_protocolo_inexistente():
form = AnularProcoloAdmForm(data={'numero': '1',
form = AnularProtocoloAdmForm(data={'numero': '1',
'ano': '2017',
'justificativa_anulacao': 'teste'
})
@ -290,7 +307,7 @@ def test_anular_protocolo_form_anula_protocolo_inexistente():
def test_anular_protocolo_form_anula_protocolo_anulado():
mommy.make(Protocolo, numero=1, ano=2017, anulado=True)
form = AnularProcoloAdmForm(data={'numero': '1',
form = AnularProtocoloAdmForm(data={'numero': '1',
'ano': '2017',
'justificativa_anulacao': 'teste'
})
@ -315,7 +332,7 @@ def test_anular_protocolo_form_anula_protocolo_com_doc_vinculado():
ano=2017,
numero_protocolo=1)
form = AnularProcoloAdmForm(data={'numero': '1',
form = AnularProtocoloAdmForm(data={'numero': '1',
'ano': '2017',
'justificativa_anulacao': 'teste'
})
@ -337,7 +354,7 @@ def test_anular_protocolo_form_anula_protocolo_com_doc_vinculado():
mommy.make(DocumentoAdministrativo,
protocolo=protocolo_documento)
form = AnularProcoloAdmForm(data={'numero': '2',
form = AnularProtocoloAdmForm(data={'numero': '2',
'ano': '2017',
'justificativa_anulacao': 'teste'
})
@ -390,8 +407,11 @@ def test_documento_administrativo_protocolo_inexistente():
assert form.errors['__all__'] == [_('Protocolo 11/2017 inexistente.')]
@pytest.mark.django_db(transaction=False)
def test_protocolo_documento_form_invalido():
config = mommy.make(AppConfig)
form = ProtocoloDocumentForm(
data={},
initial={
@ -414,8 +434,11 @@ def test_protocolo_documento_form_invalido():
assert len(errors) == 6
@pytest.mark.django_db(transaction=False)
def test_protocolo_materia_invalido():
config = mommy.make(AppConfig)
form = ProtocoloMateriaForm(data={},
initial={
'user_data_hora_manual': '',
@ -436,3 +459,247 @@ def test_protocolo_materia_invalido():
assert errors['vincular_materia'] == [_('Este campo é obrigatório.')]
assert len(errors) == 7
@pytest.mark.django_db(transaction=False)
def test_lista_documentos_anexados():
tipo_documento = mommy.make(
TipoDocumentoAdministrativo,
descricao="Tipo_Teste"
)
documento_principal = mommy.make(
DocumentoAdministrativo,
numero=20,
ano=2018,
data="2018-01-04",
tipo=tipo_documento
)
documento_anexado = mommy.make(
DocumentoAdministrativo,
numero=21,
ano=2019,
data="2019-05-04",
tipo=tipo_documento
)
documento_anexado_anexado = mommy.make(
DocumentoAdministrativo,
numero=22,
ano=2020,
data="2020-01-05",
tipo=tipo_documento
)
mommy.make(
Anexado,
documento_principal=documento_principal,
documento_anexado=documento_anexado,
data_anexacao="2019-05-11"
)
mommy.make(
Anexado,
documento_principal=documento_anexado,
documento_anexado=documento_anexado_anexado,
data_anexacao="2020-11-05"
)
lista = lista_anexados(documento_principal, False)
assert len(lista) == 2
assert lista[0] == documento_anexado
assert lista[1] == documento_anexado_anexado
@pytest.mark.django_db(transaction=False)
def make_unidade_tramitacao(descricao):
# Cria uma comissão para ser a unidade de tramitação
tipo_comissao = mommy.make(TipoComissao)
comissao = mommy.make(Comissao,
tipo=tipo_comissao,
nome=descricao,
sigla='T',
data_criacao='2016-03-21')
# Testa a comissão
assert comissao.tipo == tipo_comissao
assert comissao.nome == descricao
# Cria a unidade
unidade = mommy.make(UnidadeTramitacao, comissao=comissao)
assert unidade.comissao == comissao
return unidade
@pytest.mark.django_db(transaction=False)
def test_tramitacoes_documentos_anexados(admin_client):
tipo_documento = mommy.make(
TipoDocumentoAdministrativo,
descricao="Tipo_Teste"
)
documento_principal = mommy.make(
DocumentoAdministrativo,
numero=20,
ano=2018,
data="2018-01-04",
tipo=tipo_documento
)
documento_anexado = mommy.make(
DocumentoAdministrativo,
numero=21,
ano=2019,
data="2019-05-04",
tipo=tipo_documento
)
documento_anexado_anexado = mommy.make(
DocumentoAdministrativo,
numero=22,
ano=2020,
data="2020-01-05",
tipo=tipo_documento
)
mommy.make(
Anexado,
documento_principal=documento_principal,
documento_anexado=documento_anexado,
data_anexacao="2019-05-11"
)
mommy.make(
Anexado,
documento_principal=documento_anexado,
documento_anexado=documento_anexado_anexado,
data_anexacao="2020-11-05"
)
unidade_tramitacao_local_1 = make_unidade_tramitacao(descricao="Teste 1")
unidade_tramitacao_destino_1 = make_unidade_tramitacao(descricao="Teste 2")
unidade_tramitacao_destino_2 = make_unidade_tramitacao(descricao="Teste 3")
status = mommy.make(
StatusTramitacaoAdministrativo,
indicador='R')
# Teste criação de Tramitacao
form = TramitacaoAdmForm(data={})
form.data = {'data_tramitacao':date(2019, 5, 6),
'unidade_tramitacao_local':unidade_tramitacao_local_1.pk,
'unidade_tramitacao_destino':unidade_tramitacao_destino_1.pk,
'status':status.pk,
'urgente': False,
'texto': "Texto de teste"}
form.instance.documento_id=documento_principal.pk
assert form.is_valid()
tramitacao_principal = form.save()
tramitacao_anexada = documento_anexado.tramitacaoadministrativo_set.last()
tramitacao_anexada_anexada = documento_anexado_anexado.tramitacaoadministrativo_set.last()
# Verifica se foram criadas as tramitações para os documentos anexados e anexados aos anexados
assert documento_principal.tramitacaoadministrativo_set.last() == tramitacao_principal
assert tramitacao_principal.documento.tramitacao == (tramitacao_principal.status.indicador != "F")
assert compara_tramitacoes_doc(tramitacao_principal, tramitacao_anexada)
assert DocumentoAdministrativo.objects.get(id=documento_anexado.pk).tramitacao \
== (tramitacao_anexada.status.indicador != "F")
assert compara_tramitacoes_doc(tramitacao_anexada_anexada, tramitacao_principal)
assert DocumentoAdministrativo.objects.get(id=documento_anexado_anexado.pk).tramitacao \
== (tramitacao_anexada_anexada.status.indicador != "F")
# Teste Edição de Tramitacao
form = TramitacaoAdmEditForm(data={})
# Alterando unidade_tramitacao_destino
form.data = {'data_tramitacao':tramitacao_principal.data_tramitacao,
'unidade_tramitacao_local':tramitacao_principal.unidade_tramitacao_local.pk,
'unidade_tramitacao_destino':unidade_tramitacao_destino_2.pk,
'status':tramitacao_principal.status.pk,
'urgente': tramitacao_principal.urgente,
'texto': tramitacao_principal.texto}
form.instance = tramitacao_principal
assert form.is_valid()
tramitacao_principal = form.save()
tramitacao_anexada = documento_anexado.tramitacaoadministrativo_set.last()
tramitacao_anexada_anexada = documento_anexado_anexado.tramitacaoadministrativo_set.last()
assert tramitacao_principal.unidade_tramitacao_destino == unidade_tramitacao_destino_2
assert tramitacao_anexada.unidade_tramitacao_destino == unidade_tramitacao_destino_2
assert tramitacao_anexada_anexada.unidade_tramitacao_destino == unidade_tramitacao_destino_2
# Teste Remoção de Tramitacao
url = reverse('sapl.protocoloadm:tramitacaoadministrativo_delete',
kwargs={'pk': tramitacao_principal.pk})
response = admin_client.post(url, {'confirmar':'confirmar'} ,follow=True)
assert TramitacaoAdministrativo.objects.filter(id=tramitacao_principal.pk).count() == 0
assert TramitacaoAdministrativo.objects.filter(id=tramitacao_anexada.pk).count() == 0
assert TramitacaoAdministrativo.objects.filter(id=tramitacao_anexada_anexada.pk).count() == 0
# Testes para quando as tramitações das anexadas divergem
form = TramitacaoAdmForm(data={})
form.data = {'data_tramitacao':date(2019, 5, 6),
'unidade_tramitacao_local':unidade_tramitacao_local_1.pk,
'unidade_tramitacao_destino':unidade_tramitacao_destino_1.pk,
'status':status.pk,
'urgente': False,
'texto': "Texto de teste"}
form.instance.documento_id=documento_principal.pk
assert form.is_valid()
tramitacao_principal = form.save()
tramitacao_anexada = documento_anexado.tramitacaoadministrativo_set.last()
tramitacao_anexada_anexada = documento_anexado_anexado.tramitacaoadministrativo_set.last()
form = TramitacaoAdmEditForm(data={})
# Alterando unidade_tramitacao_destino
form.data = {'data_tramitacao':tramitacao_anexada.data_tramitacao,
'unidade_tramitacao_local':tramitacao_anexada.unidade_tramitacao_local.pk,
'unidade_tramitacao_destino':unidade_tramitacao_destino_2.pk,
'status':tramitacao_anexada.status.pk,
'urgente': tramitacao_anexada.urgente,
'texto': tramitacao_anexada.texto}
form.instance = tramitacao_anexada
assert form.is_valid()
tramitacao_anexada = form.save()
tramitacao_anexada_anexada = documento_anexado_anexado.tramitacaoadministrativo_set.last()
assert tramitacao_principal.unidade_tramitacao_destino == unidade_tramitacao_destino_1
assert tramitacao_anexada.unidade_tramitacao_destino == unidade_tramitacao_destino_2
assert tramitacao_anexada_anexada.unidade_tramitacao_destino == unidade_tramitacao_destino_2
# Editando a tramitação principal, as tramitações anexadas não devem ser editadas
form = TramitacaoAdmEditForm(data={})
# Alterando o texto
form.data = {'data_tramitacao':tramitacao_principal.data_tramitacao,
'unidade_tramitacao_local':tramitacao_principal.unidade_tramitacao_local.pk,
'unidade_tramitacao_destino':tramitacao_principal.unidade_tramitacao_destino.pk,
'status':tramitacao_principal.status.pk,
'urgente': tramitacao_principal.urgente,
'texto': "Testando a alteração"}
form.instance = tramitacao_principal
assert form.is_valid()
tramitacao_principal = form.save()
tramitacao_anexada = documento_anexado.tramitacaoadministrativo_set.last()
tramitacao_anexada_anexada = documento_anexado_anexado.tramitacaoadministrativo_set.last()
assert tramitacao_principal.texto == "Testando a alteração"
assert not tramitacao_anexada.texto == "Testando a alteração"
assert not tramitacao_anexada_anexada.texto == "Testando a alteração"
# Removendo a tramitação pricipal, as tramitações anexadas não devem ser removidas, pois divergiram
url = reverse('sapl.protocoloadm:tramitacaoadministrativo_delete',
kwargs={'pk': tramitacao_principal.pk})
response = admin_client.post(url, {'confirmar':'confirmar'} ,follow=True)
assert TramitacaoAdministrativo.objects.filter(id=tramitacao_principal.pk).count() == 0
assert TramitacaoAdministrativo.objects.filter(id=tramitacao_anexada.pk).count() == 1
assert TramitacaoAdministrativo.objects.filter(id=tramitacao_anexada_anexada.pk).count() == 1
# Removendo a tramitação anexada, a tramitação anexada à anexada deve ser removida
url = reverse('sapl.protocoloadm:tramitacaoadministrativo_delete',
kwargs={'pk': tramitacao_anexada.pk})
response = admin_client.post(url, {'confirmar':'confirmar'} ,follow=True)
assert TramitacaoAdministrativo.objects.filter(id=tramitacao_anexada.pk).count() == 0
assert TramitacaoAdministrativo.objects.filter(id=tramitacao_anexada_anexada.pk).count() == 0

7
sapl/protocoloadm/urls.py

@ -21,7 +21,8 @@ from sapl.protocoloadm.views import (AcompanhamentoDocumentoView,
atualizar_numero_documento,
doc_texto_integral,
DesvincularDocumentoView,
DesvincularMateriaView)
DesvincularMateriaView,
AnexadoCrud, DocumentoAnexadoEmLoteView)
from .apps import AppConfig
@ -30,6 +31,7 @@ app_name = AppConfig.name
urlpatterns_documento_administrativo = [
url(r'^docadm/',
include(DocumentoAdministrativoCrud.get_urls() +
AnexadoCrud.get_urls() +
TramitacaoAdmCrud.get_urls() +
DocumentoAcessorioAdministrativoCrud.get_urls())),
@ -38,6 +40,9 @@ urlpatterns_documento_administrativo = [
url(r'^docadm/texto_integral/(?P<pk>\d+)$', doc_texto_integral,
name='doc_texto_integral'),
url(r'^docadm/(?P<pk>\d+)/anexado_em_lote', DocumentoAnexadoEmLoteView.as_view(),
name='anexado_em_lote'),
]
urlpatterns_protocolo = [

286
sapl/protocoloadm/views.py

@ -17,7 +17,7 @@ from django.http.response import HttpResponseRedirect
from django.shortcuts import redirect
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from django.views.generic import ListView, CreateView
from django.views.generic import ListView, CreateView, UpdateView
from django.views.generic.base import RedirectView, TemplateView
from django.views.generic.edit import FormView
from django_filters.views import FilterView
@ -27,16 +27,17 @@ from sapl.base.email_utils import do_envia_email_confirmacao
from sapl.base.models import Autor, CasaLegislativa
from sapl.base.signals import tramitacao_signal
from sapl.comissoes.models import Comissao
from sapl.crud.base import Crud, CrudAux, MasterDetailCrud, make_pagination
from sapl.crud.base import (Crud, CrudAux, MasterDetailCrud, make_pagination,
RP_LIST, RP_DETAIL)
from sapl.materia.models import MateriaLegislativa, TipoMateriaLegislativa
from sapl.materia.views import gerar_pdf_impressos
from sapl.parlamentares.models import Legislatura, Parlamentar
from sapl.protocoloadm.models import Protocolo
from sapl.utils import (create_barcode, get_base_url, get_client_ip,
get_mime_type_from_file_extension,
get_mime_type_from_file_extension, lista_anexados,
show_results_filter_set, mail_service_configured)
from .forms import (AcompanhamentoDocumentoForm, AnularProcoloAdmForm,
from .forms import (AcompanhamentoDocumentoForm, AnularProtocoloAdmForm,
DocumentoAcessorioAdministrativoForm,
DocumentoAdministrativoFilterSet,
DocumentoAdministrativoForm, FichaPesquisaAdmForm, FichaSelecionaAdmForm, ProtocoloDocumentForm,
@ -44,10 +45,12 @@ from .forms import (AcompanhamentoDocumentoForm, AnularProcoloAdmForm,
TramitacaoAdmEditForm, TramitacaoAdmForm,
DesvincularDocumentoForm, DesvincularMateriaForm,
filtra_tramitacao_adm_destino_and_status,
filtra_tramitacao_adm_destino, filtra_tramitacao_adm_status)
filtra_tramitacao_adm_destino, filtra_tramitacao_adm_status,
AnexadoForm, AnexadoEmLoteFilterSet,
compara_tramitacoes_doc)
from .models import (AcompanhamentoDocumento, DocumentoAcessorioAdministrativo,
DocumentoAdministrativo, StatusTramitacaoAdministrativo,
TipoDocumentoAdministrativo, TramitacaoAdministrativo)
TipoDocumentoAdministrativo, TramitacaoAdministrativo, Anexado)
TipoDocumentoAdministrativoCrud = CrudAux.build(
@ -243,7 +246,7 @@ class AcompanhamentoDocumentoView(CreateView):
"documento",
documento,
destinatario)
self.logger.info('user={} .Foi enviado um e-mail de confirmação. Confira sua caixa '
self.logger.info('user={}. Foi enviado um e-mail de confirmação. Confira sua caixa '
'de mensagens e clique no link que nós enviamos para '
'confirmar o acompanhamento deste documento.'.format(usuario.username))
msg = _('Foi enviado um e-mail de confirmação. Confira sua caixa \
@ -251,19 +254,50 @@ class AcompanhamentoDocumentoView(CreateView):
confirmar o acompanhamento deste documento.')
messages.add_message(request, messages.SUCCESS, msg)
# Se o elemento existir e o email não foi confirmado:
# gerar novo hash e reenviar mensagem de email
elif not acompanhar[0].confirmado:
acompanhar = acompanhar[0]
acompanhar.hash = hash_txt
acompanhar.save()
base_url = get_base_url(request)
destinatario = AcompanhamentoDocumento.objects.get(
documento=documento,
email=email,
confirmado=False
)
casa = CasaLegislativa.objects.first()
do_envia_email_confirmacao(base_url,
casa,
"documento",
documento,
destinatario)
self.logger.info('user={}. Foi enviado um e-mail de confirmação. Confira sua caixa \
de mensagens e clique no link que nós enviamos para \
confirmar o acompanhamento deste documento.'.format(usuario.username))
msg = _('Foi enviado um e-mail de confirmação. Confira sua caixa \
de mensagens e clique no link que nós enviamos para \
confirmar o acompanhamento deste documento.')
messages.add_message(request, messages.SUCCESS, msg)
# Caso esse Acompanhamento já exista
# avisa ao usuário que esse documento já está sendo acompanhado
else:
self.logger.info('user=' + request.user.username +
'. Este e-mail já está acompanhando esse documento (pk={}).'.format(pk))
msg = _('Este e-mail já está acompanhando esse documento.')
messages.add_message(request, messages.INFO, msg)
messages.add_message(request, messages.ERROR, msg)
return self.render_to_response(
{'form': form,
'documento': documento,
'error': _('Esse documento já está\
sendo acompanhada por este e-mail.')})
})
return HttpResponseRedirect(self.get_success_url())
else:
return self.render_to_response(
@ -451,7 +485,7 @@ class ProtocoloListView(PermissionRequiredMixin, ListView):
class AnularProtocoloAdmView(PermissionRequiredMixin, CreateView):
template_name = 'protocoloadm/anular_protocoloadm.html'
form_class = AnularProcoloAdmForm
form_class = AnularProtocoloAdmForm
form_valid_message = _('Protocolo anulado com sucesso!')
permission_required = ('protocoloadm.action_anular_protocolo', )
@ -504,12 +538,12 @@ class ProtocoloDocumentoView(PermissionRequiredMixin,
def form_valid(self, form):
protocolo = form.save(commit=False)
username = self.request.user.username
try:
self.logger.debug("user=" + username +
". Tentando obter sequência de numeração.")
numeracao = sapl.base.models.AppConfig.objects.last(
).sequencia_numeracao
except AttributeError as e:
).sequencia_numeracao_protocolo
if not numeracao:
self.logger.error("user=" + username + ". É preciso definir a sequencia de "
"numeração na tabelas auxiliares! " + str(e))
msg = _('É preciso definir a sequencia de ' +
@ -691,12 +725,11 @@ class ProtocoloMateriaView(PermissionRequiredMixin, CreateView):
def form_valid(self, form):
protocolo = form.save(commit=False)
username = self.request.user.username
try:
self.logger.debug("user=" + username +
". Tentando obter sequência de numeração.")
numeracao = sapl.base.models.AppConfig.objects.last(
).sequencia_numeracao
except AttributeError:
).sequencia_numeracao_protocolo
if not numeracao:
self.logger.error("user=" + username + ". É preciso definir a sequencia de "
"numeração na tabelas auxiliares!")
msg = _('É preciso definir a sequencia de ' +
@ -910,6 +943,154 @@ class PesquisarDocumentoAdministrativoView(DocumentoAdministrativoMixin,
return self.render_to_response(context)
class AnexadoCrud(MasterDetailCrud):
model = Anexado
parent_field = 'documento_principal'
help_topic = 'documento_anexado'
public = [RP_LIST, RP_DETAIL]
class BaseMixin(MasterDetailCrud.BaseMixin):
list_field_names = ['documento_anexado', 'data_anexacao']
class CreateView(MasterDetailCrud.CreateView):
form_class = AnexadoForm
class UpdateView(MasterDetailCrud.UpdateView):
form_class = AnexadoForm
def get_initial(self):
initial = super(UpdateView, self).get_initial()
initial['tipo'] = self.object.documento_anexado.tipo.id
initial['numero'] = self.object.documento_anexado.numero
initial['ano'] = self.object.documento_anexado.ano
return initial
class DetailView(MasterDetailCrud.DetailView):
@property
def layout_key(self):
return 'AnexadoDetail'
class DocumentoAnexadoEmLoteView(PermissionRequiredMixin, FilterView):
filterset_class = AnexadoEmLoteFilterSet
template_name = 'protocoloadm/em_lote/anexado.html'
permission_required = ('protocoloadm.add_anexado', )
def get_context_data(self, **kwargs):
context = super(
DocumentoAnexadoEmLoteView,self
).get_context_data(**kwargs)
context['root_pk'] = self.kwargs['pk']
context['subnav_template_name'] = 'protocoloadm/subnav.yaml'
context['title'] = _('Documentos Anexados em Lote')
# Verifica se os campos foram preenchidos
if not self.request.GET.get('tipo', " "):
msg =_('Por favor, selecione um tipo de documento.')
messages.add_message(self.request, messages.ERROR, msg)
if not self.request.GET.get('data_0', " ") or not self.request.GET.get('data_1', " "):
msg =_('Por favor, preencha as datas.')
messages.add_message(self.request, messages.ERROR, msg)
return context
if not self.request.GET.get('data_0', " ") or not self.request.GET.get('data_1', " "):
msg =_('Por favor, preencha as datas.')
messages.add_message(self.request, messages.ERROR, msg)
return context
qr = self.request.GET.copy()
context['temp_object_list'] = context['object_list'].order_by(
'numero', '-ano'
)
context['object_list'] = []
for obj in context['temp_object_list']:
if not obj.pk == int(context['root_pk']):
documento_principal = DocumentoAdministrativo.objects.get(id=context['root_pk'])
documento_anexado = obj
is_anexado = Anexado.objects.filter(documento_principal=documento_principal,
documento_anexado=documento_anexado).exists()
if not is_anexado:
ciclico = False
anexados_anexado = Anexado.objects.filter(documento_principal=documento_anexado)
while anexados_anexado and not ciclico:
anexados = []
for anexo in anexados_anexado:
if documento_principal == anexo.documento_anexado:
ciclico = True
else:
for a in Anexado.objects.filter(documento_principal=anexo.documento_anexado):
anexados.append(a)
anexados_anexado = anexados
if not ciclico:
context['object_list'].append(obj)
context['numero_res'] = len(context['object_list'])
context['filter_url'] = ('&' + qr.urlencode()) if len(qr) > 0 else ''
context['show_results'] = show_results_filter_set(qr)
return context
def post(self, request, *args, **kwargs):
marcados = request.POST.getlist('documento_id')
data_anexacao = datetime.strptime(
request.POST['data_anexacao'], "%d/%m/%Y"
).date()
if request.POST['data_desanexacao'] == '':
data_desanexacao = None
v_data_desanexacao = data_anexacao
else:
data_desanexacao = datetime.strptime(
request.POST['data_desanexacao'], "%d/%m/%Y"
).date()
v_data_desanexacao = data_desanexacao
if len(marcados) == 0:
msg =_('Nenhum documento foi selecionado')
messages.add_message(request, messages.ERROR, msg)
if data_anexacao > v_data_desanexacao:
msg=_('Data de anexação posterior à data de desanexação.')
messages.add_message(request, messages.ERROR, msg)
return self.get(request, self.kwargs)
if data_anexacao > v_data_desanexacao:
msg =_('Data de anexação posterior à data de desanexação.')
messages.add_message(request, messages.ERROR, msg)
return self.get(request, messages.ERROR, msg)
principal = DocumentoAdministrativo.objects.get(pk = kwargs['pk'])
for documento in DocumentoAdministrativo.objects.filter(id__in = marcados):
anexado = Anexado()
anexado.documento_principal = principal
anexado.documento_anexado = documento
anexado.data_anexacao = data_anexacao
anexado.data_desanexacao = data_desanexacao
anexado.save()
msg = _('Documento(s) anexado(s).')
messages.add_message(request, messages.SUCCESS, msg)
success_url = reverse('sapl_index') + 'docadm/' + kwargs['pk'] + '/anexado'
return HttpResponseRedirect(success_url)
class TramitacaoAdmCrud(MasterDetailCrud):
model = TramitacaoAdministrativo
parent_field = 'documento'
@ -923,6 +1104,10 @@ class TramitacaoAdmCrud(MasterDetailCrud):
form_class = TramitacaoAdmForm
logger = logging.getLogger(__name__)
def get_success_url(self):
return reverse('sapl.protocoloadm:tramitacaoadministrativo_list', kwargs={
'pk': self.kwargs['pk']})
def get_initial(self):
initial = super(CreateView, self).get_initial()
local = DocumentoAdministrativo.objects.get(
@ -936,10 +1121,33 @@ class TramitacaoAdmCrud(MasterDetailCrud):
else:
initial['unidade_tramitacao_local'] = ''
initial['data_tramitacao'] = timezone.now().date()
initial['ip'] = get_client_ip(self.request)
initial['user'] = self.request.user
return initial
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
username = self.request.user.username
ultima_tramitacao = TramitacaoAdministrativo.objects.filter(
documento_id=self.kwargs['pk']).order_by(
'-data_tramitacao',
'-timestamp',
'-id').first()
#TODO: Esta checagem foi inserida na issue #2027, mas é mesmo necessária?
if ultima_tramitacao:
if ultima_tramitacao.unidade_tramitacao_destino:
context['form'].fields[
'unidade_tramitacao_local'].choices = [
(ultima_tramitacao.unidade_tramitacao_destino.pk,
ultima_tramitacao.unidade_tramitacao_destino)]
else:
self.logger.error('user=' + username + '. Unidade de tramitação destino '
'da última tramitação não pode ser vazia!')
msg = _('Unidade de tramitação destino '
' da última tramitação não pode ser vazia!')
messages.add_message(self.request, messages.ERROR, msg)
primeira_tramitacao = not(TramitacaoAdministrativo.objects.filter(
documento_id=int(kwargs['root_pk'])).exists())
@ -959,7 +1167,6 @@ class TramitacaoAdmCrud(MasterDetailCrud):
post=self.object,
request=self.request)
except Exception as e:
# TODO log error
self.logger.error('user=' + username + '. Tramitação criada, mas e-mail de acompanhamento de documento '
'não enviado. A não configuração do servidor de e-mail '
'impede o envio de aviso de tramitação. ' + str(e))
@ -974,6 +1181,12 @@ class TramitacaoAdmCrud(MasterDetailCrud):
form_class = TramitacaoAdmEditForm
logger = logging.getLogger(__name__)
def get_initial(self):
initial = super(UpdateView, self).get_initial()
initial['ip'] = get_client_ip(self.request)
initial['user'] = self.request.user
return initial
def form_valid(self, form):
self.object = form.save()
username = self.request.user.username
@ -982,7 +1195,6 @@ class TramitacaoAdmCrud(MasterDetailCrud):
post=self.object,
request=self.request)
except Exception as e:
# TODO log error
self.logger.error('user=' + username + '. Tramitação criada, mas e-mail de acompanhamento de documento '
'não enviado. A não configuração do servidor de e-mail '
'impede o envio de aviso de tramitação. ' + str(e))
@ -1003,18 +1215,26 @@ class TramitacaoAdmCrud(MasterDetailCrud):
class DetailView(DocumentoAdministrativoMixin,
MasterDetailCrud.DetailView):
pass
template_name = 'protocoloadm/tramitacaoadministrativo_detail.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['user'] = self.request.user
return context
class DeleteView(MasterDetailCrud.DeleteView):
logger = logging.getLogger(__name__)
def delete(self, request, *args, **kwargs):
tramitacao = TramitacaoAdministrativo.objects.get(
id=self.kwargs['pk'])
documento = DocumentoAdministrativo.objects.get(
id=tramitacao.documento.id)
documento = tramitacao.documento
url = reverse(
'sapl.protocoloadm:tramitacaoadministrativo_list',
kwargs={'pk': tramitacao.documento.id})
kwargs={'pk': documento.id})
ultima_tramitacao = \
documento.tramitacaoadministrativo_set.order_by(
@ -1022,11 +1242,21 @@ class TramitacaoAdmCrud(MasterDetailCrud):
'-id').first()
if tramitacao.pk != ultima_tramitacao.pk:
username = request.user.username
self.logger.error("user=" + username + ". Não é possível deletar a tramitação de pk={}. "
"Somente a última tramitação (pk={}) pode ser deletada!."
.format(tramitacao.pk, ultima_tramitacao.pk))
msg = _('Somente a última tramitação pode ser deletada!')
messages.add_message(request, messages.ERROR, msg)
return HttpResponseRedirect(url)
else:
tramitacao.delete()
tramitacoes_deletar = [tramitacao.id]
docs_anexados = lista_anexados(documento, False)
for da in docs_anexados:
tram_anexada = da.tramitacaoadministrativo_set.last()
if compara_tramitacoes_doc(tram_anexada, tramitacao):
tramitacoes_deletar.append(tram_anexada.id)
TramitacaoAdministrativo.objects.filter(id__in=tramitacoes_deletar).delete()
return HttpResponseRedirect(url)
@ -1121,7 +1351,7 @@ class ImpressosView(PermissionRequiredMixin, TemplateView):
class FichaPesquisaAdmView(PermissionRequiredMixin, FormView):
form_class = FichaPesquisaAdmForm
template_name = 'materia/impressos/ficha.html'
template_name = 'materia/impressos/impressos_form.html'
permission_required = ('materia.can_access_impressos', )
def form_valid(self, form):
@ -1139,7 +1369,7 @@ class FichaPesquisaAdmView(PermissionRequiredMixin, FormView):
class FichaSelecionaAdmView(PermissionRequiredMixin, FormView):
logger = logging.getLogger(__name__)
form_class = FichaSelecionaAdmForm
template_name = 'materia/impressos/ficha_seleciona.html'
template_name = 'materia/impressos/impressos_form.html'
permission_required = ('materia.can_access_impressos', )
def get_context_data(self, **kwargs):
@ -1201,8 +1431,8 @@ class FichaSelecionaAdmView(PermissionRequiredMixin, FormView):
self.messages.add_message(self.request, messages.INFO, mensagem)
return self.render_to_response(context)
if len(documento.assunto) > 301:
documento.assunto = documento.assunto[0:300] + '[...]'
if len(documento.assunto) > 201:
documento.assunto = documento.assunto[0:200] + '[...]'
context['documento'] = documento
return gerar_pdf_impressos(self.request, context,

8
sapl/relatorios/templates/pdf_etiqueta_protocolo_gerar.py

@ -23,7 +23,7 @@ def cabecalho(dic_cabecalho, imagem):
tmp_data += '\t\t\t\t<setFont name="Helvetica" size="12"/>\n'
tmp_data += '\t\t\t\t<drawString x="5cm" y="26.6cm">Sistema de Apoio ao Processo Legislativo</drawString>\n'
tmp_data += '\t\t\t\t<setFont name="Helvetica-Bold" size="13"/>\n'
tmp_data += '\t\t\t\t<drawString x="2.2cm" y="24.6cm">Relatório de Controle do Protocolo</drawString>\n'
tmp_data += '\t\t\t\t<drawString x="2.2cm" y="24.6cm">Relatório de Controle do Protocolo</drawString>\n'
return tmp_data
@ -36,7 +36,7 @@ def rodape(lst_rodape):
tmp_data += '\t\t\t\t<setFont name="Helvetica" size="8"/>\n'
tmp_data += '\t\t\t\t<drawString x="2cm" y="3.3cm">' + \
lst_rodape[2] + '</drawString>\n'
tmp_data += '\t\t\t\t<drawString x="17.9cm" y="3.3cm">Página <pageNumber/></drawString>\n'
tmp_data += '\t\t\t\t<drawString x="17.9cm" y="3.3cm">Página <pageNumber/></drawString>\n'
tmp_data += '\t\t\t\t<drawCentredString x="10.5cm" y="2.7cm">' + \
lst_rodape[0] + '</drawCentredString>\n'
tmp_data += '\t\t\t\t<drawCentredString x="10.5cm" y="2.3cm">' + \
@ -58,7 +58,7 @@ def paraStyle():
tmp_data += '\t\t\t<paraStyle name="all" alignment="justify"/>\n'
tmp_data += '\t\t</initialize>\n'
tmp_data += '\t\t<paraStyle name="P1" fontName="Helvetica-Bold" fontSize="5.0" leading="6" alignment="CENTER"/>\n'
tmp_data += '\t\t<paraStyle name="P2" fontName="Helvetica" fontSize="8.0" leading="9" alignment="CENTER"/>\n'
tmp_data += '\t\t<paraStyle name="P2" fontName="Helvetica" fontSize="8.0" leading="7.5" alignment="CENTER"/>\n'
tmp_data += '\t</stylesheet>\n'
return tmp_data
@ -122,7 +122,7 @@ def principal(imagem, lst_protocolos, dic_cabecalho, lst_rodape):
tmp_data += '\t<template pageSize="(62mm, 29mm)" title="Etiquetas de Protocolo" author="Luciano De Fazio" allowSplitting="20">\n'
tmp_data += '\t\t<pageTemplate id="first">\n'
tmp_data += '\t\t\t<pageGraphics>\n'
tmp_data += '\t\t\t<frame id="first" x1="0.03cm" y1="0.1cm" width="61mm" height="29mm"/>\n'
tmp_data += '\t\t\t<frame id="first" x1="0.03cm" y1="0.1cm" width="61mm" height="26mm"/>\n'
tmp_data += '\t\t\t</pageGraphics>\n'
tmp_data += '\t\t</pageTemplate>\n'
tmp_data += '\t</template>\n'

146
sapl/relatorios/templates/pdf_sessao_plenaria_gerar.py

@ -1,19 +1,18 @@
##parameters=rodape_dic, sessao='', imagem, inf_basicas_dic, lst_mesa, lst_presenca_sessao, lst_expedientes, lst_expediente_materia, lst_oradores_expediente, lst_presenca_ordem_dia, lst_votacao, lst_oradores
# #parameters=rodape_dic, sessao='', imagem, inf_basicas_dic, lst_mesa, lst_presenca_sessao, lst_expedientes, lst_expediente_materia, lst_oradores_expediente, lst_presenca_ordem_dia, lst_votacao, lst_oradores
"""Script para geração do PDF das sessoes plenarias
Autor: Gustavo Lepri
Atualizado por Luciano De Fázio - 22/03/2012
versão: 1.0
"""
import time
import os
import time
import logging
from django.template.defaultfilters import safe
from django.utils.html import strip_tags
from trml2pdf import parseString
from sapl.sessao.models import ResumoOrdenacao
from trml2pdf import parseString
def cabecalho(inf_basicas_dic, imagem):
"""
@ -129,6 +128,24 @@ def inf_basicas(inf_basicas_dic):
return tmp
def multimidia(cont_mult_dic):
"""
"""
tmp = ""
mul_audio = cont_mult_dic['multimidia_audio']
mul_video = cont_mult_dic['multimidia_video']
tmp += '\t\t<para style="P1">Conteúdo Multimídia</para>\n'
tmp += '\t\t<para style="P2">\n'
tmp += '\t\t\t<font color="white"> <br/></font>\n'
tmp += '\t\t</para>\n'
tmp += '\t\t<para style="P2" spaceAfter="5"><b>Audio: </b> ' + mul_audio + '</para>\n'
tmp += '\t\t<para style="P2" spaceAfter="5"><b>Video: </b> ' + mul_video + '</para>\n'
return tmp
def mesa(lst_mesa):
"""
@ -145,7 +162,7 @@ def mesa(lst_mesa):
return tmp
def presenca(lst_presenca_sessao,lst_ausencia_sessao):
def presenca(lst_presenca_sessao, lst_ausencia_sessao):
"""
"""
@ -202,14 +219,18 @@ def expediente_materia(lst_expediente_materia):
tmp += '\t\t<para style="P2">\n'
tmp += '\t\t\t<font color="white"> <br/></font>\n'
tmp += '\t\t</para>\n'
tmp += '<blockTable style="repeater" repeatRows="1">\n'
tmp += '<blockTable style="repeater" repeatRows="1" colWidths="3.5cm,11.5cm,3.5cm">>\n'
tmp += '<tr><td >Matéria</td><td>Ementa</td><td>Resultado da Votação</td></tr>\n'
for expediente_materia in lst_expediente_materia:
tmp += '<tr><td><para style="P3"><b>' + str(expediente_materia['num_ordem']) + '</b> - ' + expediente_materia['id_materia'] + '</para>\n' + '<para style="P3"><b>Turno: </b>' + expediente_materia[
'des_turno'] + '</para>\n' + '<para style="P3"><b>'+ expediente_materia['num_autores'] + ': </b>' + str(expediente_materia['nom_autor']) + '</para></td>\n'
'des_turno'] + '</para>\n' + '<para style="P3"><b>' + expediente_materia['num_autores'] + ': </b>' + str(expediente_materia['nom_autor']) + '</para></td>\n'
txt_ementa = expediente_materia['txt_ementa'].replace('&', '&amp;')
if len(txt_ementa) > 1000:
txt_ementa = txt_ementa[:1000] + "..."
# txt_ementa = dont_break_out(expediente_materia['txt_ementa'])
# if len(txt_ementa) > 800:
# txt_ementa = txt_ementa[:800] + "..."
tmp += '<td><para style="P4">' + txt_ementa + '</para>' + '<para style="P4">' + expediente_materia['ordem_observacao'] + '</para></td>\n'
tmp += '<td><para style="P3"><b>' + \
str(expediente_materia['nom_resultado']) + \
@ -224,6 +245,30 @@ def expediente_materia(lst_expediente_materia):
return tmp
def expediente_materia_vot_nom(lst_expediente_materia_vot_nom):
"""
"""
tmp = ''
tmp += '\t\t<para style="P1">Votações Nominais - Matérias do Expediente</para>\n\n'
tmp += '\t\t<para style="P2">\n'
tmp += '\t\t\t<font color="white"> <br/></font>\n'
tmp += '\t\t</para>\n'
tmp += '<blockTable style="repeater" repeatRows="1">\n'
tmp += '<tr><td >Matéria</td><td>Votos</td></tr>\n'
for expediente_materia_vot_nom in lst_expediente_materia_vot_nom:
tmp += '<tr><td><para style="P3">' + str(expediente_materia_vot_nom['titulo']) + '</para></td>'
if expediente_materia_vot_nom['votos']:
tmp += '<td>'
for v in expediente_materia_vot_nom['votos']:
tmp += '<para style="P3"><b>' + str(v.parlamentar) + '</b> - ' + v.voto + '</para>'
tmp += '</td>'
else:
tmp += '<td><para style="P3"><b>Matéria não votada</b></para></td>'
tmp += '</tr>\n'
tmp += '\t\t</blockTable>\n'
return tmp
def oradores_expediente(lst_oradores_expediente):
"""
@ -271,7 +316,7 @@ def votacao(lst_votacao):
tmp += '<tr><td >Matéria</td><td>Ementa</td><td>Resultado da Votação</td></tr>\n'
for votacao in lst_votacao:
tmp += '<tr><td><para style="P3"><b>' + str(votacao['num_ordem']) + '</b> - ' + votacao['id_materia'] + '</para>\n' + '<para style="P3"><b>Turno:</b> ' + votacao[
'des_turno'] + '</para>\n' + '<para style="P3"><b>'+ votacao['num_autores'] +': </b>' + str(votacao['nom_autor']) + '</para></td>\n'
'des_turno'] + '</para>\n' + '<para style="P3"><b>' + votacao['num_autores'] + ': </b>' + str(votacao['nom_autor']) + '</para></td>\n'
txt_ementa = votacao['txt_ementa'].replace('&', '&amp;')
if len(txt_ementa) > 1000:
txt_ementa = txt_ementa[:1000] + "..."
@ -289,6 +334,48 @@ def votacao(lst_votacao):
return tmp
def votacao_vot_nom(lst_votacao_vot_nom):
"""
"""
tmp = ''
tmp += '\t\t<para style="P1">Votações Nominais - Matérias da Ordem do Dia</para>\n\n'
tmp += '\t\t<para style="P2">\n'
tmp += '\t\t\t<font color="white"> <br/></font>\n'
tmp += '\t\t</para>\n'
tmp += '<blockTable style="repeater" repeatRows="1">\n'
tmp += '<tr><td >Matéria</td><td>Votos</td></tr>\n'
for votacao_vot_nom in lst_votacao_vot_nom:
tmp += '<tr><td><para style="P3">' + str(votacao_vot_nom['titulo']) + '</para></td>'
if votacao_vot_nom['votos']:
tmp += '<td>'
for v in votacao_vot_nom['votos']:
tmp += '<para style="P3"><b>' + str(v.parlamentar) + '</b> - ' + v.voto + '</para>'
tmp += '</td>'
else:
tmp += '<td><para style="P3"><b>Matéria não votada</b></para></td>'
tmp += '</tr>\n'
tmp += '\t\t</blockTable>\n'
return tmp
def oradores_ordemdia(lst_oradores_ordemdia):
"""
"""
tmp = ''
tmp += '\t\t<para style="P1">Oradores da Ordem do Dia</para>\n'
tmp += '\t\t<para style="P2">\n'
tmp += '\t\t\t<font color="white"> <br/></font>\n'
tmp += '\t\t</para>\n'
for orador_ordemdia in lst_oradores_ordemdia:
tmp += '\t\t<para style="P2" spaceAfter="5"><b>' + \
str(orador_ordemdia['num_ordem']) + '</b> - ' + \
orador_ordemdia['nome_parlamentar'] + '/' + \
str(orador_ordemdia['sigla']) + ' - ' + \
str(orador_ordemdia['observacao']) + '</para>\n'
return tmp
def oradores(lst_oradores):
"""
@ -323,10 +410,11 @@ def ocorrencias(lst_ocorrencias):
return tmp
def principal(rodape_dic, imagem, inf_basicas_dic, lst_mesa, lst_presenca_sessao, lst_ausencia_sessao, lst_expedientes, lst_expediente_materia, lst_oradores_expediente, lst_presenca_ordem_dia, lst_votacao, lst_oradores, lst_ocorrencias):
def principal(rodape_dic, imagem, inf_basicas_dic, cont_mult_dic, lst_mesa, lst_presenca_sessao, lst_ausencia_sessao, lst_expedientes, lst_expediente_materia, lst_expediente_materia_vot_nom, lst_oradores_expediente, lst_presenca_ordem_dia, lst_votacao, lst_votacao_vot_nom, lst_oradores_ordemdia, lst_oradores, lst_ocorrencias):
"""
"""
arquivoPdf = str(int(time.time() * 100)) + ".pdf"
logger = logging.getLogger(__name__)
tmp = ''
tmp += '<?xml version="1.0" encoding="utf-8" standalone="no" ?>\n'
@ -346,20 +434,24 @@ def principal(rodape_dic, imagem, inf_basicas_dic, lst_mesa, lst_presenca_sessao
ordenacao = ResumoOrdenacao.objects.first()
dict_ord_template = {
'cont_mult': '',
'cont_mult': multimidia(cont_mult_dic),
'exp': expedientes(lst_expedientes),
'id_basica': inf_basicas(inf_basicas_dic),
'lista_p': presenca(lst_presenca_sessao,lst_ausencia_sessao),
'lista_p': presenca(lst_presenca_sessao, lst_ausencia_sessao),
'lista_p_o_d': presenca_ordem_dia(lst_presenca_ordem_dia),
'mat_exp': expediente_materia(lst_expediente_materia),
'v_n_mat_exp': expediente_materia_vot_nom(lst_expediente_materia_vot_nom),
'mat_o_d': votacao(lst_votacao),
'v_n_mat_o_d': votacao_vot_nom(lst_votacao_vot_nom),
'mesa_d': mesa(lst_mesa),
'oradores_exped': oradores_expediente(lst_oradores_expediente),
'oradores_o_d': oradores_ordemdia(lst_oradores_ordemdia),
'oradores_expli': oradores(lst_oradores),
'ocorr_sessao': ocorrencias(lst_ocorrencias)
}
if ordenacao:
try:
tmp += dict_ord_template[ordenacao.primeiro]
tmp += dict_ord_template[ordenacao.segundo]
tmp += dict_ord_template[ordenacao.terceiro]
@ -371,16 +463,40 @@ def principal(rodape_dic, imagem, inf_basicas_dic, lst_mesa, lst_presenca_sessao
tmp += dict_ord_template[ordenacao.nono]
tmp += dict_ord_template[ordenacao.decimo]
tmp += dict_ord_template[ordenacao.decimo_primeiro]
tmp += dict_ord_template[ordenacao.decimo_segundo]
tmp += dict_ord_template[ordenacao.decimo_terceiro]
tmp += dict_ord_template[ordenacao.decimo_quarto]
except KeyError as e:
logger.error("KeyError: " + str(e) + ". Erro ao tentar utilizar "
"configuração de ordenação. Utilizando ordenação padrão.")
tmp += inf_basicas(inf_basicas_dic)
tmp += multimidia(cont_mult_dic)
tmp += mesa(lst_mesa)
tmp += presenca(lst_presenca_sessao, lst_ausencia_sessao)
tmp += expedientes(lst_expedientes)
tmp += expediente_materia(lst_expediente_materia)
tmp += expediente_materia_vot_nom(lst_expediente_materia_vot_nom)
tmp += oradores_expediente(lst_oradores_expediente)
tmp += presenca_ordem_dia(lst_presenca_ordem_dia)
tmp += votacao(lst_votacao)
tmp += votacao_vot_nom(lst_votacao_vot_nom)
tmp += oradores_ordemdia(lst_oradores_ordemdia)
tmp += oradores(lst_oradores)
tmp += ocorrencias(lst_ocorrencias)
else:
tmp += inf_basicas(inf_basicas_dic)
tmp += multimidia(cont_mult_dic)
tmp += mesa(lst_mesa)
tmp += presenca(lst_presenca_sessao,lst_ausencia_sessao)
tmp += presenca(lst_presenca_sessao, lst_ausencia_sessao)
tmp += expedientes(lst_expedientes)
tmp += expediente_materia(lst_expediente_materia)
tmp += expediente_materia_vot_nom(lst_expediente_materia_vot_nom)
tmp += oradores_expediente(lst_oradores_expediente)
tmp += presenca_ordem_dia(lst_presenca_ordem_dia)
tmp += votacao(lst_votacao)
tmp += votacao_vot_nom(lst_votacao_vot_nom)
tmp += oradores_ordemdia(lst_oradores_ordemdia)
tmp += oradores(lst_oradores)
tmp += ocorrencias(lst_ocorrencias)

5
sapl/relatorios/urls.py

@ -5,7 +5,8 @@ from .views import (relatorio_capa_processo,
relatorio_documento_administrativo, relatorio_espelho,
relatorio_etiqueta_protocolo, relatorio_materia,
relatorio_ordem_dia, relatorio_pauta_sessao,
relatorio_protocolo, relatorio_sessao_plenaria)
relatorio_protocolo, relatorio_sessao_plenaria,
resumo_ata_pdf)
app_name = AppConfig.name
@ -28,4 +29,6 @@ urlpatterns = [
relatorio_etiqueta_protocolo, name='relatorio_etiqueta_protocolo'),
url(r'^relatorios/pauta-sessao/(?P<pk>\d+)/$',
relatorio_pauta_sessao, name='relatorio_pauta_sessao'),
url(r'^relatorios/(?P<pk>\d+)/resumo_ata$',
resumo_ata_pdf, name='resumo_ata_pdf'),
]

180
sapl/relatorios/views.py

@ -2,12 +2,15 @@ from datetime import datetime as dt
import html
import logging
import re
import tempfile
from django.core.exceptions import ObjectDoesNotExist
from django.http import Http404, HttpResponse
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from django.template.loader import render_to_string
from sapl.settings import MEDIA_URL
from sapl.base.models import Autor, CasaLegislativa
from sapl.comissoes.models import Comissao
from sapl.materia.models import (Autoria, MateriaLegislativa, Numeracao,
@ -19,16 +22,26 @@ from sapl.sessao.models import (ExpedienteMateria, ExpedienteSessao,
IntegranteMesa, JustificativaAusencia,
Orador, OradorExpediente,
OrdemDia, PresencaOrdemDia, SessaoPlenaria,
SessaoPlenariaPresenca, OcorrenciaSessao)
SessaoPlenariaPresenca, OcorrenciaSessao,
RegistroVotacao, VotoParlamentar, OradorOrdemDia)
from sapl.settings import STATIC_ROOT
from sapl.utils import LISTA_DE_UFS, TrocaTag, filiacao_data
from sapl.sessao.views import (get_identificação_basica, get_mesa_diretora,
get_presenca_sessao, get_expedientes,
get_materias_expediente, get_oradores_expediente,
get_presenca_ordem_do_dia, get_materias_ordem_do_dia,
get_oradores_ordemdia,
get_oradores_explicações_pessoais, get_ocorrencias_da_sessão, get_assinaturas)
from .templates import (pdf_capa_processo_gerar,
pdf_documento_administrativo_gerar, pdf_espelho_gerar,
pdf_etiqueta_protocolo_gerar, pdf_materia_gerar,
pdf_ordem_dia_gerar, pdf_pauta_sessao_gerar,
pdf_protocolo_gerar, pdf_sessao_plenaria_gerar)
from weasyprint import HTML, CSS
def get_kwargs_params(request, fields):
kwargs = {}
@ -507,6 +520,18 @@ def get_sessao_plenaria(sessao, casa):
inf_basicas_dic["hr_fim_sessao"] = sessao.hora_fim
inf_basicas_dic["nom_camara"] = casa.nome
# Conteudo multimidia
cont_mult_dic = {}
if sessao.url_audio:
cont_mult_dic['multimidia_audio'] = str(sessao.url_audio)
else:
cont_mult_dic['multimidia_audio'] = 'Indisponível'
if sessao.url_video:
cont_mult_dic['multimidia_video'] = str(sessao.url_video)
else:
cont_mult_dic['multimidia_video'] = 'Indisponível'
# Lista da composicao da mesa diretora
lst_mesa = []
for composicao in IntegranteMesa.objects.filter(sessao_plenaria=sessao):
@ -566,6 +591,10 @@ def get_sessao_plenaria(sessao, casa):
# unescape HTML codes
# https://github.com/interlegis/sapl/issues/1046
conteudo = re.sub('style=".*?"', '', conteudo)
conteudo = re.sub('class=".*?"', '', conteudo)
conteudo = re.sub('align=".*?"', '', conteudo) # OSTicket Ticket #796450
conteudo = re.sub('<p\s+>', '<p>', conteudo)
conteudo = re.sub('<br\s+/>', '<br/>', conteudo) # OSTicket Ticket #796450
conteudo = html.unescape(conteudo)
# escape special character '&'
@ -633,6 +662,28 @@ def get_sessao_plenaria(sessao, casa):
dic_expediente_materia["votacao_observacao"] = ' '
lst_expediente_materia.append(dic_expediente_materia)
# Lista dos votos nominais das matérias do Expediente
lst_expediente_materia_vot_nom = []
materias_expediente_votacao_nominal = ExpedienteMateria.objects.filter(
sessao_plenaria=sessao,
tipo_votacao=2).order_by('-materia')
for mevn in materias_expediente_votacao_nominal:
votos_materia = []
titulo_materia = mevn.materia
registro = RegistroVotacao.objects.filter(expediente=mevn)
if registro:
for vp in VotoParlamentar.objects.filter(votacao=registro).order_by('parlamentar'):
votos_materia.append(vp)
dic_expediente_materia_vot_nom = {
'titulo': titulo_materia,
'votos': votos_materia
}
lst_expediente_materia_vot_nom.append(dic_expediente_materia_vot_nom)
# Lista dos oradores do Expediente
lst_oradores_expediente = []
for orador_expediente in OradorExpediente.objects.filter(
@ -722,6 +773,57 @@ def get_sessao_plenaria(sessao, casa):
dic_votacao["nom_resultado"] = "Matéria não votada"
lst_votacao.append(dic_votacao)
# Lista dos votos nominais das matérias da Ordem do Dia
lst_votacao_vot_nom = []
materias_ordem_dia_votacao_nominal = OrdemDia.objects.filter(
sessao_plenaria=sessao,
tipo_votacao=2).order_by('-materia')
for modvn in materias_ordem_dia_votacao_nominal:
votos_materia_od = []
t_materia = modvn.materia
registro_od = RegistroVotacao.objects.filter(ordem=modvn)
if registro_od:
for vp_od in VotoParlamentar.objects.filter(votacao=registro_od).order_by('parlamentar'):
votos_materia_od.append(vp_od)
dic_votacao_vot_nom = {
'titulo': t_materia,
'votos': votos_materia_od
}
lst_votacao_vot_nom.append(dic_votacao_vot_nom)
# Lista dos oradores da Ordem do Dia
lst_oradores_ordemdia = []
oradores_ordem_dia = OradorOrdemDia.objects.filter(
sessao_plenaria=sessao
).order_by('numero_ordem')
for orador_ordemdia in oradores_ordem_dia:
parlamentar_orador = Parlamentar.objects.get(
id=orador_ordemdia.parlamentar.id
)
sigla_partido = Filiacao.objects.filter(
parlamentar=parlamentar_orador
).first()
if not sigla_partido:
sigla_p = ""
else:
sigla_p = sigla_partido.partido.sigla
dic_oradores_ordemdia = {
'num_ordem': orador_ordemdia.numero_ordem,
'nome_parlamentar': parlamentar_orador.nome_parlamentar,
'observacao': orador_ordemdia.observacao,
'sigla': sigla_p
}
lst_oradores_ordemdia.append(dic_oradores_ordemdia)
# Lista dos oradores nas Explicações Pessoais
lst_oradores = []
for orador in Orador.objects.filter(
@ -762,14 +864,18 @@ def get_sessao_plenaria(sessao, casa):
lst_ocorrencias.append(o)
return (inf_basicas_dic,
cont_mult_dic,
lst_mesa,
lst_presenca_sessao,
lst_ausencia_sessao,
lst_expedientes,
lst_expediente_materia,
lst_expediente_materia_vot_nom,
lst_oradores_expediente,
lst_presenca_ordem_dia,
lst_votacao,
lst_votacao_vot_nom,
lst_oradores_ordemdia,
lst_oradores,
lst_ocorrencias)
@ -817,14 +923,18 @@ def relatorio_sessao_plenaria(request, pk):
raise Http404('Essa página não existe')
(inf_basicas_dic,
cont_mult_dic,
lst_mesa,
lst_presenca_sessao,
lst_ausencia_sessao,
lst_expedientes,
lst_expediente_materia,
lst_expediente_materia_vot_nom,
lst_oradores_expediente,
lst_presenca_ordem_dia,
lst_votacao,
lst_votacao_vot_nom,
lst_oradores_ordemdia,
lst_oradores,
lst_ocorrencias) = get_sessao_plenaria(sessao, casa)
@ -838,14 +948,18 @@ def relatorio_sessao_plenaria(request, pk):
rodape,
imagem,
inf_basicas_dic,
cont_mult_dic,
lst_mesa,
lst_presenca_sessao,
lst_ausencia_sessao,
lst_expedientes,
lst_expediente_materia,
lst_expediente_materia_vot_nom,
lst_oradores_expediente,
lst_presenca_ordem_dia,
lst_votacao,
lst_votacao_vot_nom,
lst_oradores_ordemdia,
lst_oradores,
lst_ocorrencias)
@ -1148,3 +1262,67 @@ def get_pauta_sessao(sessao, casa):
return (lst_expediente_materia,
lst_votacao,
inf_basicas_dic)
def make_pdf(base_url,main_template,header_template,main_css='',header_css=''):
html = HTML(base_url=base_url, string=main_template)
main_doc = html.render(stylesheets=[])
def get_page_body(boxes):
for box in boxes:
if box.element_tag == 'body':
return box
return get_page_body(box.all_children())
# Template of header
html = HTML(base_url=base_url,string=header_template)
header = html.render(stylesheets=[CSS(string='@page {size:A4; margin:1cm;}')])
header_page = header.pages[0]
header_body = get_page_body(header_page._page_box.all_children())
header_body = header_body.copy_with_children(header_body.all_children())
for page in main_doc.pages:
page_body = get_page_body(page._page_box.all_children())
page_body.children += header_body.all_children()
pdf_file = main_doc.write_pdf()
return pdf_file
def resumo_ata_pdf(request,pk):
base_url = request.build_absolute_uri()
casa = CasaLegislativa.objects.first()
rodape = ' '.join(get_rodape(casa))
sessao_plenaria = SessaoPlenaria.objects.get(pk=pk)
context = {}
context.update(get_identificação_basica(sessao_plenaria))
context.update(get_mesa_diretora(sessao_plenaria))
context.update(get_presenca_sessao(sessao_plenaria))
context.update(get_expedientes(sessao_plenaria))
context.update(get_materias_expediente(sessao_plenaria))
context.update(get_oradores_expediente(sessao_plenaria))
context.update(get_presenca_ordem_do_dia(sessao_plenaria))
context.update(get_materias_ordem_do_dia(sessao_plenaria))
context.update(get_oradores_ordemdia(sessao_plenaria))
context.update(get_oradores_explicações_pessoais(sessao_plenaria))
context.update(get_ocorrencias_da_sessão(sessao_plenaria))
context.update(get_assinaturas(sessao_plenaria))
context.update({'object': sessao_plenaria})
context.update({'data': dt.today().strftime('%d/%m/%Y')})
context.update({'rodape': rodape})
header_context = {"casa": casa, 'logotipo':casa.logotipo, 'MEDIA_URL': MEDIA_URL}
html_template = render_to_string('relatorios/relatorio_ata.html', context)
html_header = render_to_string('relatorios/header_ata.html', header_context)
pdf_file = make_pdf(base_url=base_url,main_template=html_template,header_template=html_header)
response = HttpResponse(content_type='application/pdf;')
response['Content-Disposition'] = 'inline; filename=relatorio.pdf'
response['Content-Transfer-Encoding'] = 'binary'
response.write(pdf_file)
return response

7
sapl/rules/map_rules.py

@ -60,6 +60,7 @@ rules_group_administrativo = {
'can_access_impressos'], __perms_publicas__),
# TODO: tratar em sapl.api a questão de ostencivo e restritivo
(protocoloadm.DocumentoAdministrativo, __base__, set()),
(protocoloadm.Anexado, __base__, set()),
(protocoloadm.DocumentoAcessorioAdministrativo, __base__, set()),
(protocoloadm.TramitacaoAdministrativo, __base__, set()),
]
@ -118,6 +119,8 @@ rules_group_materia = {
(materia.Autoria, __base__, __perms_publicas__),
(materia.DespachoInicial, __base__, __perms_publicas__),
(materia.DocumentoAcessorio, __base__, __perms_publicas__),
(materia.MateriaAssunto, __base__, __perms_publicas__),
(materia.AssuntoMateria, __base__, __perms_publicas__),
(materia.MateriaLegislativa, __base__ +
['can_access_impressos'], __perms_publicas__),
@ -175,6 +178,7 @@ rules_group_sessao = {
(sessao.ExpedienteSessao, __base__, __perms_publicas__),
(sessao.Orador, __base__, __perms_publicas__),
(sessao.OradorExpediente, __base__, __perms_publicas__),
(sessao.OradorOrdemDia, __base__, __perms_publicas__),
(sessao.OrdemDia, __base__, __perms_publicas__),
(sessao.PresencaOrdemDia, __base__, __perms_publicas__),
(sessao.RegistroVotacao, __base__, __perms_publicas__),
@ -274,6 +278,8 @@ rules_group_geral = {
(parlamentares.ComposicaoMesa, __base__, __perms_publicas__),
(parlamentares.Frente, __base__, __perms_publicas__),
(parlamentares.Votante, __base__, __perms_publicas__),
(parlamentares.Bloco, __base__, __perms_publicas__),
(sessao.CargoBancada, __base__, __perms_publicas__),
(sessao.Bancada, __base__, __perms_publicas__),
@ -282,7 +288,6 @@ rules_group_geral = {
(sessao.TipoExpediente, __base__, __perms_publicas__),
(sessao.TipoJustificativa, __base__, __perms_publicas__),
(sessao.JustificativaAusencia, __base__, __perms_publicas__),
(sessao.Bloco, __base__, __perms_publicas__),
(sessao.ResumoOrdenacao, __base__, __perms_publicas__),
(sessao.TipoRetiradaPauta, __base__, __perms_publicas__),

324
sapl/sessao/forms.py

@ -1,6 +1,6 @@
from datetime import datetime
from crispy_forms.helper import FormHelper
from sapl.crispy_layout_mixin import SaplFormHelper
from crispy_forms.layout import HTML, Button, Fieldset, Layout
from django import forms
from django.contrib.contenttypes.models import ContentType
@ -20,32 +20,21 @@ from sapl.materia.models import (MateriaLegislativa, StatusTramitacao,
from sapl.parlamentares.models import Parlamentar, Mandato
from sapl.utils import (RANGE_DIAS_MES, RANGE_MESES,
MateriaPesquisaOrderingFilter, autor_label,
autor_modal, timezone, choice_anos_com_sessaoplenaria)
autor_modal, timezone, choice_anos_com_sessaoplenaria,
FileFieldCheckMixin)
from .models import (Bancada, Bloco, ExpedienteMateria, JustificativaAusencia,
from .models import (Bancada, ExpedienteMateria, JustificativaAusencia,
Orador, OradorExpediente, OrdemDia, PresencaOrdemDia, SessaoPlenaria,
SessaoPlenariaPresenca, TipoResultadoVotacao,
OcorrenciaSessao, RetiradaPauta, TipoRetiradaPauta)
OcorrenciaSessao, RetiradaPauta, TipoRetiradaPauta, OradorOrdemDia, ORDENACAO_RESUMO,
ResumoOrdenacao)
MES_CHOICES = RANGE_MESES
DIA_CHOICES = RANGE_DIAS_MES
ORDENACAO_RESUMO = [('cont_mult', 'Conteúdo Multimídia'),
('exp', 'Expedientes'),
('id_basica', 'Identificação Básica'),
('lista_p', 'Lista de Presença'),
('lista_p_o_d', 'Lista de Presença Ordem do Dia'),
('mat_exp', 'Matérias do Expediente'),
('mat_o_d', 'Matérias da Ordem do Dia'),
('mesa_d', 'Mesa Diretora'),
('oradores_exped', 'Oradores do Expediente'),
('oradores_expli', 'Oradores das Explicações Pessoais'),
('ocorr_sessao', 'Ocorrências da Sessão')]
class SessaoPlenariaForm(ModelForm):
class SessaoPlenariaForm(FileFieldCheckMixin, ModelForm):
class Meta:
model = SessaoPlenaria
@ -207,7 +196,7 @@ class RetiradaPautaForm(ModelForm):
('expediente', 6)])
row3 = to_row([('observacao', 12)])
self.helper = FormHelper()
self.helper = SaplFormHelper()
self.helper.layout = SaplFormLayout(
Fieldset(_('Retirada de Pauta'),
row1, row2, row3))
@ -327,41 +316,6 @@ class BancadaForm(ModelForm):
return bancada
class BlocoForm(ModelForm):
class Meta:
model = Bloco
fields = ['nome', 'partidos', 'data_criacao',
'data_extincao', 'descricao']
def clean(self):
super(BlocoForm, self).clean()
if not self.is_valid():
return self.cleaned_data
if self.cleaned_data['data_extincao']:
if (self.cleaned_data['data_extincao'] <
self.cleaned_data['data_criacao']):
msg = _('Data de extinção não pode ser menor que a de criação')
raise ValidationError(msg)
return self.cleaned_data
@transaction.atomic
def save(self, commit=True):
bloco = super(BlocoForm, self).save(commit)
content_type = ContentType.objects.get_for_model(Bloco)
object_id = bloco.pk
tipo = TipoAutor.objects.get(content_type=content_type)
Autor.objects.create(
content_type=content_type,
object_id=object_id,
tipo=tipo,
nome=bloco.nome
)
return bloco
class ExpedienteMateriaForm(ModelForm):
_model = ExpedienteMateria
@ -591,7 +545,7 @@ class SessaoPlenariaFilterSet(django_filters.FilterSet):
('data_inicio__day', 3),
('tipo', 3)])
self.form.helper = FormHelper()
self.form.helper = SaplFormHelper()
self.form.helper.form_method = 'GET'
self.form.helper.layout = Layout(
Fieldset(self.titulo,
@ -664,7 +618,7 @@ class AdicionarVariasMateriasFilterSet(MateriaLegislativaFilterSet):
row9 = to_row(
[('ementa', 12)])
self.form.helper = FormHelper()
self.form.helper = SaplFormHelper()
self.form.helper.form_method = 'GET'
self.form.helper.layout = Layout(
Fieldset(_('Pesquisa de Matéria'),
@ -690,6 +644,28 @@ class OradorForm(ModelForm):
self.fields['parlamentar'].queryset = Parlamentar.objects.filter(
id__in=ids).order_by('nome_parlamentar')
def clean(self):
super(OradorForm, self).clean()
cleaned_data = self.cleaned_data
if not self.is_valid():
return self.cleaned_data
sessao_id = self.initial['id_sessao']
numero = self.initial.get('numero')
numero_ordem = cleaned_data['numero_ordem']
ordem = Orador.objects.filter(
sessao_plenaria_id=sessao_id,
numero_ordem=numero_ordem
).exists()
if ordem and numero_ordem != numero:
raise ValidationError(_(
"Já existe orador nesta posição de ordem de pronunciamento"
))
return self.cleaned_data
class Meta:
model = Orador
exclude = ['sessao_plenaria']
@ -733,37 +709,112 @@ class OradorExpedienteForm(ModelForm):
exclude = ['sessao_plenaria']
class OradorOrdemDiaForm(ModelForm):
def __init__(self, *args, **kwargs):
super(OradorOrdemDiaForm, self).__init__(*args, **kwargs)
id_sessao = int(self.initial['id_sessao'])
ids = [p.parlamentar.id for p in PresencaOrdemDia.objects.filter(
sessao_plenaria_id=id_sessao
)]
self.fields['parlamentar'].queryset = Parlamentar.objects.filter(
id__in=ids
).order_by('nome_parlamentar')
def clean(self):
super(OradorOrdemDiaForm, self).clean()
cleaned_data = self.cleaned_data
if not self.is_valid():
return self.cleaned_data
sessao_id = self.initial['id_sessao']
numero = self.initial.get('numero')
numero_ordem = cleaned_data['numero_ordem']
ordem = OradorOrdemDia.objects.filter(
sessao_plenaria_id=sessao_id,
numero_ordem=numero_ordem
).exists()
if ordem and numero_ordem != numero:
raise ValidationError(_(
"Já existe orador nesta posição de ordem de pronunciamento"
))
return self.cleaned_data
class Meta:
model = OradorOrdemDia
exclude = ['sessao_plenaria']
class PautaSessaoFilterSet(SessaoPlenariaFilterSet):
titulo = _('Pesquisa de Pauta de Sessão')
class ResumoOrdenacaoForm(forms.Form):
primeiro = forms.ChoiceField(label=_(''),
choices=ORDENACAO_RESUMO)
segundo = forms.ChoiceField(label=_(''),
choices=ORDENACAO_RESUMO)
terceiro = forms.ChoiceField(label='',
choices=ORDENACAO_RESUMO)
quarto = forms.ChoiceField(label=_(''),
choices=ORDENACAO_RESUMO)
quinto = forms.ChoiceField(label=_(''),
choices=ORDENACAO_RESUMO)
sexto = forms.ChoiceField(label=_(''),
choices=ORDENACAO_RESUMO)
setimo = forms.ChoiceField(label=_(''),
choices=ORDENACAO_RESUMO)
oitavo = forms.ChoiceField(label=_(''),
choices=ORDENACAO_RESUMO)
nono = forms.ChoiceField(label=_(''),
choices=ORDENACAO_RESUMO)
decimo = forms.ChoiceField(label='10°',
choices=ORDENACAO_RESUMO)
decimo_primeiro = forms.ChoiceField(label='11°',
choices=ORDENACAO_RESUMO)
primeiro = forms.ChoiceField(
label='',
choices=ORDENACAO_RESUMO
)
segundo = forms.ChoiceField(
label='',
choices=ORDENACAO_RESUMO
)
terceiro = forms.ChoiceField(
label='',
choices=ORDENACAO_RESUMO
)
quarto = forms.ChoiceField(
label='',
choices=ORDENACAO_RESUMO
)
quinto = forms.ChoiceField(
label='',
choices=ORDENACAO_RESUMO
)
sexto = forms.ChoiceField(
label='',
choices=ORDENACAO_RESUMO
)
setimo = forms.ChoiceField(
label='',
choices=ORDENACAO_RESUMO
)
oitavo = forms.ChoiceField(
label='',
choices=ORDENACAO_RESUMO
)
nono = forms.ChoiceField(
label='',
choices=ORDENACAO_RESUMO
)
decimo = forms.ChoiceField(
label='10°',
choices=ORDENACAO_RESUMO
)
decimo_primeiro = forms.ChoiceField(
label='11°',
choices=ORDENACAO_RESUMO
)
decimo_segundo = forms.ChoiceField(
label='12°',
choices=ORDENACAO_RESUMO
)
decimo_terceiro = forms.ChoiceField(
label='13°',
choices=ORDENACAO_RESUMO
)
decimo_quarto = forms.ChoiceField(
label='14°',
choices=ORDENACAO_RESUMO
)
def __init__(self, *args, **kwargs):
super(ResumoOrdenacaoForm, self).__init__(*args, **kwargs)
row1 = to_row(
[('primeiro', 12)])
row2 = to_row(
@ -786,15 +837,25 @@ class ResumoOrdenacaoForm(forms.Form):
[('decimo', 12)])
row11 = to_row(
[('decimo_primeiro', 12)])
row12 = to_row(
[('decimo_segundo', 12)])
row13 = to_row(
[('decimo_terceiro', 12)])
row14 = to_row(
[('decimo_quarto', 12)]
)
self.helper = FormHelper()
self.helper = SaplFormHelper()
self.helper.layout = Layout(
Fieldset(_(''),
row1, row2, row3, row4, row5,
row6, row7, row8, row9, row10, row11,
row6, row7, row8, row9, row10,
row11, row12, row13, row14,
form_actions(label='Atualizar'))
)
super().__init__(*args, **kwargs)
def clean(self):
super(ResumoOrdenacaoForm, self).clean()
@ -813,6 +874,27 @@ class ResumoOrdenacaoForm(forms.Form):
'Não é possível ter campos repetidos'))
return self.cleaned_data
def save(self):
ordenacao = ResumoOrdenacao.objects.get()
cleaned_data = self.cleaned_data
ordenacao.primeiro = cleaned_data['primeiro']
ordenacao.segundo = cleaned_data['segundo']
ordenacao.terceiro = cleaned_data['terceiro']
ordenacao.quarto = cleaned_data['quarto']
ordenacao.quinto = cleaned_data['quinto']
ordenacao.sexto = cleaned_data['sexto']
ordenacao.setimo = cleaned_data['setimo']
ordenacao.oitavo = cleaned_data['oitavo']
ordenacao.nono = cleaned_data['nono']
ordenacao.decimo = cleaned_data['decimo']
ordenacao.decimo_primeiro = cleaned_data['decimo_primeiro']
ordenacao.decimo_segundo = cleaned_data['decimo_segundo']
ordenacao.decimo_terceiro = cleaned_data['decimo_terceiro']
ordenacao.decimo_quarto = cleaned_data['decimo_quarto']
ordenacao.save()
class JustificativaAusenciaForm(ModelForm):
@ -853,7 +935,7 @@ class JustificativaAusenciaForm(ModelForm):
row8 = to_row(
[('observacao', 12)])
self.helper = FormHelper()
self.helper = SaplFormHelper()
self.helper.layout = SaplFormLayout(
Fieldset(_('Justificativa de Ausência'),
row1, row2, row3,
@ -915,79 +997,3 @@ class JustificativaAusenciaForm(ModelForm):
justificativa.materias_do_expediente.clear()
justificativa.materias_da_ordem_do_dia.clear()
return justificativa
class VotacaoEmBlocoFilterSet(MateriaLegislativaFilterSet):
o = MateriaPesquisaOrderingFilter()
tramitacao__status = django_filters.ModelChoiceFilter(
required=True,
queryset=StatusTramitacao.objects.all(),
label=_('Status da Matéria'))
class Meta:
model = MateriaLegislativa
fields = ['tramitacao__status',
'numero',
'numero_protocolo',
'ano',
'tipo',
'data_apresentacao',
'data_publicacao',
'autoria__autor__tipo',
# FIXME 'autoria__autor__partido',
'relatoria__parlamentar_id',
'local_origem_externa',
'em_tramitacao',
]
def __init__(self, *args, **kwargs):
super(MateriaLegislativaFilterSet, self).__init__(*args, **kwargs)
self.filters['tipo'].label = 'Tipo de Matéria'
self.filters['autoria__autor__tipo'].label = 'Tipo de Autor'
# self.filters['autoria__autor__partido'].label = 'Partido do Autor'
self.filters['relatoria__parlamentar_id'].label = 'Relatoria'
row1 = to_row(
[('tramitacao__status', 12)])
row2 = to_row(
[('tipo', 12)])
row3 = to_row(
[('numero', 4),
('ano', 4),
('numero_protocolo', 4)])
row4 = to_row(
[('data_apresentacao', 6),
('data_publicacao', 6)])
row5 = to_row(
[('autoria__autor', 0),
(Button('pesquisar',
'Pesquisar Autor',
css_class='btn btn-primary btn-sm'), 2),
(Button('limpar',
'limpar Autor',
css_class='btn btn-primary btn-sm'), 10)])
row6 = to_row(
[('autoria__autor__tipo', 6),
# ('autoria__autor__partido', 6)
])
row7 = to_row(
[('relatoria__parlamentar_id', 6),
('local_origem_externa', 6)])
row8 = to_row(
[('em_tramitacao', 6),
('o', 6)])
row9 = to_row(
[('ementa', 12)])
self.form.helper = FormHelper()
self.form.helper.form_method = 'GET'
self.form.helper.layout = Layout(
Fieldset(_('Pesquisa de Matéria'),
row1, row2, row3,
HTML(autor_label),
HTML(autor_modal),
row4, row5, row6, row7, row8, row9,
form_actions(label='Pesquisar'))
)

2
sapl/sessao/migrations/0028_auto_20181031_0902.py

@ -38,7 +38,7 @@ class Migration(migrations.Migration):
],
options={
'verbose_name_plural': 'Tipos de Retirada de Pauta',
'verbose_name': 'Tipo de Retidara de Pauta',
'verbose_name': 'Tipo de Retirada de Pauta',
'ordering': ['descricao'],
},
),

25
sapl/sessao/migrations/0033_auto_20190228_1803.py

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-02-28 21:03
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('sessao', '0032_merge_20181122_1527'),
]
operations = [
migrations.AddField(
model_name='resumoordenacao',
name='decimo_segundo',
field=models.CharField(default='Votos Nominais Mat Expediente', max_length=30),
),
migrations.AddField(
model_name='resumoordenacao',
name='decimo_terceiro',
field=models.CharField(default='Votos Nominais Mat Ordem Dia', max_length=30),
),
]

34
sapl/sessao/migrations/0034_oradorordemdia.py

@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-03-26 16:14
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import sapl.sessao.models
class Migration(migrations.Migration):
dependencies = [
('parlamentares', '0025_auto_20180924_1724'),
('sessao', '0033_auto_20190228_1803'),
]
operations = [
migrations.CreateModel(
name='OradorOrdemDia',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('numero_ordem', models.PositiveIntegerField(verbose_name='Ordem de pronunciamento')),
('url_discurso', models.URLField(blank=True, max_length=150, verbose_name='URL Vídeo')),
('observacao', models.CharField(blank=True, max_length=150, verbose_name='Observação')),
('upload_anexo', models.FileField(blank=True, null=True, upload_to=sapl.sessao.models.anexo_upload_path, verbose_name='Anexo do Orador')),
('parlamentar', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='parlamentares.Parlamentar', verbose_name='Parlamentar')),
('sessao_plenaria', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sessao.SessaoPlenaria')),
],
options={
'verbose_name_plural': 'Oradores da Ordem do Dia',
'verbose_name': 'Orador da Ordem do Dia',
},
),
]

20
sapl/sessao/migrations/0035_resumoordenacao_decimo_quarto.py

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-03-26 18:14
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('sessao', '0034_oradorordemdia'),
]
operations = [
migrations.AddField(
model_name='resumoordenacao',
name='decimo_quarto',
field=models.CharField(default='Oradores da Ordem do Dia', max_length=30),
),
]

21
sapl/sessao/migrations/0036_auto_20190412_1106.py

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-04-12 14:06
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('sessao', '0035_resumoordenacao_decimo_quarto'),
]
operations = [
migrations.AlterField(
model_name='expedientesessao',
name='sessao_plenaria',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='expedientesessao_set', to='sessao.SessaoPlenaria'),
),
]

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

Loading…
Cancel
Save