Browse Source

Merge pull request #10 from interlegis/master

update 12/12/2017
pull/1635/head
Rogério Frá 8 years ago
committed by GitHub
parent
commit
3515ce540f
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .coveragerc
  2. 3
      .gitignore
  3. 2
      README.rst
  4. 1
      check_migrations.sh
  5. 22
      check_qa.sh
  6. 15
      docker-compose.yml
  7. 34
      docs/credits.txt
  8. 2
      docs/deploy.rst
  9. 111
      docs/importacao_25_31.rst
  10. 5
      docs/instalacao31.rst
  11. 2
      gunicorn_start.sh
  12. 4
      requirements/dev-requirements.txt
  13. 2
      requirements/migration-requirements.txt
  14. 4
      requirements/requirements.txt
  15. 2
      requirements/test-requirements.txt
  16. 16
      sapl/api/forms.py
  17. 65
      sapl/base/forms.py
  18. 20
      sapl/base/migrations/0009_appconfig_mostrar_brasao_painel.py
  19. 19
      sapl/base/migrations/0010_remove_appconfig_painel_aberto.py
  20. 19
      sapl/base/migrations/0011_auto_20171121_0958.py
  21. 20
      sapl/base/migrations/0012_auto_20171205_0917.py
  22. 69
      sapl/base/models.py
  23. 158
      sapl/base/search_indexes.py
  24. 9
      sapl/base/templatetags/base_tags.py
  25. 21
      sapl/base/tests/test_form.py
  26. 35
      sapl/base/tests/test_view_base.py
  27. 8
      sapl/base/tests/teststub_urls.py
  28. 10
      sapl/base/urls.py
  29. 97
      sapl/base/views.py
  30. 19
      sapl/comissoes/migrations/0003_auto_20171204_1658.py
  31. 1
      sapl/comissoes/models.py
  32. 7
      sapl/comissoes/views.py
  33. 25
      sapl/compilacao/migrations/0004_auto_20171031_1327.py
  34. 21
      sapl/compilacao/models.py
  35. 3
      sapl/compilacao/tests/test_tipo_texto_articulado_form.py
  36. 32
      sapl/compilacao/views.py
  37. 75
      sapl/crispy_layout_mixin.py
  38. 28
      sapl/crud/base.py
  39. 4
      sapl/crud/tests/stub_app/views.py
  40. 23
      sapl/decorators.py
  41. 54
      sapl/hashers.py
  42. 2
      sapl/legacy/management/commands/migracao_25_31.py
  43. 4
      sapl/legacy/management/commands/recria_constraints.py
  44. 42
      sapl/legacy/migracao_documentos.py
  45. 79
      sapl/legacy/migracao_usuarios.py
  46. 732
      sapl/legacy/migration.py
  47. 13
      sapl/legacy/models.py
  48. 3
      sapl/legacy/scripts/exporta_zope/.gitignore
  49. 261
      sapl/legacy/scripts/exporta_zope/exporta_zope.py
  50. 3
      sapl/legacy/scripts/exporta_zope/requirements.txt
  51. 28
      sapl/legacy/scripts/fix_tables.sql
  52. 7
      sapl/legacy/scripts/migra_dbs.sh
  53. 73
      sapl/legacy/scripts/migra_um_db.sh
  54. 10
      sapl/legacy/scripts/recria_dbs_postgres.sh
  55. 1
      sapl/legacy/scripts/recria_um_db_postgres.sh
  56. 150
      sapl/legacy/scripts/street_sweeper.py
  57. 15
      sapl/legacy_migration_settings.py
  58. 3
      sapl/materia/apps.py
  59. 6
      sapl/materia/email_utils.py
  60. 310
      sapl/materia/forms.py
  61. 26
      sapl/materia/migrations/0012_auto_20170815_1238.py
  62. 21
      sapl/materia/migrations/0013_auto_20170816_1136.py
  63. 19
      sapl/materia/migrations/0014_auto_20170905_0818.py
  64. 37
      sapl/materia/migrations/0015_auto_20170908_1024.py
  65. 16
      sapl/materia/migrations/0016_merge.py
  66. 20
      sapl/materia/migrations/0017_auto_20170918_1257.py
  67. 101
      sapl/materia/migrations/0018_auto_20171113_1339.py
  68. 26
      sapl/materia/migrations/0019_auto_20171127_1500.py
  69. 27
      sapl/materia/migrations/0020_auto_20171204_1658.py
  70. 80
      sapl/materia/models.py
  71. 10
      sapl/materia/receivers.py
  72. 3
      sapl/materia/tests/test_materia.py
  73. 68
      sapl/materia/tests/test_materia_form.py
  74. 14
      sapl/materia/urls.py
  75. 304
      sapl/materia/views.py
  76. 119
      sapl/norma/fixtures/pre_popula_tipo_vinculo_norma.json
  77. 70
      sapl/norma/forms.py
  78. 39
      sapl/norma/migrations/0008_normajuridica_popula_tipo_vinculo_norma.py
  79. 26
      sapl/norma/migrations/0009_auto_20171113_1339.py
  80. 4
      sapl/norma/models.py
  81. 10
      sapl/norma/tests/test_norma.py
  82. 74
      sapl/norma/views.py
  83. 6
      sapl/painel/urls.py
  84. 387
      sapl/painel/views.py
  85. 56
      sapl/parlamentares/forms.py
  86. 27
      sapl/parlamentares/migrations/0009_auto_20170905_1617.py
  87. 36
      sapl/parlamentares/migrations/0010_corrige_data_inicio_mandato.py
  88. 20
      sapl/parlamentares/migrations/0011_auto_20171010_1433.py
  89. 25
      sapl/parlamentares/migrations/0012_auto_20171020_1045.py
  90. 15
      sapl/parlamentares/models.py
  91. 24
      sapl/parlamentares/tests/test_parlamentares.py
  92. 3
      sapl/parlamentares/urls.py
  93. 61
      sapl/parlamentares/views.py
  94. 99
      sapl/protocoloadm/forms.py
  95. 19
      sapl/protocoloadm/migrations/0002_remove_documentoadministrativo_numero_protocolo.py
  96. 3
      sapl/protocoloadm/models.py
  97. 33
      sapl/protocoloadm/tests/test_protocoloadm.py
  98. 122
      sapl/protocoloadm/views.py
  99. 4
      sapl/relatorios/templates/pdf_sessao_plenaria_gerar.py
  100. 126
      sapl/relatorios/views.py

1
.coveragerc

@ -1,4 +1,5 @@
[run]
source = sapl
omit =
sapl/wsgi.py
manage.py

3
.gitignore

@ -6,6 +6,9 @@ __pycache__/
# C extensions
*.so
# Nodejs
node_modules/
# Distribution / packaging
.Python
env/

2
README.rst

@ -22,7 +22,7 @@ Instalação do Ambiente de Desenvolvimento
Instruções para Importação da base mysql 2.5
============================================
`Importação da Base do SAPL 2.5 para SAPL 3.1 <https://github.com/interlegis/sapl/blob/master/docs/importacao_25_31.rst>`_
`Importação da Base do SAPL 2.5 para SAPL 3.1 <https://github.com/interlegis/sapl/wiki/Migra%C3%A7%C3%A3o-sapl-2.5-para-3.1>`_
Instruções para Deploy

1
check_migrations.sh

@ -21,6 +21,7 @@ if [ $MIGRATIONS -eq 0 ]; then
echo
echo -e "${RED}ALGUMAS ALTERAÇÕES EXIGEM MIGRAÇÃO.${NC}"
echo -e "${RED}RODE 'python manage.py makemigrations' ANTES DE SUBMETER SEU CÓDIGO...${NC}"
echo -e "${RED}lembre de adicionar os arquivos criados ao git com 'git add .' ou semelhante.${NC}"
echo
exit 1
fi

22
check_qa.sh

@ -1,17 +1,17 @@
#!/bin/bash
# Check if there's some debug breakpoint in codebase
# Verifica se um breakpoint foi esquecido no código
me=`basename "$0"`
stmts=`grep --exclude=$me -r -l "ipdb.set_trace()" * | wc -l`
if [ $stmts != '0' ]
busca=`grep --color=auto --exclude=$me --exclude=ipython_log.py* -r -l "pdb.set_trace()" .`
if [ ! -z "$busca" ]
then
echo "=================================================================="
echo "ERROR: ipdb.set_trace() call in codebase! Remove, please."
grep --exclude=$me -r -n "ipdb.set_trace()" *
echo "=================================================================="
echo "============================================================================"
echo "ERROR: pdb.set_trace() encontrado nos seguintes arquivos. Remova, por favor."
echo "$busca"
echo "============================================================================"
fi
# QA checks: run this before every commit
./manage.py check
flake8 --exclude='ipython_log.py*,migrations,templates' .
isort --recursive --check-only --skip='migrations' --skip='templates' --skip='ipython_log.py' .
# ./manage.py check
# flake8 --exclude='ipython_log.py*,migrations,templates' .
# isort --recursive --check-only --skip='migrations' --skip='templates' --skip='ipython_log.py' .

15
docker-compose.yml

@ -1,5 +1,6 @@
sapldb:
image: postgres
restart: always
environment:
POSTGRES_PASSWORD: sapl
POSTGRES_USER: sapl
@ -8,9 +9,19 @@ sapldb:
volumes:
- sapldb_data:/var/lib/postgresql/data/
ports:
- "5532:5432"
- "5432:5432"
sapl:
image: interlegis/sapl:3.1.20-BETA
image: interlegis/sapl:3.1.39-BETA
restart: always
environment:
ADMIN_PASSWORD: interlegis
ADMIN_EMAIL: email@dominio.net
DEBUG: 'False'
USE_TLS: 'False'
EMAIL_PORT: 587
EMAIL_HOST: smtp.dominio.net
EMAIL_HOST_USER: usuariosmtp
EMAIL_HOST_PASSWORD: senhasmtp
volumes:
- sapl_data:/var/interlegis/sapl/data
- sapl_media:/var/interlegis/sapl/media

34
docs/credits.txt

@ -0,0 +1,34 @@
Créditos do SAPL - até versão 2.5
=================================
Obrigado aos colaboradores:
- Adriano Gomes
- Angelo Marcondes Neto
- Claudio Morale
- Daniel C. Azevedo
- Davi Lima de Medeiros
- Edson Ma
- Fernando Ciciliati Júnior
- Gustavo Lepri
- Halison Casimiro
- Helder Vieira
- Jean Rodrigo Ferri
- João Lima
- José Borges
- Leandro Roberto
- Leonardo Caballero
- Luciano Di Fázio
- Luis Fernando Pires Machado
- Marcio Mazza
- Marcos Fragomeni
- Maria Cristina André de Mello
- Marta Maria Pincowsca Cardoso Maia
- Paulo Fernandes de Souza Júnior
- Petronio Barbosa Carvalho
- Ricardo Esperandio
- Rodrigo Barbosa Luz
- Sesóstris Vieira
- Sérgio Damiati
- Wilton Souza Alencar
- Wu Man Qi

2
docs/deploy.rst

@ -30,7 +30,7 @@ Com o ambiente em produção, os arquivos estáticos devem ser servidos pelo web
para que os arquivos SASS/SCSS sejam compilados em arquivos .css em ambiente de produção, e em seguida rode::
./manage.py collectstatic --no-input
./manage.py collectstatic --no-input --clear
para coletar todos os arquivos estáticos do projeto e guarda-los no diretório definido em `STATIC_ROOT`, que será também o diretório no qual o `NGINX` irá referenciar para a aplicação.

111
docs/importacao_25_31.rst

@ -1,111 +0,0 @@
Instruções para Importação da base mysql 2.5
============================================
Para entrar no ambiente virtual::
workon sapl
Instalar Dependências::
pip3 install -r requirements/migration-requirements.txt
Criar um arquivo sapl/legacy/.env com o seguinte conteúdo (parametros de acesso ao banco 2.5)::
DATABASE_URL = mysql://[usuario do mysql]:[senha do myuysql]@[host]:[porta]/[banco]
o conteúdo do arquivo será semelhante a isso::
DATABASE_URL = mysql://sapl:sapl@localhost:3306/interlegis
Posteriormente rodar a seguinte sequencia de comandos estando no ambiente virtual::
./manage.py shell --settings=sapl.legacy_migration_settings
%run sapl/legacy/migration.py
migrate()
Migração de documentos do sapl 2.5
----------------------------------
No sapl 2.5 todos os documentos ficavam armazenados no ZODB (o banco do Zope).
No sapl 3.1 eles ficam no sistema de arquivos convencional e portanto precisam:
1. ser exportados para o sistema de arquivos
2. ser vinculados ao novo banco importado para o sapl 3.1
Exportar os documentos para o sistema de arquivos
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Siga os seguintes passos:
1. Instale o `Products.FSDump` no sapl 2.5.
Para isso basta colocar a pasta `Products/FSDump` do projeto https://github.com/zopefoundation/Products.FSDump na pasta `Products` da instalação do sapl 2.5 e reiniciar o Zope.
A pasta a ser instalada é a seguinte:
https://github.com/zopefoundation/Products.FSDump/tree/master/Products/FSDump
2. Na ZMI, na pasta `sapl_documentos`, adicione um objeto do tipo `Dumper`:
- Em `Filesystem path` escolha uma pasta do sistema de arquivos local para onde os arquivos serão copiados
- Desmarque a opção `Use .metadata file`
- Clique no botão `Add`
3. Use o objeto `Dumper` criado para exportar os arquivos:
- Clique no objeto `Dumper` criado para ver suas opções
- Confira seus parametros e clique em `Change and Dump`
- Aguarde a exportação dos arquivos e verifique que foram copiados para a pasta indicada
Vincular os documentos ao novo banco do sapl 3.1
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1. Primeiramente migre o banco do sapl 2.5 para o sapl 3.1
2. Copie a pasta exportada `sapl_documentos` dentro da pasta `media` da instalação do sapl 3.1
3. De forma semelhante ao realizado na migração do banco, dentro no mesmo ambiente virtual, rode os seguintes comandos::
./manage.py shell --settings=sapl.legacy_migration_settings
%run sapl/legacy/migracao_documentos.py
migrar_documentos()
Para indexar os arquivos para pesquisa textual
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1. workon sapl
2. ./manage.py rebuild_index
Dependendo da quantidade de arquivos a serem indexados, pode ser listado o seguinte erro 'Too many open files'
Isto está ligado a quantidade máxima de aquivos que podem ser abertos ao mesmo tempo pelo sistema operacional
Para aumentar este limite::
sudo nano /etc/security/limits.conf
* soft nofile 9000
* hard nofile 65000
sudo nano /etc/pam.d/common-session
session required pam_limits.so
Após reiniciar, verificar se foram carregados os novos parâmetros com o comando::
ulimit -a
deve ser apresentado o seguinte::
open files (-n) 9000

5
docs/instalacao31.rst

@ -185,11 +185,6 @@ Copie a chave que aparecerá, edite o arquivo .env e altere o valor do parâmetr
./manage.py migrate
* Atualizar arquivos estáticos::
./manage.py collectstatic --noinput
* Subir o servidor do django::
./manage.py runserver 0.0.0.0:8001

2
gunicorn_start.sh

@ -42,4 +42,6 @@ exec gunicorn ${DJANGO_WSGI_MODULE}:application \
--name $NAME \
--workers $NUM_WORKERS \
--user $USER \
--access-logfile - \
--error-logfile - \
--bind=unix:$SOCKFILE

4
requirements/dev-requirements.txt

@ -1,9 +1,9 @@
-r test-requirements.txt
autopep8==1.2.4
beautifulsoup4==4.4.1
beautifulsoup4==4.6.0
django-debug-toolbar==1.5
ipdb==0.10.1
pip-review==0.4
pygraphviz==1.3.1
pytest-ipdb==0.1-prerelease2
pipdeptree
pipdeptree==0.10.1

2
requirements/migration-requirements.txt

@ -1,2 +1,2 @@
-r dev-requirements.txt
mysqlclient
mysqlclient==1.3.12

4
requirements/requirements.txt

@ -16,8 +16,8 @@ django-filter==0.15.3
django-floppyforms==1.6.2
django-model-utils==2.5
django-sass-processor==0.5.4
djangorestframework
drfdocs
djangorestframework==3.4.0
drfdocs==0.0.11
easy-thumbnails==2.3
git+git://github.com/interlegis/trml2pdf.git
libsass==0.11.1

2
requirements/test-requirements.txt

@ -1,6 +1,6 @@
-r requirements.txt
coverage==4.1
django-webtest
django-webtest==1.7.8
flake8==2.6.2
isort==4.2.5
model-mommy==1.2.6

16
sapl/api/forms.py

@ -179,18 +179,16 @@ class AutoresPossiveisFilterSet(FilterSet):
data_inicio__lte=data_relativa,
data_fim__gte=data_relativa).first()
params = {
'parlamentar_set__mandato__data_inicio_mandato__lte':
data_relativa,
'parlamentar_set__mandato__data_fim_mandato__gte': data_relativa
}
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():
params['parlamentar_set__ativo'] = True
q = q & Q(parlamentar_set__ativo=True)
qs = queryset.filter(**params)
return qs
return queryset.filter(q)
def filter_comissao(self, queryset, data_relativa):
return queryset.filter(

65
sapl/base/forms.py

@ -1,4 +1,3 @@
import django_filters
from crispy_forms.bootstrap import FieldWithButtons, InlineRadios, StrictButton
from crispy_forms.helper import FormHelper
from crispy_forms.layout import HTML, Button, Div, Field, Fieldset, Layout, Row
@ -12,8 +11,9 @@ from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db import models, transaction
from django.forms import ModelForm
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import string_concat
from django.utils.translation import ugettext_lazy as _
import django_filters
from sapl.base.models import Autor, TipoAutor
from sapl.crispy_layout_mixin import (SaplFormLayout, form_actions, to_column,
@ -28,6 +28,7 @@ from sapl.utils import (RANGE_ANOS, ChoiceWithoutValidationField,
from .models import AppConfig, CasaLegislativa
ACTION_CREATE_USERS_AUTOR_CHOICE = [
('A', _('Associar um usuário existente')),
('N', _('Autor sem Usuário de Acesso ao Sapl')),
@ -45,27 +46,14 @@ STATUS_USER_CHOICE = [
class TipoAutorForm(ModelForm):
content_type = forms.ModelChoiceField(
queryset=ContentType.objects.all(),
label=TipoAutor._meta.get_field('content_type').verbose_name,
required=False)
class Meta:
model = TipoAutor
fields = ['descricao',
'content_type']
fields = ['descricao']
def __init__(self, *args, **kwargs):
super(TipoAutorForm, self).__init__(*args, **kwargs)
content_types = ContentType.objects.get_for_models(
*models_with_gr_for_model(Autor))
self.fields['content_type'].choices = [
('', _('Outros (Especifique)'))] + [
(ct.pk, ct) for key, ct in content_types.items()]
class AutorForm(ModelForm):
senha = forms.CharField(
@ -231,7 +219,7 @@ class AutorForm(ModelForm):
if 'status_user' in self.Meta.fields:
if self.instance.pk and self.instance.user_id:
if getattr(
self.instance.user.username,
self.instance.user,
get_user_model().USERNAME_FIELD) != cd['username']:
if 'status_user' not in cd or not cd['status_user']:
raise ValidationError(
@ -409,7 +397,7 @@ class RelatorioAtasFilterSet(django_filters.FilterSet):
self.form.helper.form_method = 'GET'
self.form.helper.layout = Layout(
Fieldset(_('Atas das Sessões Plenárias'),
row1, form_actions(save_label='Pesquisar'))
row1, form_actions(label='Pesquisar'))
)
@ -439,7 +427,7 @@ class RelatorioPresencaSessaoFilterSet(django_filters.FilterSet):
self.form.helper.form_method = 'GET'
self.form.helper.layout = Layout(
Fieldset(_('Presença dos parlamentares nas sessões plenárias'),
row1, form_actions(save_label='Pesquisar'))
row1, form_actions(label='Pesquisar'))
)
@property
@ -483,7 +471,7 @@ class RelatorioHistoricoTramitacaoFilterSet(django_filters.FilterSet):
self.form.helper.layout = Layout(
Fieldset(_('Histórico de Tramita'),
row1, row2,
form_actions(save_label='Pesquisar'))
form_actions(label='Pesquisar'))
)
@ -514,7 +502,7 @@ class RelatorioMateriasTramitacaoilterSet(django_filters.FilterSet):
self.form.helper.layout = Layout(
Fieldset(_('Pesquisa de Matéria em Tramitação'),
row1, row2, row3, row4,
form_actions(save_label='Pesquisar'))
form_actions(label='Pesquisar'))
)
@ -540,7 +528,7 @@ class RelatorioMateriasPorAnoAutorTipoFilterSet(django_filters.FilterSet):
self.form.helper.layout = Layout(
Fieldset(_('Pesquisar'),
row1,
form_actions(save_label='Pesquisar'))
form_actions(label='Pesquisar'))
)
@ -586,7 +574,7 @@ class RelatorioMateriasPorAutorFilterSet(django_filters.FilterSet):
HTML(autor_label),
HTML(autor_modal),
row3,
form_actions(save_label='Pesquisar'))
form_actions(label='Pesquisar'))
)
@ -623,7 +611,7 @@ class CasaLegislativaForm(ModelForm):
logotipo = self.cleaned_data.get('logotipo', False)
if logotipo:
if logotipo.size > MAX_IMAGE_UPLOAD_SIZE:
raise ValidationError("Imagem muito grande. ( > 2mb )")
raise ValidationError("Imagem muito grande. ( > 2MB )")
return logotipo
@ -642,18 +630,25 @@ class LoginForm(AuthenticationForm):
class ConfiguracoesAppForm(ModelForm):
mostrar_brasao_painel = forms.BooleanField(
help_text=_('Sugerimos fortemente que faça o upload de imagens com '
'o fundo transparente.'),
label=_('Mostrar brasão da Casa no painel?'),
required=False)
class Meta:
model = AppConfig
fields = ['documentos_administrativos',
'sequencia_numeracao',
'painel_aberto',
# 'painel_aberto', # TODO: a ser implementado na versão 3.2
'texto_articulado_proposicao',
'texto_articulado_materia',
'texto_articulado_norma',
'proposicao_incorporacao_obrigatoria',
'cronometro_discurso',
'cronometro_aparte',
'cronometro_ordem']
'cronometro_ordem',
'mostrar_brasao_painel']
def __init__(self, *args, **kwargs):
super(ConfiguracoesAppForm, self).__init__(*args, **kwargs)
@ -661,6 +656,20 @@ class ConfiguracoesAppForm(ModelForm):
self.fields['cronometro_aparte'].widget.attrs['class'] = 'cronometro'
self.fields['cronometro_ordem'].widget.attrs['class'] = 'cronometro'
def clean_mostrar_brasao_painel(self):
mostrar_brasao_painel = self.cleaned_data.get(
'mostrar_brasao_painel', False)
casa = CasaLegislativa.objects.first()
if not casa:
raise ValidationError("Não há casa legislativa relacionada")
if (not bool(casa.logotipo) and mostrar_brasao_painel):
raise ValidationError("Não há logitipo configurado para esta "
"Casa legislativa.")
return mostrar_brasao_painel
class RecuperarSenhaForm(PasswordResetForm):
@ -671,7 +680,7 @@ class RecuperarSenhaForm(PasswordResetForm):
self.helper.layout = Layout(
Fieldset(_('Insira o e-mail cadastrado com a sua conta'),
row1,
form_actions(save_label='Enviar'))
form_actions(label='Enviar'))
)
super(RecuperarSenhaForm, self).__init__(*args, **kwargs)
@ -702,4 +711,4 @@ class NovaSenhaForm(SetPasswordForm):
self.helper = FormHelper()
self.helper.layout = Layout(
row1,
form_actions(save_label='Enviar'))
form_actions(label='Enviar'))

20
sapl/base/migrations/0009_appconfig_mostrar_brasao_painel.py

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.13 on 2017-10-16 20:06
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('base', '0008_auto_20170814_1409'),
]
operations = [
migrations.AddField(
model_name='appconfig',
name='mostrar_brasao_painel',
field=models.BooleanField(default=False, verbose_name='Mostrar brasão da Casa no painel?'),
),
]

19
sapl/base/migrations/0010_remove_appconfig_painel_aberto.py

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.11 on 2017-10-18 16:50
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('base', '0009_appconfig_mostrar_brasao_painel'),
]
operations = [
migrations.RemoveField(
model_name='appconfig',
name='painel_aberto',
),
]

19
sapl/base/migrations/0011_auto_20171121_0958.py

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.13 on 2017-11-21 11:58
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('base', '0010_remove_appconfig_painel_aberto'),
]
operations = [
migrations.AlterModelOptions(
name='appconfig',
options={'ordering': ('-id',), 'permissions': (('menu_sistemas', 'Renderizar Menu Sistemas'), ('view_tabelas_auxiliares', 'Visualizar Tabelas Auxiliares')), 'verbose_name': 'Configurações da Aplicação', 'verbose_name_plural': 'Configurações da Aplicação'},
),
]

20
sapl/base/migrations/0012_auto_20171205_0917.py

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.13 on 2017-12-05 11:17
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('base', '0011_auto_20171121_0958'),
]
operations = [
migrations.AlterField(
model_name='tipoautor',
name='descricao',
field=models.CharField(help_text='Obs: Não crie tipos de autores semelhante aos tipos fixos. ', max_length=50, verbose_name='Descrição'),
),
]

69
sapl/base/models.py

@ -1,10 +1,18 @@
import reversion
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.db.models.signals import post_migrate
from django.db.utils import DEFAULT_DB_ALIAS
from django.utils.translation import ugettext_lazy as _
import reversion
from sapl.utils import (
UF,
YES_NO_CHOICES,
get_settings_auth_user_model,
models_with_gr_for_model
)
from sapl.utils import UF, YES_NO_CHOICES, get_settings_auth_user_model
TIPO_DOCUMENTO_ADMINISTRATIVO = (('O', _('Ostensivo')),
('R', _('Restritivo')))
@ -122,9 +130,10 @@ class AppConfig(models.Model):
verbose_name=_('Sequência de numeração'),
choices=SEQUENCIA_NUMERACAO, default='A')
painel_aberto = models.BooleanField(
verbose_name=_('Painel aberto para usuário anônimo'),
choices=YES_NO_CHOICES, default=False)
# TODO: a ser implementado na versão 3.2
# painel_aberto = models.BooleanField(
# verbose_name=_('Painel aberto para usuário anônimo'),
# choices=YES_NO_CHOICES, default=False)
texto_articulado_proposicao = models.BooleanField(
verbose_name=_('Usar Textos Articulados para Proposições'),
@ -157,6 +166,10 @@ class AppConfig(models.Model):
blank=True,
null=True)
mostrar_brasao_painel = models.BooleanField(
default=False,
verbose_name=_('Mostrar brasão da Casa no painel?'))
class Meta:
verbose_name = _('Configurações da Aplicação')
verbose_name_plural = _('Configurações da Aplicação')
@ -164,13 +177,15 @@ class AppConfig(models.Model):
('menu_sistemas', _('Renderizar Menu Sistemas')),
('view_tabelas_auxiliares', _('Visualizar Tabelas Auxiliares')),
)
ordering = ('-id',)
@classmethod
def attr(cls, attr):
config = AppConfig.objects.first()
if not config:
return ''
config = AppConfig()
config.save()
return getattr(config, attr)
@ -181,7 +196,10 @@ class AppConfig(models.Model):
@reversion.register()
class TipoAutor(models.Model):
descricao = models.CharField(max_length=50, verbose_name=_('Descrição'))
descricao = models.CharField(
max_length=50, verbose_name=_('Descrição'),
help_text=_('Obs: Não crie tipos de autores '
'semelhante aos tipos fixos. '))
content_type = models.OneToOneField(
ContentType,
@ -242,3 +260,40 @@ class Autor(models.Model):
return str(self.partido)
else:
"""
def cria_models_tipo_autor(app_config, verbosity=2, interactive=True,
using=DEFAULT_DB_ALIAS, **kwargs):
models = models_with_gr_for_model(Autor)
print("\n\033[93m\033[1m{}\033[0m".format(
_('Atualizando registros TipoAutor do SAPL:')))
for model in models:
content_type = ContentType.objects.get_for_model(model)
tipo_autor = TipoAutor.objects.filter(
content_type=content_type.id).exists()
if tipo_autor:
msg1 = "Carga de {} não efetuada.".format(
TipoAutor._meta.verbose_name)
msg2 = " Já Existe um {} {} relacionado...".format(
TipoAutor._meta.verbose_name,
model._meta.verbose_name)
msg = " {}{}".format(msg1, msg2)
else:
novo_autor = TipoAutor()
novo_autor.content_type_id = content_type.id
novo_autor.descricao = model._meta.verbose_name
novo_autor.save()
msg1 = "Carga de {} efetuada.".format(
TipoAutor._meta.verbose_name)
msg2 = " {} {} criado...".format(
TipoAutor._meta.verbose_name, content_type.model)
msg = " {}{}".format(msg1, msg2)
print(msg)
# Disconecta função para evitar a chamada repetidas vezes.
post_migrate.disconnect(receiver=cria_models_tipo_autor)
post_migrate.connect(receiver=cria_models_tipo_autor)

158
sapl/base/search_indexes.py

@ -3,33 +3,36 @@ import os.path
import re
import string
import textract
from django.db.models import Q, F, Value
from django.db.models.fields import TextField
from django.db.models.fields.files import FieldFile
from django.db.models.functions import Concat
from django.template import loader
from haystack import indexes
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
import textract
from sapl.compilacao.models import TextoArticulado, Dispositivo,\
STATUS_TA_PUBLIC, STATUS_TA_IMMUTABLE_PUBLIC
from sapl.materia.models import DocumentoAcessorio, MateriaLegislativa
from sapl.norma.models import NormaJuridica
from sapl.settings import BASE_DIR, SOLR_URL
logger = logging.getLogger(BASE_DIR.name)
class DocumentoAcessorioIndex(indexes.SearchIndex, indexes.Indexable):
text = indexes.CharField(document=True, use_template=True)
class TextExtractField(CharField):
filename = 'arquivo'
model = DocumentoAcessorio
template_name = 'materia/documentoacessorio_text.txt'
def __init__(self, **kwargs):
super().__init__(**kwargs)
assert self.model_attr
def get_model(self):
return self.model
def index_queryset(self, using=None):
return self.get_model().objects.all()
def get_updated_field(self):
return 'data_ultima_atualizacao'
if not isinstance(self.model_attr, (list, tuple)):
self.model_attr = (self.model_attr, )
def solr_extraction(self, arquivo):
extracted_data = self._get_backend(None).extract_file_contents(
@ -59,71 +62,122 @@ class DocumentoAcessorioIndex(indexes.SearchIndex, indexes.Indexable):
print(msg)
logger.error(msg)
def prepare(self, obj):
if not self.filename or not self.model or not self.template_name:
raise Exception
data = super(DocumentoAcessorioIndex, self).prepare(obj)
arquivo = getattr(obj, self.filename)
if arquivo:
if not os.path.exists(arquivo.path):
return self.prepared_data
if not os.path.splitext(arquivo.path)[1][:1]:
return self.prepared_data
def file_extractor(self, arquivo):
if not os.path.exists(arquivo.path) or \
not os.path.splitext(arquivo.path)[1][:1]:
return ''
# Em ambiente de produção utiliza-se o SOLR
if SOLR_URL:
try:
extracted_data = self.solr_extraction(arquivo)
return self.solr_extraction(arquivo)
except Exception:
self.print_error(arquivo)
return self.prepared_data
# Em ambiente de DEV utiliza-se o Whoosh
# Como ele não possui extração, faz-se uso do textract
else:
try:
extracted_data = self.whoosh_extraction(arquivo)
return self.whoosh_extraction(arquivo)
except ExtensionNotSupported as e:
print(str(e))
logger.error(str(e))
return self.prepared_data
except Exception:
self.print_error(arquivo)
return self.prepared_data
return ''
def ta_extractor(self, value):
r = []
for ta in value.filter(privacidade__in=[
STATUS_TA_PUBLIC,
STATUS_TA_IMMUTABLE_PUBLIC]):
dispositivos = Dispositivo.objects.filter(
Q(ta=ta) | Q(ta_publicado=ta)
).order_by(
'ordem'
).annotate(
rotulo_texto=Concat(
F('rotulo'), Value(' '), F('texto'),
output_field=TextField(),
)
).values_list(
'rotulo_texto', flat=True)
r += list(filter(lambda x: x.strip(), dispositivos))
return ' '.join(r)
def string_extractor(self, value):
return value
def extract_data(self, obj):
data = ''
for attr, func in self.model_attr:
if not hasattr(obj, attr) or not hasattr(self, func):
raise Exception
# Now we'll finally perform the template processing to render the
# text field with *all* of our metadata visible for templating:
t = loader.select_template((
'search/indexes/' + self.template_name, ))
data['text'] = t.render({'object': obj,
'extracted': extracted_data})
value = getattr(obj, attr)
if not value:
continue
data += getattr(self, func)(value)
return data
return self.prepared_data
def prepare_template(self, obj):
app_label, model_name = get_model_ct_tuple(obj)
template_names = ['search/indexes/%s/%s_%s.txt' %
(app_label, model_name, self.instance_name)]
t = loader.select_template(template_names)
class MateriaLegislativaIndex(DocumentoAcessorioIndex):
text = indexes.CharField(document=True, use_template=True)
return t.render({'object': obj,
'extracted': self.extract_data(obj)})
filename = 'texto_original'
model = MateriaLegislativa
template_name = 'materia/materialegislativa_text.txt'
class DocumentoAcessorioIndex(SearchIndex, Indexable):
model = DocumentoAcessorio
text = TextExtractField(
document=True, use_template=True,
model_attr=(
('arquivo', 'file_extractor'),
('ementa', 'string_extractor'),
('indexacao', 'string_extractor'),
)
)
def get_model(self):
return self.model
def index_queryset(self, using=None):
return self.get_model().objects.all()
def get_updated_field(self):
return 'data_ultima_atualizacao'
class NormaJuridicaIndex(DocumentoAcessorioIndex):
text = indexes.CharField(document=True, use_template=True)
filename = 'texto_integral'
model = NormaJuridica
template_name = 'norma/normajuridica_text.txt'
text = TextExtractField(
document=True, use_template=True,
model_attr=(
('texto_integral', 'file_extractor'),
('texto_articulado', 'ta_extractor'),
('ementa', 'string_extractor'),
('indexacao', 'string_extractor'),
('observacao', 'string_extractor'),
)
)
def get_updated_field(self):
return 'data_ultima_atualizacao'
class MateriaLegislativaIndex(DocumentoAcessorioIndex):
model = MateriaLegislativa
text = TextExtractField(
document=True, use_template=True,
model_attr=(
('texto_original', 'file_extractor'),
('texto_articulado', 'ta_extractor'),
('ementa', 'string_extractor'),
('indexacao', 'string_extractor'),
('observacao', 'string_extractor'),
)
)

9
sapl/base/templatetags/base_tags.py

@ -0,0 +1,9 @@
from django import template
register = template.Library()
@register.filter
def tipoautor_contenttype_list(tipo):
return 'sapl.'+tipo.content_type.app_label+':'+tipo.content_type.model+'_list'

21
sapl/base/tests/test_form.py

@ -1,3 +1,4 @@
import pytest
from django.utils.translation import ugettext_lazy as _
from sapl.base.forms import CasaLegislativaForm
@ -18,3 +19,23 @@ def test_valida_campos_obrigatorios_casa_legislativa_form():
assert errors['uf'] == [_('Este campo é obrigatório.')]
assert len(errors) == 6
@pytest.mark.django_db(transaction=False)
def test_casa_legislativa_form_invalido():
form = CasaLegislativaForm(data={'codigo': 'codigo',
'nome': 'nome',
'sigla': 'sg',
'endereco': 'endereco',
'cep': '7000000',
'municipio': 'municipio',
'uf': 'uf',
'telefone': '33333333',
'fax': '33333333',
'logotipo': 'image',
'endereco_web': 'web',
'email': 'email',
'informacao_geral': 'informacao_geral'
})
assert not form.is_valid()

35
sapl/base/tests/test_view_base.py

@ -0,0 +1,35 @@
import pytest
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
@pytest.mark.django_db(transaction=False)
def test_incluir_casa_legislativa_errors(admin_client):
response = admin_client.post(reverse('sapl.base:casalegislativa_create'),
{'salvar': 'salvar'},
follow=True)
assert (response.context_data['form'].errors['nome'] ==
[_('Este campo é obrigatório.')])
assert (response.context_data['form'].errors['sigla'] ==
[_('Este campo é obrigatório.')])
assert (response.context_data['form'].errors['endereco'] ==
[_('Este campo é obrigatório.')])
assert (response.context_data['form'].errors['cep'] ==
[_('Este campo é obrigatório.')])
assert (response.context_data['form'].errors['municipio'] ==
[_('Este campo é obrigatório.')])
assert (response.context_data['form'].errors['uf'] ==
[_('Este campo é obrigatório.')])
@pytest.mark.django_db(transaction=False)
def test_incluir_tipo_autor_errors(admin_client):
response = admin_client.post(reverse('sapl.base:tipoautor_create'),
{'salvar': 'salvar'},
follow=True)
assert (response.context_data['form'].errors['descricao'] ==
[_('Este campo é obrigatório.')])

8
sapl/base/tests/teststub_urls.py

@ -3,7 +3,9 @@ from django.views.generic.base import TemplateView
from sapl.urls import urlpatterns as original_patterns
urlpatterns = original_patterns + patterns('', url(r'^zzzz$',
ptrn = patterns('',
url(r'^zzzz$',
TemplateView.as_view(
template_name='index.html'),
name='zzzz'))
template_name='index.html'), name='zzzz'))
urlpatterns = original_patterns + ptrn

10
sapl/base/urls.py

@ -11,7 +11,7 @@ from sapl.settings import EMAIL_SEND_USER
from .apps import AppConfig
from .forms import LoginForm, NovaSenhaForm, RecuperarSenhaForm
from .views import (AppConfigCrud, CasaLegislativaCrud, HelpView,
from .views import (AppConfigCrud, CasaLegislativaCrud, HelpTopicView,
RelatorioAtasView, RelatorioHistoricoTramitacaoView,
RelatorioMateriasPorAnoAutorTipoView,
RelatorioMateriasPorAutorView,
@ -54,12 +54,10 @@ urlpatterns = [
url(r'^sistema/autor/tipo/', include(TipoAutorCrud.get_urls())),
url(r'^sistema/autor/', include(AutorCrud.get_urls())),
url(r'^sistema/ajuda/', TemplateView.as_view(template_name='ajuda.html')),
url(r'^sistema/ajuda/(?P<topic>\w+)$',
HelpView.as_view(), name='help_topic'),
url(r'^sistema/ajuda/',
TemplateView.as_view(template_name='ajuda/index.html'),
name='help_base'),
HelpTopicView.as_view(), name='help_topic'),
url(r'^sistema/ajuda/$', TemplateView.as_view(template_name='ajuda.html'),
name='help'),
url(r'^sistema/casa-legislativa/', include(CasaLegislativaCrud.get_urls()),
name="casa_legislativa"),
url(r'^sistema/app-config/', include(AppConfigCrud.get_urls())),

97
sapl/base/views.py

@ -1,17 +1,17 @@
from django.conf import settings
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.tokens import default_token_generator
from django.core.exceptions import ObjectDoesNotExist
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.core.mail import send_mail
from django.core.urlresolvers import reverse
from django.db.models import Count
from django.http import HttpResponseRedirect
from django.http import Http404, HttpResponseRedirect
from django.template import TemplateDoesNotExist
from django.template.loader import get_template
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext_lazy as _, string_concat
from django.views.generic.base import TemplateView
from django_filters.views import FilterView
from haystack.views import SearchView
@ -23,7 +23,8 @@ from sapl.materia.models import (Autoria, MateriaLegislativa,
TipoMateriaLegislativa)
from sapl.sessao.models import (PresencaOrdemDia, SessaoPlenaria,
SessaoPlenariaPresenca)
from sapl.utils import parlamentares_ativos, sapl_logger
from sapl.utils import (parlamentares_ativos, sapl_logger,
show_results_filter_set)
from .forms import (CasaLegislativaForm, ConfiguracoesAppForm,
RelatorioAtasFilterSet,
@ -53,16 +54,52 @@ class ConfirmarEmailView(TemplateView):
class TipoAutorCrud(CrudAux):
model = TipoAutor
help_path = 'tipo-autor'
help_topic = 'tipo-autor'
class BaseMixin(CrudAux.BaseMixin):
list_field_names = ['descricao', 'content_type']
list_field_names = ['descricao']
form_class = TipoAutorForm
@property
def verbose_name(self):
vn = super().verbose_name
vn = string_concat(vn, ' ', _('Externo ao SAPL'))
return vn
class ListView(CrudAux.ListView):
def get_queryset(self):
qs = CrudAux.ListView.get_queryset(self)
qs = qs.filter(content_type__isnull=True)
return qs
def get_context_data(self, **kwargs):
context = CrudAux.ListView.get_context_data(self, **kwargs)
context['tipos_sapl'] = TipoAutor.objects.filter(
content_type__isnull=False)
return context
class TipoAutorMixin:
def dispatch(self, request, *args, **kwargs):
object = self.get_object()
if object.content_type:
raise PermissionDenied()
return super().get(request, *args, **kwargs)
class UpdateView(TipoAutorMixin, CrudAux.UpdateView):
pass
class DetailView(TipoAutorMixin, CrudAux.DetailView):
pass
class DeleteView(TipoAutorMixin, CrudAux.DeleteView):
pass
class AutorCrud(CrudAux):
model = Autor
help_path = 'autor'
help_topic = 'autor'
class BaseMixin(CrudAux.BaseMixin):
list_field_names = ['tipo', 'nome', 'user']
@ -202,6 +239,9 @@ class RelatorioAtasView(FilterView):
context['object_list'] = context['object_list'].exclude(upload_ata='')
qr = self.request.GET.copy()
context['filter_url'] = ('&' + qr.urlencode()) if len(qr) > 0 else ''
context['show_results'] = show_results_filter_set(qr)
return context
@ -220,7 +260,7 @@ class RelatorioPresencaSessaoView(FilterView):
return context
# =====================================================================
if 'salvar' in self.request.GET:
# if 'salvar' not in self.request.GET:
where = context['object_list'].query.where
_range = where.children[0].rhs
@ -294,6 +334,9 @@ class RelatorioPresencaSessaoView(FilterView):
# =====================================================================
qr = self.request.GET.copy()
context['filter_url'] = ('&' + qr.urlencode()) if len(qr) > 0 else ''
context['show_results'] = show_results_filter_set(qr)
return context
@ -308,6 +351,9 @@ class RelatorioHistoricoTramitacaoView(FilterView):
context['title'] = _('Histórico de Tramitações')
qr = self.request.GET.copy()
context['filter_url'] = ('&' + qr.urlencode()) if len(qr) > 0 else ''
context['show_results'] = show_results_filter_set(qr)
return context
@ -337,6 +383,8 @@ class RelatorioMateriasTramitacaoView(FilterView):
qr = self.request.GET.copy()
context['filter_url'] = ('&' + qr.urlencode()) if len(qr) > 0 else ''
context['show_results'] = show_results_filter_set(qr)
return context
@ -409,6 +457,8 @@ class RelatorioMateriasPorAnoAutorTipoView(FilterView):
qr = self.request.GET.copy()
context['filter_url'] = ('&' + qr.urlencode()) if len(qr) > 0 else ''
context['show_results'] = show_results_filter_set(qr)
if 'ano' in self.request.GET and self.request.GET['ano']:
ano = int(self.request.GET['ano'])
context['relatorio'] = self.get_materias_autor_ano(ano)
@ -447,6 +497,8 @@ class RelatorioMateriasPorAutorView(FilterView):
qr = self.request.GET.copy()
context['filter_url'] = ('&' + qr.urlencode()) if len(qr) > 0 else ''
context['show_results'] = show_results_filter_set(qr)
return context
@ -477,11 +529,15 @@ class CasaLegislativaCrud(CrudAux):
kwargs={'pk': self.kwargs['pk']}))
class HelpView(PermissionRequiredMixin, TemplateView):
# XXX treat non existing template as a 404!!!!
class HelpTopicView(TemplateView):
def get_template_names(self):
return ['ajuda/%s.html' % self.kwargs['topic']]
topico = self.kwargs['topic']
try:
get_template('ajuda/%s.html' % topico)
except TemplateDoesNotExist:
raise Http404()
return ['ajuda/%s.html' % topico]
class AppConfigCrud(CrudAux):
@ -501,15 +557,18 @@ class AppConfigCrud(CrudAux):
class CreateView(CrudAux.CreateView):
def get(self, request, *args, **kwargs):
app_config = AppConfig.objects.last()
if app_config:
app_config = AppConfig.objects.first()
if not app_config:
app_config = AppConfig()
app_config.save()
return HttpResponseRedirect(
reverse('sapl.base:appconfig_update',
kwargs={'pk': app_config.pk}))
else:
self.object = None
return super(CrudAux.CreateView, self).get(
request, *args, **kwargs)
def post(self, request, *args, **kwargs):
return self.get(request, *args, **kwargs)
class ListView(CrudAux.ListView):

19
sapl/comissoes/migrations/0003_auto_20171204_1658.py

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.11 on 2017-12-04 18:58
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('comissoes', '0002_auto_20170809_1236'),
]
operations = [
migrations.AlterModelOptions(
name='comissao',
options={'ordering': ['nome'], 'verbose_name': 'Comissão', 'verbose_name_plural': 'Comissões'},
),
]

1
sapl/comissoes/models.py

@ -95,6 +95,7 @@ class Comissao(models.Model):
class Meta:
verbose_name = _('Comissão')
verbose_name_plural = _('Comissões')
ordering = ['nome']
def __str__(self):
return self.sigla + ' - ' + self.nome

7
sapl/comissoes/views.py

@ -55,9 +55,6 @@ class ComposicaoCrud(MasterDetailCrud):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# context['composicao_pk'] = context['composicao_list'].last(
# ).pk if self.take_composicao_pk(
# ) == 0 else self.take_composicao_pk()
composicao_pk = self.take_composicao_pk()
@ -72,13 +69,13 @@ class ComposicaoCrud(MasterDetailCrud):
context['participacao_set'] = Participacao.objects.filter(
composicao__pk=context['composicao_pk']
).order_by('parlamentar')
).order_by('id')
return context
class ComissaoCrud(Crud):
model = Comissao
help_path = 'modulo_comissoes'
help_topic = 'modulo_comissoes'
public = [RP_LIST, RP_DETAIL, ]
class BaseMixin(Crud.BaseMixin):

25
sapl/compilacao/migrations/0004_auto_20171031_1327.py

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.13 on 2017-10-31 15:27
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('compilacao', '0003_auto_20170825_1136'),
]
operations = [
migrations.AlterField(
model_name='tipotextoarticulado',
name='participacao_social',
field=models.BooleanField(choices=[(True, 'Sim'), (False, 'Não')], default=False, verbose_name='Participação Social'),
),
migrations.AlterField(
model_name='tipotextoarticulado',
name='publicacao_func',
field=models.BooleanField(choices=[(True, 'Sim'), (False, 'Não')], default=False, verbose_name='Histórico de Publicação'),
),
]

21
sapl/compilacao/models.py

@ -1,6 +1,4 @@
from datetime import datetime
import reversion
from django.contrib import messages
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
@ -10,9 +8,11 @@ from django.db.models.aggregates import Max
from django.db.models.deletion import PROTECT
from django.http.response import Http404
from django.template import defaultfilters
from django.utils import timezone
from django.utils.decorators import classonlymethod
from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _
import reversion
from sapl.compilacao.utils import (get_integrations_view_names, int_to_letter,
int_to_roman)
@ -115,13 +115,13 @@ class TipoTextoArticulado(models.Model):
on_delete=models.SET_NULL,
verbose_name=_('Modelo Integrado'))
participacao_social = models.BooleanField(
blank=False,
blank=False, default=False,
choices=YES_NO_CHOICES,
verbose_name=_('Participação Social'))
publicacao_func = models.BooleanField(
choices=YES_NO_CHOICES,
blank=False,
blank=False, default=False,
verbose_name=_('Histórico de Publicação'))
perfis = models.ManyToManyField(
@ -357,9 +357,9 @@ class TextoArticulado(TimestampedMixin):
if not ta.data:
ta.data = getattr(obj, map_fields['data']
if map_fields['data'] else 'xxx',
datetime.now())
timezone.now())
if not ta.data:
ta.data = datetime.now()
ta.data = timezone.now()
ta.ementa = getattr(
obj, map_fields['ementa']
@ -370,15 +370,16 @@ class TextoArticulado(TimestampedMixin):
obj, map_fields['observacao']
if map_fields['observacao'] else 'xxx', '')
now = timezone.now()
ta.numero = getattr(
obj, map_fields['numero']
if map_fields['numero'] else 'xxx', int('%s%s%s' % (
int(datetime.now().year),
int(datetime.now().month),
int(datetime.now().day))))
int(now.year),
int(now.month),
int(now.day))))
ta.ano = getattr(obj, map_fields['ano']
if map_fields['ano'] else 'xxx', datetime.now().year)
if map_fields['ano'] else 'xxx', now.year)
ta.save()
return ta

3
sapl/compilacao/tests/test_tipo_texto_articulado_form.py

@ -3,8 +3,7 @@ from django.utils.translation import ugettext as _
from model_mommy import mommy
from sapl.compilacao import forms
from sapl.compilacao.models import (PerfilEstruturalTextoArticulado,
TipoNota)
from sapl.compilacao.models import PerfilEstruturalTextoArticulado, TipoNota
from sapl.compilacao.views import choice_models_in_extenal_views

32
sapl/compilacao/views.py

@ -1,7 +1,7 @@
import logging
import sys
from collections import OrderedDict
from datetime import timedelta
import logging
import sys
from braces.views import FormMessagesMixin
from django import forms
@ -19,8 +19,8 @@ from django.http.response import (HttpResponse, HttpResponseRedirect,
from django.shortcuts import get_object_or_404, redirect
from django.utils.dateparse import parse_date
from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import string_concat
from django.utils.translation import ugettext_lazy as _
from django.views.generic.base import TemplateView
from django.views.generic.detail import DetailView
from django.views.generic.edit import (CreateView, DeleteView, FormView,
@ -50,6 +50,7 @@ from sapl.compilacao.utils import (DISPOSITIVO_SELECT_RELATED,
from sapl.crud.base import Crud, CrudListView, make_pagination
from sapl.settings import BASE_DIR
TipoNotaCrud = Crud.build(TipoNota, 'tipo_nota')
TipoVideCrud = Crud.build(TipoVide, 'tipo_vide')
TipoPublicacaoCrud = Crud.build(TipoPublicacao, 'tipo_publicacao')
@ -829,7 +830,7 @@ class PublicacaoListView(PublicacaoMixin, ListView):
@property
def title(self):
return _('%s de %s' % (
return _('%s <small>(%s)</small>' % (
self.model._meta.verbose_name_plural,
self.ta))
@ -845,6 +846,8 @@ class PublicacaoListView(PublicacaoMixin, ListView):
def get_context_data(self, **kwargs):
context = super(PublicacaoListView, self).get_context_data(**kwargs)
context['title'] = self.title
context['object'] = self.ta
context['NO_ENTRIES_MSG'] = CrudListView.no_entries_msg
return context
@ -876,7 +879,17 @@ class PublicacaoCreateView(PublicacaoMixin, FormMessagesMixin, CreateView):
class PublicacaoDetailView(PublicacaoMixin, DetailView):
model = Publicacao
permission_required = 'compilacao.detail_publicacao'
permission_required = []
@property
def list_url(self):
return reverse_lazy('sapl.compilacao:ta_pub_list',
kwargs={
'ta_id': self.kwargs['ta_id']})
@property
def verbose_name_plural(self):
return self.model._meta.verbose_name_plural
class PublicacaoUpdateView(PublicacaoMixin, UpdateView):
@ -1157,10 +1170,14 @@ class TextEditView(CompMixin, TemplateView):
self.object.save()
messages.success(request, _(
'Texto Articulado desbloqueado com sucesso.'))
if self.object.content_object:
self.object.content_object.save()
else:
if 'lock' in request.GET:
# TODO - implementar logging de ação de usuário
# TODO - implementar logging de ação de usuário
notificacoes = self.get_notificacoes(
object_list=self.object.dispositivos_set.all(),
type_notificacoes=['danger', ])
@ -1183,6 +1200,9 @@ class TextEditView(CompMixin, TemplateView):
messages.success(request, _(
'Texto Articulado bloqueado com sucesso.'))
if self.object.content_object:
self.object.content_object.save()
return redirect(to=reverse_lazy(
'sapl.compilacao:ta_text', kwargs={
'ta_id': self.object.id}))

75
sapl/crispy_layout_mixin.py

@ -1,13 +1,13 @@
from math import ceil
import rtyaml
from crispy_forms.bootstrap import FormActions
from crispy_forms.helper import FormHelper
from crispy_forms.layout import HTML, Div, Fieldset, Layout, Submit
from django import template
from django.core.urlresolvers import reverse
from django.core.urlresolvers import reverse, reverse_lazy
from django.utils import formats
from django.utils.translation import ugettext as _
import rtyaml
def heads_and_tails(list_of_lists):
@ -34,9 +34,19 @@ def to_fieldsets(fields):
yield field
def form_actions(more=[], save_label=_('Salvar')):
def form_actions(more=[],
label=_('Salvar'), name='salvar', css_class='pull-right', disabled=True):
if disabled:
doubleclick = 'this.form.submit();this.disabled=true;'
else:
doubleclick = 'return true;'
return FormActions(
Submit('salvar', save_label, css_class='pull-right'), *more)
Submit(name, label, css_class=css_class,
# para impedir resubmissão do form
onclick=doubleclick),
*more)
class SaplFormLayout(Layout):
@ -46,7 +56,7 @@ class SaplFormLayout(Layout):
buttons = actions
if not buttons:
buttons = form_actions(save_label=save_label, more=[
buttons = form_actions(label=save_label, more=[
HTML('<a href="{{ view.cancel_url }}"'
' class="btn btn-inverse">%s</a>' % cancel_label)
if cancel_label else None])
@ -202,7 +212,16 @@ class CrispyLayoutFormMixin:
def get_column(self, fieldname, span):
obj = self.get_object()
func = None
if '|' in fieldname:
fieldname, func = tuple(fieldname.split('|'))
if func:
verbose_name, text = getattr(self, func)(obj, fieldname)
else:
verbose_name, text = get_field_display(obj, fieldname)
return {
'id': fieldname,
'span': span,
@ -210,6 +229,52 @@ class CrispyLayoutFormMixin:
'text': text,
}
def fk_urlize_for_detail(self, obj, fieldname):
field = obj._meta.get_field(fieldname)
value = getattr(obj, fieldname)
display = '<a href="{}">{}</a>'.format(
reverse(
'%s:%s_detail' % (
value._meta.app_config.name, value._meta.model_name),
args=(value.id,)),
value)
return field.verbose_name, display
def m2m_urlize_for_detail(self, obj, fieldname):
manager, fieldname = tuple(fieldname.split('__'))
manager = getattr(obj, manager)
verbose_name = manager.model._meta.verbose_name
display = ''
for item in manager.all():
obj_m2m = getattr(item, fieldname)
if obj == obj_m2m:
continue
verbose_name = item._meta.get_field(fieldname).verbose_name
display += '<li><a href="{}">{}</a></li>'.format(
reverse(
'%s:%s_detail' % (
obj_m2m._meta.app_config.name, obj_m2m._meta.model_name),
args=(obj_m2m.id,)),
obj_m2m)
display += ''
if display:
display = '<ul>%s</ul>' % display
else:
verbose_name = ''
return verbose_name, display
@property
def layout_display(self):

28
sapl/crud/base.py

@ -81,7 +81,7 @@ def make_pagination(index, num_pages):
"""
variáveis do crud:
help_path
help_topic
container_field
container_field_set
is_m2m
@ -865,7 +865,7 @@ class Crud:
DetailView = CrudDetailView
UpdateView = CrudUpdateView
DeleteView = CrudDeleteView
help_path = ''
help_topic = ''
class PublicMixin:
permission_required = []
@ -877,7 +877,7 @@ class Crud:
if view:
class CrudViewWithBase(cls.BaseMixin, view):
model = cls.model
help_path = cls.help_path
help_topic = cls.help_topic
crud = cls
CrudViewWithBase.__name__ = view.__name__
return CrudViewWithBase
@ -909,13 +909,13 @@ class Crud:
for regex, view, suffix in cruds]
@classonlymethod
def build(cls, _model, _help_path, _model_set=None, list_field_names=[]):
def build(cls, _model, _help_topic, _model_set=None, list_field_names=[]):
def create_class(_list_field_names):
class ModelCrud(cls):
model = _model
model_set = _model_set
help_path = _help_path
help_topic = _help_topic
list_field_names = _list_field_names
return ModelCrud
@ -938,16 +938,6 @@ class CrudAux(Crud):
class BaseMixin(Crud.BaseMixin):
subnav_template_name = None
def __init__(self, **kwargs):
super().__init__(**kwargs)
"""
Mantem as permissões individuais geradas pelo Crud através do
Modelo e adiciona a obrigatoriedade de permissão para view
tabelas auxiliares.
"""
self.permission_required = self.permission_required + \
self.crud.permission_required
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
"""Força o template filter subnav em base/templatetags/menus.py
@ -960,10 +950,10 @@ class CrudAux(Crud):
return context
@classonlymethod
def build(cls, _model, _help_path, _model_set=None, list_field_names=[]):
def build(cls, _model, _help_topic, _model_set=None, list_field_names=[]):
ModelCrud = Crud.build(
_model, _help_path, _model_set, list_field_names)
_model, _help_topic, _model_set, list_field_names)
class ModelCrudAux(CrudAux, ModelCrud):
pass
@ -1415,10 +1405,10 @@ class MasterDetailCrud(Crud):
return ''
@classonlymethod
def build(cls, model, parent_field, help_path,
def build(cls, model, parent_field, help_topic,
_model_set=None, list_field_names=[]):
crud = super(MasterDetailCrud, cls).build(
model, help_path, _model_set=_model_set,
model, help_topic, _model_set=_model_set,
list_field_names=list_field_names)
crud.parent_field = parent_field
return crud

4
sapl/crud/tests/stub_app/views.py

@ -6,7 +6,7 @@ from .models import City, Country
class CountryCrud(Crud):
model = Country
help_path = 'help_path',
help_topic = 'help_topic',
class ListView(CrudListView):
paginate_by = 10
@ -14,4 +14,4 @@ class CountryCrud(Crud):
class CityCrud(MasterDetailCrud):
model = City
help_path = 'help_path',
help_topic = 'help_topic',

23
sapl/decorators.py

@ -1,6 +1,8 @@
from datetime import date
from functools import wraps
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
def vigencia_atual(decorated_method):
"""
@ -13,25 +15,26 @@ def vigencia_atual(decorated_method):
"""
@wraps(decorated_method)
def display_atual(self):
try:
string_displayed = decorated_method(self)
except TypeError:
string_displayed = ""
if hasattr(self, 'data_inicio') and hasattr(self, 'data_fim'):
today = date.today()
today = timezone.now().today().date()
e_atual = self.data_inicio <= today <= self.data_fim
string_displayed = "{} {}".format(
string_displayed, "(Atual)" if e_atual else "")
else:
print('{} {}'.format(
"Instance does not have the attributes [{}, {}].".format(
instancia_sem_atributo = "{} [{}, {}].".format(
'Instância não possui os atributos',
'data_inicio',
'data_fim'
),
"Decorator @{} has been disabled.".format(
'data_fim')
mensagem_decorator = "Decorator @{} foi desabilitado.".format(
vigencia_atual.__name__()
)
print(_('{} {}'.format(
_(instancia_sem_atributo),
_(mensagem_decorator)
)
)
)

54
sapl/hashers.py

@ -0,0 +1,54 @@
import base64
import hashlib
from django.contrib.auth.hashers import PBKDF2PasswordHasher, make_password
from django.utils.encoding import force_bytes
def to_base64(source):
return base64.b64encode(source).decode('utf-8')
class ZopeSHA1PasswordHasher(PBKDF2PasswordHasher):
"""
The SHA1 password hashing algorithm used by Zope.
Zope uses `password + salt`, Django has `salt + password`.
Pre encode with SHA1 in this order and PBKDF2 afterwards.
based on https://www.fourdigits.nl/blog/converting-plone-data-to-django/
"""
algorithm = "zope_sha1_pbkdf2"
def encode(self, password, salt, iterations=None):
assert password is not None
assert salt
password = force_bytes(password)
decoded_salt = base64.b64decode(salt)
# this is what is stored in zope
hashed = hashlib.sha1(password + decoded_salt).digest() + decoded_salt
hashed = to_base64(hashed)
# encode again with the standard method
return super().encode(hashed, salt, iterations)
def get_salt_from_zope_sha1(data):
intermediate = base64.b64decode(data)
salt = intermediate[20:].strip()
return to_base64(salt)
ZOPE_SHA1_PREFIX = '{SSHA}'
def zope_encoded_password_to_django(encoded):
if encoded.startswith(ZOPE_SHA1_PREFIX):
data = encoded[len(ZOPE_SHA1_PREFIX):]
salt = get_salt_from_zope_sha1(data)
hasher = ZopeSHA1PasswordHasher()
return super(ZopeSHA1PasswordHasher, hasher).encode(data, salt)
else:
# assume it's a plain password and use the default hashing
return make_password(encoded)

2
sapl/legacy/management/commands/migracao_25_31.py

@ -1,3 +1,4 @@
from django.core import management
from django.core.management.base import BaseCommand
from sapl.legacy import migration
@ -17,4 +18,5 @@ class Command(BaseCommand):
)
def handle(self, *args, **options):
management.call_command('migrate')
migration.migrate(interativo=not options['force'])

4
sapl/legacy/management/commands/recria_constraints.py

@ -1,7 +1,5 @@
from django.core.management.base import BaseCommand
from sapl.legacy.migration import recria_constraints
class Command(BaseCommand):
@ -9,4 +7,4 @@ class Command(BaseCommand):
'migração de dados')
def handle(self, *args, **options):
recria_constraints()
pass

42
sapl/legacy/migracao_documentos.py

@ -5,6 +5,7 @@ import re
import magic
from sapl.base.models import CasaLegislativa
from sapl.legacy.migration import warn
from sapl.materia.models import (DocumentoAcessorio, MateriaLegislativa,
Proposicao)
from sapl.norma.models import NormaJuridica
@ -21,6 +22,7 @@ EXTENSOES = {
'application/vnd.oasis.opendocument.text': '.odt',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx', # noqa
'application/xml': '.xml',
'text/xml': '.xml',
'application/zip': '.zip',
'image/jpeg': '.jpeg',
'image/png': '.png',
@ -90,11 +92,11 @@ DOCS = {
],
}
DOCS = {tipo: [(campo,
DOCS = {model: [(campo,
os.path.join('sapl_documentos', origem),
os.path.join('sapl', destino))
for campo, origem, destino in campos]
for tipo, campos in DOCS.items()}
for model, campos in DOCS.items()}
def em_media(caminho):
@ -125,10 +127,14 @@ def migrar_docs_logo():
# a pasta props_sapl deve conter apenas o origem e metadatas!
# Edit: Aparentemente há diretório que contém properties ao invés de
# metadata. O assert foi modificado para essa situação.
assert set(os.listdir(em_media(props_sapl))) < {
sobrando = set(os.listdir(em_media(props_sapl))) - {
'logo_casa.gif', '.metadata', 'logo_casa.gif.metadata',
'.properties', 'logo_casa.gif.properties', '.objects'}
if sobrando:
warn('Os seguintes arquivos da pasta props_sapl foram ignorados: ' +
', '.join(sobrando))
mover_documento(origem, destino)
casa = get_casa_legislativa()
casa.logotipo = destino
@ -152,9 +158,9 @@ def get_extensao(caminho):
)) from e
def migrar_docs_por_ids(tipo):
for campo, base_origem, base_destino in DOCS[tipo]:
print('#### Migrando {} de {} ####'.format(campo, tipo.__name__))
def migrar_docs_por_ids(model):
for campo, base_origem, base_destino in DOCS[model]:
print('#### Migrando {} de {} ####'.format(campo, model.__name__))
dir_origem, nome_origem = os.path.split(em_media(base_origem))
pat = re.compile('^{}$'.format(nome_origem.format('(\d+)')))
@ -168,11 +174,15 @@ def migrar_docs_por_ids(tipo):
match = pat.match(arq)
if match:
# associa documento ao objeto
try:
origem = os.path.join(dir_origem, match.group(0))
id = match.group(1)
obj = tipo.objects.get(pk=id)
try:
obj = model.objects.get(pk=id)
except model.DoesNotExist:
msg = ' {} (pk={}) não encontrado para documento em [{}]'
print(msg.format(
model.__name__, id, origem))
else:
extensao = get_extensao(origem)
if hasattr(obj, "ano"):
destino = base_destino.format(id, extensao, obj.ano)
@ -185,19 +195,19 @@ def migrar_docs_por_ids(tipo):
setattr(obj, campo, destino)
obj.save()
except tipo.DoesNotExist:
msg = ' {} (pk={}) não encontrado para documento em [{}]'
print(msg.format(
tipo.__name__, id, destino))
def migrar_documentos():
# aqui supomos que uma pasta chamada sapl_documentos está em MEDIA_ROOT
# com o conteúdo da pasta de mesmo nome do zope
# Os arquivos da pasta serão movidos para a nova estrutura e a pasta será
# Os arquivos da pasta serão MOVIDOS para a nova estrutura e a pasta será
# apagada
#
# Isto significa que para rodar novamente esta função é preciso
# restaurar a pasta sapl_documentos ao estado inicial
migrar_docs_logo()
for tipo in [
for model in [
Parlamentar,
MateriaLegislativa,
DocumentoAcessorio,
@ -207,7 +217,7 @@ def migrar_documentos():
DocumentoAdministrativo,
DocumentoAcessorioAdministrativo,
]:
migrar_docs_por_ids(tipo)
migrar_docs_por_ids(model)
sobrando = [os.path.join(dir, file)
for (dir, _, files) in os.walk(em_media('sapl_documentos'))

79
sapl/legacy/migracao_usuarios.py

@ -0,0 +1,79 @@
from django.contrib.auth.models import Group, User
from sapl.settings import MEDIA_ROOT
PERFIL_LEGADO_PARA_NOVO = {legado: Group.objects.get(name=novo)
for legado, novo in [
('Autor', 'Autor'),
('Operador', 'Operador Geral'),
('Operador Comissao', 'Operador de Comissões'),
('Operador Materia', 'Operador de Matéria'),
('Operador Modulo Administrativo', 'Operador Administrativo'),
('Operador Norma', 'Operador de Norma Jurídica'),
('Operador Parlamentar', 'Parlamentar'),
('Operador Protocolo', 'Operador de Protocolo Administrativo'),
('Operador Sessao Plenaria', 'Operador de Sessão Plenária'),
('Parlamentar', 'Votante'),
]
}
ADMINISTRADORES = {'Administrador', 'Manager'}
IGNORADOS = {
# sem significado fora do zope
'Alterar Senha', 'Authenticated', 'Owner',
# obsoletos (vide docs a seguir)
'Operador Mesa Diretora',
'Operador Ordem Dia',
'Operador Tabela Auxiliar',
'Operador Lexml',
}
def migra_usuarios():
"""
o arquivo media/USERS e importa os usuários nele listados,
com senhas e perfis.
Os usuários são criados se necessário e seus perfis ajustados.
Os seguintes perfis no legado não correspondem a nenhum no código atual
e estão sendo **ignorados**:
* Operador Mesa Diretora
Apenas **8 usuários**, em todas as bases, têm esse perfil
e não têm nem "Operador" nem "Operador Sessao Plenaria"
* Operador Ordem Dia
Apenas **16 usuários**, em todas as bases, têm esse perfil
e não têm nem "Operador" nem "Operador Sessao Plenaria"
* Operador Tabela Auxiliar
A edição das tabelas auxiliares deve ser feita por um administrador
* Operador Lexml
Também podemos assumir que essa é uma tarefa de um administrador
"""
ARQUIVO_USUARIOS = MEDIA_ROOT.child('USERS')
with open(ARQUIVO_USUARIOS, 'r') as f:
usuarios = eval(f.read())
usuarios = [
(nome,
# troca senha "inicial" por uma inutilizável
senha if senha != 'inicial' else None,
# filtra perfis ignorados
{p for p in perfis if p not in IGNORADOS})
for nome, senha, perfis in usuarios]
for nome, senha, perfis in usuarios:
usuario = User.objects.get_or_create(username=nome)[0]
for perfil in perfis:
if perfil in ADMINISTRADORES:
# Manager
usuario.is_staff = True
usuario.save()
else:
usuario.groups.add(PERFIL_LEGADO_PARA_NOVO[perfil])
# apaga arquivo (importante pois contém senhas)
ARQUIVO_USUARIOS.remove()

732
sapl/legacy/migration.py

@ -1,5 +1,6 @@
import re
from datetime import date
from functools import lru_cache, partial
from subprocess import PIPE, call
import pkg_resources
@ -11,14 +12,15 @@ from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
from django.db import OperationalError, ProgrammingError, connections, models
from django.db.models import CharField, Count, Max, ProtectedError, TextField
from django.db import connections, transaction
from django.db.models import Count, Max
from django.db.models.base import ModelBase
from model_mommy import mommy
from model_mommy.mommy import foreign_key_required, make
from sapl.base.models import Argumento, Autor, Constraint, ProblemaMigracao
from sapl.base.models import AppConfig as AppConf
from sapl.base.models import (Autor, CasaLegislativa, ProblemaMigracao,
TipoAutor)
from sapl.comissoes.models import Comissao, Composicao, Participacao
from sapl.legacy.models import TipoNumeracaoProtocolo
from sapl.materia.models import (AcompanhamentoMateria, Proposicao,
StatusTramitacao, TipoDocumento,
TipoMateriaLegislativa, TipoProposicao,
@ -26,7 +28,7 @@ from sapl.materia.models import (AcompanhamentoMateria, Proposicao,
from sapl.norma.models import (AssuntoNorma, NormaJuridica, NormaRelacionada,
TipoVinculoNormaJuridica)
from sapl.parlamentares.models import (Legislatura, Mandato, Parlamentar,
TipoAfastamento)
Partido, TipoAfastamento)
from sapl.protocoloadm.models import (DocumentoAdministrativo, Protocolo,
StatusTramitacaoAdministrativo)
from sapl.sessao.models import ExpedienteMateria, OrdemDia, RegistroVotacao
@ -112,80 +114,29 @@ def warn(msg):
print('CUIDADO! ' + msg)
def erro(msg):
print('ERRO: ' + msg)
class ForeignKeyFaltando(ObjectDoesNotExist):
'Uma FK aponta para um registro inexistente'
pass
def get_fk_related(field, value, label=None):
if value is None and field.null is False:
value = 0
if value is not None:
try:
value = field.related_model.objects.get(id=value)
except ObjectDoesNotExist:
msg = 'FK [%s] não encontrada para valor %s ' \
'(em %s %s)' % (
field.name, value,
field.model.__name__, label or '---')
if value == 0:
if not field.null:
fields_dict = get_fields_dict(field.related_model)
# Cria stub ao final da tabela para evitar erros
pk = get_last_value(field.related_model)
with reversion.create_revision():
reversion.set_comment('Stub criado pela migração')
value = mommy.make(
field.related_model, **fields_dict,
pk=(pk + 1 or 1))
descricao = 'stub criado para campos não nuláveis!'
save_relation(value, [field.name], msg, descricao,
eh_stub=True)
warn(msg + ' => ' + descricao)
else:
value = None
else:
if field.model._meta.label == 'sessao.RegistroVotacao' and \
field.name == 'ordem':
return value
# Caso TipoProposicao não exista, um objeto será criado então
# com content_type=13 (ProblemaMigracao)
if field.related_model.__name__ == 'TipoProposicao':
tipo = TipoProposicao.objects.filter(descricao='Erro')
if not tipo:
with reversion.create_revision():
reversion.set_comment(
'TipoProposicao "Erro" criado')
ct = ContentType.objects.get(pk=13)
value = TipoProposicao.objects.create(
id=value, descricao='Erro', content_type=ct)
ultimo_valor = get_last_value(type(value))
alter_sequence(type(value), ultimo_valor + 1)
else:
value = tipo[0]
else:
with reversion.create_revision():
reversion.set_comment('Stub criado pela migração')
value = make_stub(field.related_model, value)
descricao = 'stub criado para entrada orfã!'
warn(msg + ' => ' + descricao)
save_relation(value, [field.name], msg, descricao,
eh_stub=True)
else:
assert value
return value
@lru_cache()
def _get_all_ids_from_model(model):
# esta função para uso apenas em get_fk_related
return set(model.objects.values_list('id', flat=True))
def get_field(model, fieldname):
return model._meta.get_field(fieldname)
def get_fk_related(field, value, label=None):
if value is None and field.null:
return None
def exec_sql_file(path, db='default'):
cursor = connections[db].cursor()
for line in open(path):
try:
cursor.execute(line)
except (OperationalError, ProgrammingError) as e:
print("Args: '%s'" % (str(e.args)))
# if field.related_model.objects.filter(id=value).exists():
if value in _get_all_ids_from_model(field.related_model):
return value
else:
msg = 'FK [%s] não encontrada para o valor %s (em %s %s)' % (
field.name, value, field.model.__name__, label or '---')
warn(msg)
raise ForeignKeyFaltando(msg)
def exec_sql(sql, db='default'):
@ -193,6 +144,123 @@ def exec_sql(sql, db='default'):
cursor.execute(sql)
return cursor
# UNIFORMIZAÇÃO DO BANCO ANTES DA MIGRAÇÃO ###############################
SQL_NAO_TEM_TABELA = '''
SELECT count(*)
FROM information_schema.columns
WHERE table_schema=database()
AND TABLE_NAME="{}"
'''
SQL_NAO_TEM_COLUNA = SQL_NAO_TEM_TABELA + ' AND COLUMN_NAME="{}"'
exec_legado = partial(exec_sql, db='legacy')
def existe_tabela_no_legado(tabela):
sql = SQL_NAO_TEM_TABELA.format(tabela)
return exec_legado(sql).fetchone()[0]
def existe_coluna_no_legado(tabela, coluna):
sql = SQL_NAO_TEM_COLUNA.format(tabela, coluna)
return exec_legado(sql).fetchone()[0] > 0
def garante_coluna_no_legado(tabela, spec_coluna):
coluna = spec_coluna.split()[0]
if not existe_coluna_no_legado(tabela, coluna):
exec_legado('ALTER TABLE {} ADD COLUMN {}'.format(tabela, spec_coluna))
assert existe_coluna_no_legado(tabela, coluna)
def garante_tabela_no_legado(create_table):
tabela = create_table.strip().splitlines()[0].split()[2]
if not existe_tabela_no_legado(tabela):
exec_legado(create_table)
assert existe_tabela_no_legado(tabela)
def uniformiza_banco():
exec_legado('''
SELECT replace(@@sql_mode,"STRICT_TRANS_TABLES,","ALLOW_INVALID_DATES");
''')
# ajusta data zero em proposicao
# isso é necessário para poder alterar a tabela a seguir
exec_legado('''
UPDATE proposicao SET dat_envio = "1800-01-01" WHERE dat_envio = 0;
alter table proposicao modify dat_envio datetime;
UPDATE proposicao SET dat_envio = NULL where dat_envio = "1800-01-01";
''')
garante_coluna_no_legado('proposicao',
'num_proposicao int(11) NULL')
garante_coluna_no_legado('tipo_materia_legislativa',
'ind_num_automatica BOOLEAN NULL DEFAULT FALSE')
garante_coluna_no_legado('tipo_materia_legislativa',
'quorum_minimo_votacao int(11) NULL')
# Cria campos cod_presenca_sessao (sendo a nova PK da tabela)
# e dat_sessao em sessao_plenaria_presenca
if not existe_coluna_no_legado('sessao_plenaria_presenca',
'cod_presenca_sessao'):
exec_legado('''
ALTER TABLE sessao_plenaria_presenca
DROP PRIMARY KEY,
ADD cod_presenca_sessao INT auto_increment PRIMARY KEY FIRST;
''')
assert existe_coluna_no_legado('sessao_plenaria_presenca',
'cod_presenca_sessao')
garante_coluna_no_legado('sessao_plenaria_presenca',
'dat_sessao DATE NULL')
garante_tabela_no_legado('''
CREATE TABLE lexml_registro_publicador (
cod_publicador INT auto_increment NOT NULL,
id_publicador INT, nom_publicador varchar(255),
adm_email varchar(50),
sigla varchar(255),
nom_responsavel varchar(255),
tipo varchar(50),
id_responsavel INT, PRIMARY KEY (cod_publicador));
''')
garante_tabela_no_legado('''
CREATE TABLE lexml_registro_provedor (
cod_provedor INT auto_increment NOT NULL,
id_provedor INT, nom_provedor varchar(255),
sgl_provedor varchar(15),
adm_email varchar(50),
nom_responsavel varchar(255),
tipo varchar(50),
id_responsavel INT, xml_provedor longtext,
PRIMARY KEY (cod_provedor));
''')
garante_tabela_no_legado('''
CREATE TABLE tipo_situacao_militar (
tip_situacao_militar INT auto_increment NOT NULL,
des_tipo_situacao varchar(50),
ind_excluido INT, PRIMARY KEY (tip_situacao_militar));
''')
update_specs = '''
vinculo_norma_juridica| ind_excluido = '' | trim(ind_excluido) = '0'
unidade_tramitacao | cod_parlamentar = NULL | cod_parlamentar = 0
parlamentar | cod_nivel_instrucao = NULL | cod_nivel_instrucao = 0
parlamentar | tip_situacao_militar = NULL | tip_situacao_militar = 0
mandato | tip_afastamento = NULL | tip_afastamento = 0
relatoria | tip_fim_relatoria = NULL | tip_fim_relatoria = 0
'''.strip().splitlines()
for spec in update_specs:
spec = spec.split('|')
exec_legado('UPDATE {} SET {} WHERE {}'.format(*spec))
def iter_sql_records(sql, db):
class Record:
@ -204,150 +272,6 @@ def iter_sql_records(sql, db):
record.__dict__.update(zip(fieldnames, row))
yield record
# Todos os models têm no máximo uma constraint unique together
# Isso é necessário para que o método delete_constraints funcione corretamente
assert all(len(model._meta.unique_together) <= 1
for app in appconfs
for model in app.models.values())
def delete_constraints(model):
# pega nome da unique constraint dado o nome da tabela
table = model._meta.db_table
cursor = exec_sql("SELECT conname FROM pg_constraint WHERE conrelid = "
"(SELECT oid FROM pg_class WHERE relname LIKE "
"'%s') and contype = 'u';" % (table))
result = ()
result = cursor.fetchall()
# se existir um resultado, unique constraint será deletado
for r in result:
if r[0].endswith('key'):
words_list = r[0].split('_')
constraint = Constraint.objects.create(
nome_tabela=table, nome_constraint=r[0],
nome_model=model.__name__, tipo_constraint='one_to_one')
for w in words_list:
Argumento.objects.create(constraint=constraint, argumento=w)
else:
if model._meta.unique_together:
args_list = model._meta.unique_together[0]
constraint = Constraint.objects.create(
nome_tabela=table, nome_constraint=r[0],
nome_model=model.__name__,
tipo_constraint='unique_together')
for a in args_list:
Argumento.objects.create(constraint=constraint,
argumento=a)
warn('Excluindo unique constraint de nome %s' % r[0])
exec_sql("ALTER TABLE %s DROP CONSTRAINT %s;" %
(table, r[0]))
def problema_duplicatas(model, lista_duplicatas, argumentos):
for obj in lista_duplicatas:
pks = []
string_pks = ""
problema = "%s de PK %s não é único." % (model.__name__, obj.pk)
args_dict = {k: obj.__dict__[k]
for k in set(argumentos) & set(obj.__dict__.keys())}
for dup in model.objects.filter(**args_dict):
pks.append(dup.pk)
string_pks = "(" + ", ".join(map(str, pks)) + ")"
descricao = "As entradas de PK %s são idênticas, mas " \
"apenas uma deve existir" % string_pks
with reversion.create_revision():
warn(problema + ' => ' + descricao)
save_relation(obj=obj, problema=problema,
descricao=descricao, eh_stub=False, critico=True)
reversion.set_comment('%s não é único.' % model.__name__)
def recria_constraints():
constraints = Constraint.objects.all()
for con in constraints:
if con.tipo_constraint == 'one_to_one':
nome_tabela = con.nome_tabela
nome_constraint = con.nome_constraint
args = [a.argumento for a in con.argumento_set.all()]
args_string = ''
args_string = "(" + "_".join(map(str, args[2:-1])) + ")"
model = ContentType.objects.filter(
model=con.nome_model.lower())[0].model_class()
try:
exec_sql("ALTER TABLE %s ADD CONSTRAINT %s UNIQUE %s;" %
(nome_tabela, nome_constraint, args_string))
except ProgrammingError:
info('A constraint %s já foi recriada!' % nome_constraint)
if con.tipo_constraint == 'unique_together':
nome_tabela = con.nome_tabela
nome_constraint = con.nome_constraint
# Pegando explicitamente o primeiro valor do filter,
# pois pode ser que haja mais de uma ocorrência
model = ContentType.objects.filter(
model=con.nome_model.lower())[0].model_class()
args = [a.argumento for a in con.argumento_set.all()]
for i in range(len(args)):
if isinstance(model._meta.get_field(args[i]),
models.ForeignKey):
args[i] = args[i] + '_id'
args_string = ''
args_string += "(" + ', '.join(map(str, args)) + ")"
distintos = model.objects.distinct(*args)
todos = model.objects.all()
if hasattr(model, "content_type"):
distintos = distintos.exclude(content_type_id=None,
object_id=None)
todos = todos.exclude(content_type_id=None, object_id=None)
lista_duplicatas = list(set(todos).difference(set(distintos)))
if lista_duplicatas:
problema_duplicatas(model, lista_duplicatas, args)
else:
try:
exec_sql("ALTER TABLE %s ADD CONSTRAINT %s UNIQUE %s;" %
(nome_tabela, nome_constraint, args_string))
except ProgrammingError:
info('A constraint %s já foi recriada!' % nome_constraint)
except Exception as err:
problema = re.findall('\(.*?\)', err.args[0])
erro('A constraint [%s] da tabela [%s] não pode ser" \
recriada' % (nome_constraint, nome_tabela))
erro('Os dados %s = %s estão duplicados. '
'Arrume antes de recriar as constraints!' %
(problema[0], problema[1]))
def obj_desnecessario(obj):
relacoes = [
f for f in obj._meta.get_fields()
if (f.one_to_many or f.one_to_one) and f.auto_created]
sem_referencia = not any(rr.related_model.objects.filter(
**{rr.field.name: obj}).exists() for rr in relacoes)
if type(obj).__name__ == 'Parlamentar' and sem_referencia and \
obj.autor.all():
sem_referencia = False
return sem_referencia
def get_last_value(model):
last_value = model.objects.all().aggregate(Max('pk'))
return last_value['pk__max'] if last_value['pk__max'] else 0
def alter_sequence(model, id):
sequence_name = '%s_id_seq' % model._meta.db_table
exec_sql('ALTER SEQUENCE %s RESTART WITH %s MINVALUE -1;' % (
sequence_name, id))
def save_with_id(new, id):
last_value = get_last_value(type(new))
alter_sequence(type(new), id)
new.save()
alter_sequence(type(new), last_value + 1)
assert new.id == id, 'New id is different from provided!'
def save_relation(obj, nome_campo='', problema='', descricao='',
eh_stub=False, critico=False):
@ -357,24 +281,6 @@ def save_relation(obj, nome_campo='', problema='', descricao='',
link.save()
def make_stub(model, id):
fields_dict = get_fields_dict(model)
new = mommy.prepare(model, **fields_dict, pk=id)
save_with_id(new, id)
return new
def get_fields_dict(model):
all_fields = model._meta.get_fields()
fields_dict = {}
fields_dict = {f.name: '????????????'[:f.max_length]
for f in all_fields
if isinstance(f, (CharField, TextField)) and
not f.choices and not f.blank}
return fields_dict
def fill_vinculo_norma_juridica():
lista = [('A', 'Altera o(a)',
'Alterado(a) pelo(a)'),
@ -408,6 +314,27 @@ def fill_vinculo_norma_juridica():
TipoVinculoNormaJuridica.objects.bulk_create(lista_objs)
def fill_dados_basicos():
# Ajusta sequencia numérica e cria base.AppConfig
letra = 'A'
try:
tipo = TipoNumeracaoProtocolo.objects.latest('dat_inicial_protocolo')
if 'POR ANO' in tipo.des_numeracao_protocolo:
letra = 'A'
elif 'POR LEGISLATURA' in tipo.des_numeracao_protocolo:
letra = 'L'
elif 'CONSECUTIVO' in tipo.des_numeracao_protocolo:
letra = 'U'
except Exception as e:
pass
appconf = AppConf(sequencia_numeracao=letra)
appconf.save()
# Cria instância de CasaLegislativa
casa = CasaLegislativa()
casa.save()
# Uma anomalia no sapl 2.5 causa a duplicação de registros de votação.
# Essa duplicação deve ser eliminada para que não haja erro no sapl 3.1
def excluir_registrovotacao_duplicados():
@ -436,6 +363,35 @@ def excluir_registrovotacao_duplicados():
assert 0
def delete_old(legacy_model, cols_values):
# ajuste necessário por conta de cósigos html em txt_expediente
if legacy_model.__name__ == 'ExpedienteSessaoPlenaria':
cols_values.pop('txt_expediente')
def eq_clause(col, value):
if value is None:
return '{} IS NULL'.format(col)
else:
return '{}="{}"'.format(col, value)
delete_sql = 'delete from {} where {}'.format(
legacy_model._meta.db_table,
' and '.join([eq_clause(col, value)
for col, value in cols_values.items()]))
exec_sql(delete_sql, 'legacy')
def get_last_pk(model):
last_value = model.objects.all().aggregate(Max('pk'))
return last_value['pk__max'] or 0
def reinicia_sequence(model, id):
sequence_name = '%s_id_seq' % model._meta.db_table
exec_sql('ALTER SEQUENCE %s RESTART WITH %s MINVALUE -1;' % (
sequence_name, id))
class DataMigrator:
def __init__(self):
@ -449,52 +405,46 @@ class DataMigrator:
for field in new._meta.fields:
old_field_name = renames.get(field.name)
field_type = field.get_internal_type()
msg = ("O valor do campo %s (%s) da model %s era inválido" %
(field.name, field_type, field.model.__name__))
if old_field_name:
old_value = getattr(old, old_field_name)
if isinstance(field, models.ForeignKey):
old_type = type(old) # not necessarily a model
if hasattr(old_type, '_meta') and \
old_type._meta.pk.name != 'id':
if field_type == 'ForeignKey':
# not necessarily a model
if hasattr(old, '_meta') and old._meta.pk.name != 'id':
label = old.pk
else:
label = '-- SEM PK --'
fk_field_name = '{}_id'.format(field.name)
value = get_fk_related(field, old_value, label)
setattr(new, fk_field_name, value)
else:
value = getattr(old, old_field_name)
# TODO rever esse DateField após as mudança para datas com
# timezone
if field_type == 'DateField' and \
not field.null and value is None:
# TODO REVER ISSO
descricao = 'A data 1111-11-11 foi colocada no lugar'
problema = 'O valor da data era nulo ou inválido'
warn(msg +
' => ' + descricao)
warn("O valor do campo %s (%s) do model %s "
"era inválido => %s" % (
field.name, field_type,
field.model.__name__, descricao))
value = date(1111, 11, 11)
self.data_mudada['obj'] = new
self.data_mudada['descricao'] = descricao
self.data_mudada['problema'] = problema
self.data_mudada.setdefault('nome_campo', []).\
append(field.name)
if field_type == 'CharField' or field_type == 'TextField':
if value is None or value == 'None':
if (field_type in ['CharField', 'TextField']
and value in [None, 'None']):
value = ''
setattr(new, field.name, value)
elif field.model.__name__ == 'TipoAutor' and \
field.name == 'content_type':
model = normalize(new.descricao.lower()).replace(' ', '')
content_types = field.related_model.objects.filter(
model=model).exclude(app_label='legacy')
assert len(content_types) <= 1
value = content_types[0] if content_types else None
setattr(new, field.name, value)
def migrate(self, obj=appconfs, interativo=True):
# warning: model/app migration order is of utmost importance
exec_sql_file(PROJECT_DIR.child(
'sapl', 'legacy', 'scripts', 'fix_tables.sql'), 'legacy')
self.to_delete = []
uniformiza_banco()
# excluindo database antigo.
if interativo:
@ -513,32 +463,13 @@ class DataMigrator:
'--database=default', '--no-input'], stdout=PIPE)
fill_vinculo_norma_juridica()
fill_dados_basicos()
info('Começando migração: %s...' % obj)
self._do_migrate(obj)
# Itera várias vezes na lista excluindo o que for possível
info('Deletando models com ind_excluido...')
while self.delete_ind_excluido():
pass
# Salva o que não pôde ser excluido da lista no problema da migração
for obj in self.to_delete:
msg = 'A entrada de PK %s da model %s não pode ser ' \
'excluida' % (obj.pk, obj._meta.model_name)
descricao = 'Um ou mais objetos protegidos'
warn(msg + ' => ' + descricao)
save_relation(obj=obj, problema=msg,
descricao=descricao, eh_stub=False)
info('Excluindo possíveis duplicações em RegistroVotacao...')
excluir_registrovotacao_duplicados()
info('Deletando stubs desnecessários...')
while self.delete_stubs():
pass
info('Recriando constraints...')
recria_constraints()
def _do_migrate(self, obj):
if isinstance(obj, AppConfig):
models_to_migrate = (model for model in obj.models.values()
@ -566,80 +497,70 @@ class DataMigrator:
legacy_model = legacy_app.get_model(legacy_model_name)
legacy_pk_name = legacy_model._meta.pk.name
delete_constraints(model)
# setup migration strategy for tables with or without a pk
if legacy_pk_name == 'id':
deve_ajustar_sequence_ao_final = False
# There is no pk in the legacy table
def save(new, old):
with reversion.create_revision():
new.save()
reversion.set_comment('Objeto criado pela migração')
# apaga registro do legado
delete_old(legacy_model, old.__dict__)
old_records = iter_sql_records(
'select * from ' + legacy_model._meta.db_table, 'legacy')
else:
deve_ajustar_sequence_ao_final = True
def save(new, old):
with reversion.create_revision():
save_with_id(new, getattr(old, legacy_pk_name))
# salva new com id de old
new.id = getattr(old, legacy_pk_name)
new.save()
reversion.set_comment('Objeto criado pela migração')
# apaga registro do legado
delete_old(legacy_model, {legacy_pk_name: new.id})
old_records = legacy_model.objects.all().order_by(legacy_pk_name)
ajuste_antes_salvar = AJUSTE_ANTES_SALVAR.get(model)
ajuste_depois_salvar = AJUSTE_DEPOIS_SALVAR.get(model)
# convert old records to new ones
with transaction.atomic():
for old in old_records:
if getattr(old, 'ind_excluido', False):
# não migramos registros marcados como excluídos
continue
new = model()
try:
self.populate_renamed_fields(new, old)
if ajuste_antes_salvar:
ajuste_antes_salvar(new, old)
except ForeignKeyFaltando:
# tentamos preencher uma FK e o ojeto relacionado
# não existe
# então este é um objeo órfão: simplesmente ignoramos
continue
else:
save(new, old)
if ajuste_depois_salvar:
ajuste_depois_salvar(new, old)
if self.data_mudada:
with reversion.create_revision():
save_relation(**self.data_mudada)
self.data_mudada.clear()
reversion.set_comment('Ajuste de data pela migração')
if getattr(old, 'ind_excluido', False):
self.to_delete.append(new)
# necessário para ajustar sequence da tabela para o ultimo valor de id
ultimo_valor = get_last_value(model)
alter_sequence(model, ultimo_valor + 1)
def delete_ind_excluido(self):
excluidos = 0
for obj in self.to_delete:
if obj_desnecessario(obj):
try:
obj.delete()
except ProtectedError:
pass
else:
self.to_delete.remove(obj)
excluidos += 1
return excluidos
def delete_stubs(self):
excluidos = 0
for obj in ProblemaMigracao.objects.all():
if obj.content_object and obj.eh_stub:
original = obj.content_type.get_all_objects_for_this_type(
id=obj.object_id)
if obj_desnecessario(original[0]):
qtd_exclusoes, *_ = original.delete()
assert qtd_exclusoes == 1
qtd_exclusoes, *_ = obj.delete()
assert qtd_exclusoes == 1
excluidos = excluidos + 1
elif not obj.content_object and not obj.eh_stub:
qtd_exclusoes, *_ = obj.delete()
assert qtd_exclusoes == 1
excluidos = excluidos + 1
return excluidos
reversion.set_comment(
'Ajuste de data pela migração')
# reinicia sequence
if deve_ajustar_sequence_ao_final:
last_pk = get_last_pk(model)
reinicia_sequence(model, last_pk + 1)
def migrate(obj=appconfs, interativo=True):
@ -654,28 +575,23 @@ def adjust_acompanhamentomateria(new, old):
def adjust_documentoadministrativo(new, old):
if new.numero_protocolo:
try:
protocolo = Protocolo.objects.get(numero=new.numero_protocolo,
ano=new.ano)
new.protocolo = protocolo
except Exception:
try:
protocolo = Protocolo.objects.get(numero=new.numero_protocolo,
ano=new.ano + 1)
new.protocolo = protocolo
except Exception:
protocolo = mommy.make(Protocolo, numero=new.numero_protocolo,
ano=new.ano)
with reversion.create_revision():
problema = 'Protocolo Vinculado [numero_protocolo=%s, '\
'ano=%s] não existe' % (new.numero_protocolo,
new.ano)
descricao = 'O protocolo inexistente foi criado'
warn(problema + ' => ' + descricao)
save_relation(obj=protocolo, problema=problema,
descricao=descricao, eh_stub=True)
reversion.set_comment('Protocolo não existia.')
if old.num_protocolo:
protocolo = Protocolo.objects.filter(
numero=old.num_protocolo, ano=new.ano)
if not protocolo:
protocolo = Protocolo.objects.filter(
numero=old.num_protocolo, ano=new.ano + 1)
print('PROTOCOLO ENCONTRADO APENAS PARA O ANO SEGUINTE!!!!! '
'DocumentoAdministrativo: {}, numero_protocolo: {}, '
'ano doc adm: {}'.format(
old.cod_documento, old.num_protocolo, new.ano))
if not protocolo:
raise ForeignKeyFaltando(
'Protocolo {} faltando '
'(referenciado no documento administrativo {}'.format(
old.num_protocolo, old.cod_documento))
assert len(protocolo) == 1
new.protocolo = protocolo[0]
def adjust_mandato(new, old):
@ -685,6 +601,9 @@ def adjust_mandato(new, old):
legislatura = Legislatura.objects.latest('data_fim')
new.data_fim_mandato = legislatura.data_fim
new.data_expedicao_diploma = legislatura.data_inicio
if not new.data_inicio_mandato:
new.data_inicio_mandato = new.legislatura.data_inicio
new.data_fim_mandato = new.legislatura.data_fim
def adjust_ordemdia_antes_salvar(new, old):
@ -707,7 +626,6 @@ def adjust_ordemdia_depois_salvar(new, old):
save_relation(obj=new, problema=problema,
descricao=descricao, eh_stub=False)
reversion.set_comment('OrdemDia sem número da ordem.')
pass
def adjust_parlamentar(new, old):
@ -723,7 +641,7 @@ def adjust_parlamentar(new, old):
def adjust_participacao(new, old):
composicao = Composicao()
composicao.comissao, composicao.periodo = [
composicao.comissao_id, composicao.periodo_id = [
get_fk_related(Composicao._meta.get_field(name), value)
for name, value in (('comissao', old.cod_comissao),
('periodo', old.cod_periodo_comp))]
@ -766,10 +684,8 @@ def adjust_normarelacionada(new, old):
def adjust_protocolo_antes_salvar(new, old):
data_ajuste = date(2014, 11, 13)
if old.num_protocolo is None and data_ajuste >= old.dat_protocolo:
new.numero = old.pk
if old.num_protocolo is None:
new.numero = old.cod_protocolo
def adjust_protocolo_depois_salvar(new, old):
@ -815,11 +731,18 @@ def adjust_tipoafastamento(new, old):
def adjust_tipoproposicao(new, old):
if old.ind_mat_ou_doc == 'M':
new.tipo_conteudo_related = TipoMateriaLegislativa.objects.get(
tipo_materia = TipoMateriaLegislativa.objects.filter(
pk=old.tip_mat_ou_doc)
if tipo_materia:
new.tipo_conteudo_related = tipo_materia[0]
else:
raise ForeignKeyFaltando
elif old.ind_mat_ou_doc == 'D':
new.tipo_conteudo_related = TipoDocumento.objects.get(
pk=old.tip_mat_ou_doc)
tipo_documento = TipoDocumento.objects.filter(pk=old.tip_mat_ou_doc)
if tipo_documento:
new.tipo_conteudo_related = tipo_documento[0]
else:
raise ForeignKeyFaltando
def adjust_statustramitacao(new, old):
@ -840,6 +763,14 @@ def adjust_tramitacao(new, old):
new.turno = 'U'
def adjust_tipo_autor(new, old):
model_apontado = normalize(new.descricao.lower()).replace(' ', '')
content_types = ContentType.objects.filter(
model=model_apontado).exclude(app_label='legacy')
assert len(content_types) <= 1
new.content_type = content_types[0] if content_types else None
def adjust_normajuridica_antes_salvar(new, old):
# Ajusta choice de esfera_federacao
# O 'S' vem de 'Selecionar'. Na versão antiga do SAPL, quando uma opção do
@ -852,43 +783,55 @@ def adjust_normajuridica_antes_salvar(new, old):
def adjust_normajuridica_depois_salvar(new, old):
# Ajusta relação M2M
lista_pks_assunto = old.cod_assunto.split(',')
# list(filter(..)) usado para retirar strings vazias da lista
for pk_assunto in list(filter(None, lista_pks_assunto)):
if not old.cod_assunto: # it can be null or empty
return
# lista de pks separadas por vírgulas (ignorando strings vazias)
lista_pks_assunto = [int(pk) for pk in old.cod_assunto.split(',') if pk]
for pk_assunto in lista_pks_assunto:
try:
new.assuntos.add(AssuntoNorma.objects.get(pk=pk_assunto))
except ObjectDoesNotExist:
pass # ignora assuntos inexistentes
def adjust_autor(new, old):
if old.cod_parlamentar:
def vincula_autor(new, old, model_relacionado, campo_relacionado, campo_nome):
pk_rel = getattr(old, campo_relacionado)
if pk_rel:
try:
new.autor_related = Parlamentar.objects.get(pk=old.cod_parlamentar)
except Exception:
with reversion.create_revision():
msg = 'Um parlamentar relacionado de PK [%s] não existia' \
% old.cod_parlamentar
reversion.set_comment('Stub criado pela migração')
value = make_stub(Parlamentar, old.cod_parlamentar)
descricao = 'stub criado para entrada orfã!'
warn(msg + ' => ' + descricao)
save_relation(value, [], msg, descricao,
eh_stub=True)
new.autor_related = value
new.nome = new.autor_related.nome_parlamentar
new.autor_related = model_relacionado.objects.get(pk=pk_rel)
except ObjectDoesNotExist:
# ignoramos o autor órfão
raise ForeignKeyFaltando('{} inexiste para autor'.format(
model_relacionado._meta.verbose_name))
else:
new.nome = getattr(new.autor_related, campo_nome)
return True
elif old.cod_comissao:
new.autor_related = Comissao.objects.get(pk=old.cod_comissao)
new.nome = new.autor_related.nome
def adjust_autor(new, old):
for args in [
# essa ordem é importante
(Parlamentar, 'cod_parlamentar', 'nome_parlamentar'),
(Comissao, 'cod_comissao', 'nome'),
(Partido, 'cod_partido', 'nome')]:
if vincula_autor(new, old, *args):
break
if old.col_username:
if not get_user_model().objects.filter(
username=old.col_username).exists():
user = get_user_model()(username=old.col_username)
user.set_password(12345)
user_model = get_user_model()
if not user_model.objects.filter(username=old.col_username).exists():
# cria um novo ususaŕio para o autor
user = user_model(username=old.col_username)
# gera uma senha inutilizável, que precisará ser trocada
user.set_password(None)
with reversion.create_revision():
user.save()
reversion.set_comment('Objeto criado pela migração')
reversion.set_comment(
'Usuário criado pela migração para o autor {}'.format(
old.cod_autor))
grupo_autor = Group.objects.get(name="Autor")
user.groups.add(grupo_autor)
@ -905,6 +848,7 @@ def adjust_comissao(new, old):
AJUSTE_ANTES_SALVAR = {
Autor: adjust_autor,
TipoAutor: adjust_tipo_autor,
AcompanhamentoMateria: adjust_acompanhamentomateria,
Comissao: adjust_comissao,
DocumentoAdministrativo: adjust_documentoadministrativo,
@ -935,31 +879,13 @@ AJUSTE_DEPOIS_SALVAR = {
# CHECKS ####################################################################
def get_ind_excluido(obj):
legacy_model = legacy_app.get_model(type(obj).__name__)
return getattr(legacy_model.objects.get(
**{legacy_model._meta.pk.name: obj.id}), 'ind_excluido', False)
def get_ind_excluido(new):
legacy_model = legacy_app.get_model(type(new).__name__)
old = legacy_model.objects.get(**{legacy_model._meta.pk.name: new.id})
return getattr(old, 'ind_excluido', False)
def check_app_no_ind_excluido(app):
for model in app.models.values():
assert not any(get_ind_excluido(obj) for obj in model.objects.all())
assert not any(get_ind_excluido(new) for new in model.objects.all())
print('OK!')
# MOMMY MAKE WITH LOG ######################################################
def make_with_log(model, _quantity=None, make_m2m=False, **attrs):
last_value = get_last_value(model)
alter_sequence(model, last_value + 1)
fields_dict = get_fields_dict(model)
stub = make(model, _quantity, make_m2m, **fields_dict)
problema = 'Um stub foi necessário durante a criação de um outro stub'
descricao = 'Essa entrada é necessária para um dos stubs criados'
' anteriormente'
warn(problema)
save_relation(obj=stub, problema=problema,
descricao=descricao, eh_stub=True)
return stub
make_with_log.required = foreign_key_required

13
sapl/legacy/models.py

@ -968,6 +968,19 @@ class TipoNormaJuridica(models.Model):
db_table = 'tipo_norma_juridica'
class TipoNumeracaoProtocolo(models.Model):
seq_tip_num_protocolo = models.AutoField(primary_key=True)
tip_numeracao_protocolo = models.IntegerField()
des_numeracao_protocolo = models.CharField(max_length=50)
dat_inicial_protocolo = models.DateTimeField()
vlr_inicial_protocolo = models.IntegerField()
ind_excluido = models.IntegerField()
class Meta:
managed = False
db_table = 'tipo_numeracao_protocolo'
class TipoProposicao(models.Model):
tip_proposicao = models.AutoField(primary_key=True)
des_tipo_proposicao = models.CharField(max_length=50)

3
sapl/legacy/scripts/exporta_zope/.gitignore

@ -0,0 +1,3 @@
Data*.fs*
sapl_documentos
XSLT

261
sapl/legacy/scripts/exporta_zope/exporta_zope.py

@ -0,0 +1,261 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# IMPORTANTE:
# Esse script precisa rodar em python 2
# e depende apenas do descrito no arquivo requiments.txt
import os.path
import sys
from collections import defaultdict
from functools import partial
from os.path import splitext
import yaml
import ZODB.DB
import ZODB.FileStorage
from ZODB.broken import Broken
EXTENSOES = {
'application/msword': '.doc',
'application/pdf': '.pdf',
'application/vnd.oasis.opendocument.text': '.odt',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx', # noqa
'application/xml': '.xml',
'text/xml': '.xml',
'application/zip': '.zip',
'image/jpeg': '.jpeg',
'image/png': '.png',
'image/gif': '.gif',
'text/html': '.html',
'text/rtf': '.rtf',
'text/x-python': '.py',
'text/plain': '.txt',
'SDE-Document': 'xml',
# TODO rever...
'text/richtext': '.rtf',
# sem extensao
'application/octet-stream': '', # binario
'inode/x-empty': '', # vazio
'text/x-unknown-content-type': '',
}
def br(obj):
if isinstance(obj, Broken):
return obj.__Broken_state__
else:
return obj
extensoes_desconhecidas = defaultdict(list)
def dump_file(doc, path):
id = doc['__name__']
name, extension = splitext(id)
content_type = doc['content_type']
extension = extension or EXTENSOES.get(content_type, 'ZZZZ')
fullname = os.path.join(path, name + extension)
print(fullname)
if extension == 'ZZZZ':
extensoes_desconhecidas[content_type].append(fullname)
# A partir daqui usamos dict.pop('...') nos __Broken_state__
# para contornar um "vazamento" de memória que ocorre
# ao percorrer a árvore de objetos
#
# Imaginamos que, internamente, o ZODB está guardando referências
# para os objetos Broken criados e não conseguimos identificar como.
#
# Essa medida descarta quase todos os dados retornados
# e só funciona na primeira passagem
pdata = br(doc.pop('data'))
if isinstance(pdata, str):
# Retrocedemos se pdata ja eh uma str (necessario em Images)
doc['data'] = pdata
pdata = doc
with open(fullname, 'w') as arq:
while pdata:
arq.write(pdata.pop('data'))
pdata = br(pdata.pop('next', None))
return id
def enumerate_by_key_list(folder, key_list, type_key):
for entry in folder.get(key_list, []):
id, meta_type = entry['id'], entry[type_key]
obj = br(folder.get(id, None))
yield id, obj, meta_type
enumerate_folder = partial(enumerate_by_key_list,
key_list='_objects', type_key='meta_type')
enumerate_properties = partial(enumerate_by_key_list,
key_list='_properties', type_key='type')
def enumerate_btree(folder):
contagem_esperada = folder['_count'].value
tree = folder['_tree']
for contagem_real, (id, obj) in enumerate(tree.iteritems(), start=1):
obj, meta_type = br(obj), type(obj).__name__
yield id, obj, meta_type
# verificação de consistência
assert contagem_esperada == contagem_real
nao_identificados = defaultdict(list)
def dump_folder(folder, path='', enum=enumerate_folder):
name = folder['id']
path = os.path.join(path, name)
if not os.path.exists(path):
os.makedirs(path)
for id, obj, meta_type in enum(folder):
dump = DUMP_FUNCTIONS.get(meta_type, '?')
if dump == '?':
nao_identificados[meta_type].append(path + '/' + id)
elif dump:
id_interno = dump(obj, path)
assert id == id_interno
return name
def decode_iso8859(obj):
return obj.decode('iso8859-1') if isinstance(obj, str) else obj
def read_sde(element):
def read_properties():
for id, obj, meta_type in enumerate_properties(element):
yield id, decode_iso8859(obj)
def read_children():
for id, obj, meta_type in enumerate_folder(element):
assert meta_type in ['SDE-Document-Element',
'SDE-Template-Element',
'SDE-Template-Link',
'SDE-Template-Attribute',
'Script (Python)',
]
if meta_type != 'Script (Python)':
# ignoramos os scrips python de eventos dos templates
yield id, read_sde(obj)
data = dict(read_properties())
children = list(read_children())
if children:
data['children'] = children
return data
def save_as_yaml(path, name, obj):
fullname = os.path.join(path, name)
with open(fullname, 'w') as arquivo:
yaml.safe_dump(obj, arquivo)
print(fullname)
return fullname
def dump_sde(strdoc, path, tipo):
id = strdoc['id']
sde = read_sde(strdoc)
save_as_yaml(path, '{}.{}.yaml'.format(id, tipo), sde)
return id
DUMP_FUNCTIONS = {
'File': dump_file,
'Image': dump_file,
'Folder': partial(dump_folder, enum=enumerate_folder),
'BTreeFolder2': partial(dump_folder, enum=enumerate_btree),
'SDE-Document': partial(dump_sde, tipo='sde.document'),
'SDE-Template': partial(dump_sde, tipo='sde.template'),
# explicitamente ignorados
'ZCatalog': None,
'Dumper': None,
}
def get_app(data_fs_path):
storage = ZODB.FileStorage.FileStorage(data_fs_path)
db = ZODB.DB(storage)
connection = db.open()
root = connection.root()
app = br(root['Application'])
def close_db():
db.close()
return app, close_db
def find_sapl(app):
for obj in app['_objects']:
id, meta_type = obj['id'], obj['meta_type']
if id.startswith('cm_') and meta_type == 'Folder':
cm_zzz = br(app[id])
sapl = br(cm_zzz.get('sapl', None))
if sapl and 'sapl_documentos' in sapl and 'acl_users' in sapl:
return sapl
def dump_propriedades(docs, path):
props_sapl = br(docs['props_sapl'])
ids = [p['id'] for p in props_sapl['_properties']]
props = {id: props_sapl[id] for id in ids}
props = {id: p.decode('iso-8859-1') if isinstance(p, str) else p
for id, p in props.items()}
save_as_yaml(path, 'sapl_documentos/propriedades.yaml', props)
def dump_usuarios(sapl, path):
users = br(br(sapl['acl_users'])['data'])
users = {k: br(v) for k, v in users['data'].items()}
save_as_yaml(path, 'usuarios.yaml', users)
def dump_sapl(data_fs_path, destino='../../../../media'):
app, close_db = get_app(data_fs_path)
try:
sapl = find_sapl(app)
# extrai folhas XSLT
dump_folder(br(sapl['XSLT']), destino)
# extrai usuários com suas senhas e perfis
dump_usuarios(sapl, destino)
# extrai documentos
docs = br(sapl['sapl_documentos'])
nao_identificados.clear()
dump_folder(docs, destino)
dump_propriedades(docs, destino)
if nao_identificados:
print('#' * 80)
print('#' * 80)
print(u'FORAM ENCONTRADOS ARQUIVOS DE FORMATO NÃO IDENTIFICADO!!!')
print(u'REFAÇA A EXPORTAÇÃO\n')
print(nao_identificados)
print('#' * 80)
print('#' * 80)
finally:
close_db()
if __name__ == "__main__":
if len(sys.argv) == 2:
data_fs_path = sys.argv[1]
dump_sapl(data_fs_path)
else:
print('Uso: python exporta_zope <caminho p Data.fs>')

3
sapl/legacy/scripts/exporta_zope/requirements.txt

@ -0,0 +1,3 @@
# ZODB version 3.7.4
PyYAML==3.12
ZODB==5.3.0

28
sapl/legacy/scripts/fix_tables.sql

@ -1,28 +0,0 @@
-- Apaga as restrições somente para essa sessão
SELECT REPLACE(@@sql_mode,'STRICT_TRANS_TABLES,','ALLOW_INVALID_DATES');
-- Exclui procedures caso já existam
DROP PROCEDURE IF EXISTS verifica_campos_proposicao;
DROP PROCEDURE IF EXISTS verifica_campos_tipo_materia_legislativa;
DROP PROCEDURE IF EXISTS verifica_campos_sessao_plenaria_presenca;
DROP PROCEDURE IF EXISTS cria_lexml_registro_provedor_e_publicador;
DROP PROCEDURE IF EXISTS cria_tipo_situacao_militar;
DROP PROCEDURE IF EXISTS muda_vinculo_norma_juridica_ind_excluido;
-- Procedure para criar campo num_proposicao em proposicao
CREATE PROCEDURE verifica_campos_proposicao() BEGIN IF NOT EXISTS (SELECT * FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='proposicao' AND column_name='num_proposicao') THEN UPDATE proposicao SET dat_envio = '1800-01-01' WHERE CAST(dat_envio AS CHAR(20)) = '0000-00-00 00:00:00'; ALTER TABLE proposicao ADD COLUMN num_proposicao INT(11) NULL after txt_justif_devolucao; END IF; END;
-- Procedure para criar campo iind_num_automatica em tipo_materia_legislativa
CREATE PROCEDURE verifica_campos_tipo_materia_legislativa() BEGIN IF NOT EXISTS (SELECT * FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='tipo_materia_legislativa' AND column_name='ind_num_automatica') THEN ALTER TABLE tipo_materia_legislativa ADD COLUMN ind_num_automatica BOOLEAN NULL DEFAULT FALSE after des_tipo_materia; END IF; IF NOT EXISTS (SELECT * FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='tipo_materia_legislativa' AND column_name='quorum_minimo_votacao') THEN ALTER TABLE tipo_materia_legislativa ADD COLUMN quorum_minimo_votacao INT(11) NULL after ind_num_automatica; END IF; END;
-- Procedure para criar campos cod_presenca_sessao (sendo a nova PK da tabela) e dat_sessao em sessao_plenaria_presenca
CREATE PROCEDURE verifica_campos_sessao_plenaria_presenca() BEGIN IF NOT EXISTS (SELECT * FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='sessao_plenaria_presenca' AND column_name='cod_presenca_sessao') THEN ALTER TABLE sessao_plenaria_presenca DROP PRIMARY KEY, ADD cod_presenca_sessao INT AUTO_INCREMENT PRIMARY KEY FIRST; END IF; IF NOT EXISTS (SELECT * FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='sessao_plenaria_presenca' AND column_name='dat_sessao') THEN ALTER TABLE sessao_plenaria_presenca ADD COLUMN dat_sessao DATE NULL after cod_parlamentar; END IF; END;
-- Procedure para criar tabela lexml_registro_provedor e lexml_registro_publicador
CREATE PROCEDURE cria_lexml_registro_provedor_e_publicador() BEGIN IF NOT EXISTS (SELECT * FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='lexml_registro_publicador') THEN CREATE TABLE lexml_registro_publicador (cod_publicador INT AUTO_INCREMENT NOT NULL, id_publicador INT, nom_publicador VARCHAR(255), adm_email VARCHAR(50), sigla VARCHAR(255), nom_responsavel VARCHAR(255), tipo VARCHAR(50), id_responsavel INT, PRIMARY KEY (cod_publicador)); END IF; IF NOT EXISTS (SELECT * FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='lexml_registro_provedor') THEN CREATE TABLE lexml_registro_provedor (cod_provedor INT AUTO_INCREMENT NOT NULL, id_provedor INT, nom_provedor VARCHAR(255), sgl_provedor VARCHAR(15), adm_email VARCHAR(50), nom_responsavel VARCHAR(255), tipo VARCHAR(50), id_responsavel INT, xml_provedor LONGTEXT, PRIMARY KEY (cod_provedor)); END IF; END;
-- Procedure para criar tabela tipo_situacao_militar
CREATE PROCEDURE cria_tipo_situacao_militar() BEGIN IF NOT EXISTS (SELECT * FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='tipo_situacao_militar') THEN CREATE TABLE tipo_situacao_militar (tip_situacao_militar INT AUTO_INCREMENT NOT NULL, des_tipo_situacao VARCHAR(50), ind_excluido INT, PRIMARY KEY (tip_situacao_militar)); END IF; END;
-- Procedure para mudar valor do campo ind_excluido da tabela vinculo_norma_juridica de 0 para string vazia ''
CREATE PROCEDURE muda_vinculo_norma_juridica_ind_excluido() BEGIN UPDATE vinculo_norma_juridica SET ind_excluido = '' WHERE trim(ind_excluido) = '0'; END;
-- Executa as procedures criadas acima
CALL verifica_campos_proposicao;
CALL verifica_campos_tipo_materia_legislativa;
CALL verifica_campos_sessao_plenaria_presenca;
CALL cria_lexml_registro_provedor_e_publicador;
CALL cria_tipo_situacao_militar;
CALL muda_vinculo_norma_juridica_ind_excluido;

7
sapl/legacy/scripts/migra_dbs.sh

@ -2,4 +2,9 @@
# rodar esse script na raiz do projeto
parallel --verbose ./sapl/legacy/scripts/migra_um_db.sh :::: <(mysql -u root -padmin -e 'show databases;' | grep '^sapl_')
if [ $# -ge 1 ]; then
parallel -eta --verbose -j+0 ./sapl/legacy/scripts/migra_um_db.sh :::: <(mysql -u $1 -p$2 -e 'show databases;' | grep '^sapl_') ::: $1 ::: $2
else
echo "USO:"
echo " $0 <usuário mysql> [senha mysql]"
fi;

73
sapl/legacy/scripts/migra_um_db.sh

@ -1,31 +1,48 @@
#!/bin/bash
# rodar esse script na raiz do projeto
DIR=~/logs_migracao
mkdir -p $DIR
LOG="$DIR/$1.migracao.log"
rm -f $LOG
echo "########################################" | tee -a $LOG
echo "MIGRANDO BANCO $1" | tee -a $LOG
echo "########################################" | tee -a $LOG
echo >> $LOG
echo "--- DJANGO MIGRATE ---" | tee -a $LOG
echo >> $LOG
DATABASE_NAME=$1 ./manage.py migrate --settings sapl.legacy_migration_settings
echo >> $LOG
echo "--- MIGRACAO DE DADOS ---" | tee -a $LOG
echo >> $LOG
DATABASE_NAME=$1 ./manage.py migracao_25_31 -f --settings sapl.legacy_migration_settings |& tee -a $LOG
echo >> $LOG
echo "--- RECRIANDO CONSTRAINTS ---" | tee -a $LOG
echo >> $LOG
DATABASE_NAME=$1 ./manage.py recria_constraints --settings sapl.legacy_migration_settings |& tee -a $LOG
echo >> $LOG
if [ $# -ge 2 ]; then
# proteje pasta com dumps de alterações acidentais
chmod -R -w ~/sapl_dumps
DATE=$(date +%Y-%m-%d)
DIR=~/${DATE}_logs_migracao
mkdir -p $DIR
LOG="$DIR/$1.migracao.log"
rm -f $LOG
echo "########################################" | tee -a $LOG
echo "MIGRANDO BANCO $1" | tee -a $LOG
echo "########################################" | tee -a $LOG
echo >> $LOG
if [ $3 ]; then
# se há senha do mysql
mysql -u $2 -p "$3" -N -s -e "DROP DATABASE IF EXISTS $1; CREATE DATABASE $1;"
mysql -u $2 -p "$3" < ~/sapl_dumps/$1.sql
else
# se não há senha do mysql
mysql -u $2 -N -s -e "DROP DATABASE IF EXISTS $1; CREATE DATABASE $1;"
mysql -u $2 < ~/sapl_dumps/$1.sql
fi;
echo "O banco legado foi restaurado" |& tee -a $LOG
echo >> $LOG
echo "--- DJANGO MIGRATE ---" | tee -a $LOG
echo >> $LOG
DATABASE_NAME=$1 ./manage.py migrate --settings sapl.legacy_migration_settings
echo >> $LOG
# XXX Na primeira execução desse comando aparece o erro de "Coammands out of sync"
# A solução mais rápida foi executar duas vezes seguidas pra poder migrar.
DATABASE_NAME=$1 ./manage.py migracao_25_31 -f --settings sapl.legacy_migration_settings
echo "--- MIGRACAO DE DADOS ---" | tee -a $LOG
echo >> $LOG
DATABASE_NAME=$1 ./manage.py migracao_25_31 -f --settings sapl.legacy_migration_settings |& tee -a $LOG
echo >> $LOG
else
echo "USO:"
echo " $0 <nome_database> <usuário mysql> [senha mysql]"
fi;

10
sapl/legacy/scripts/recria_dbs_postgres.sh

@ -1,5 +1,11 @@
#!/bin/bash
# (Re)cria todos os bancos postgres para migração
# cria um banco postgres (de mesmo nome) para cada banco mysql cujo nome começa com "sapl_"
mysql -u root -padmin -e 'show databases;' | grep '^sapl_' | xargs -I{} ./recria_um_db_postgres.sh {}
if [ $# -eq 2 ]; then
parallel --verbose -j+0 ./recria_um_db_postgres.sh :::: <(mysql -u $1 -p$2 -e 'show databases;' | grep '^sapl_' | grep -v '_copy$')
else
echo "USO:"
echo " $0 [usuário mysql] [senha mysql]"
fi;

1
sapl/legacy/scripts/recria_um_db_postgres.sh

@ -1,5 +1,6 @@
# (Re)cria um db postgres
# uso: recria_um_db_postgres <NOME DO BANCO>
echo "Database $1"
sudo -u postgres psql -c "drop DATABASE if exists $1"
sudo -u postgres psql -c "CREATE DATABASE $1 WITH OWNER = sapl ENCODING = 'UTF8' TABLESPACE = pg_default LC_COLLATE = 'pt_BR.UTF-8' LC_CTYPE = 'pt_BR.UTF-8' CONNECTION LIMIT = -1 TEMPLATE template0;"

150
sapl/legacy/scripts/street_sweeper.py

@ -1,150 +0,0 @@
#!/usr/bin/python
# requisito: pip install PyMySQL
import pymysql.cursors
HOST = 'localhost'
USER = 'root'
PASSWORD = ''
DB = ''
SELECT_EXCLUIDOS = "SELECT %s FROM %s WHERE ind_excluido = 1 ORDER BY %s"
REGISTROS_INCONSISTENTES = "DELETE FROM %s WHERE %s "
"in (%s) AND ind_excluido = 0 "
EXCLUI_REGISTRO = "DELETE FROM %s WHERE ind_excluido=1"
NORMA_DEP = "DELETE FROM vinculo_norma_juridica WHERE cod_norma_referente in (%s) OR \
cod_norma_referida in (%s) AND ind_excluido = 0 "
mapa = {} # mapa com tabela principal -> tabelas dependentes
mapa['tipo_autor'] = ['autor']
mapa['materia_legislativa'] = ['acomp_materia', 'autoria', 'despacho_inicial',
'documento_acessorio', 'expediente_materia',
'legislacao_citada', 'materia_assunto',
'numeracao', 'ordem_dia', 'parecer',
'proposicao', 'registro_votacao',
'relatoria', 'tramitacao']
mapa['norma_juridica'] = ['vinculo_norma_juridica']
mapa['comissao'] = ['composicao_comissao']
mapa['sessao_legislativa'] = ['composicao_mesa']
mapa['tipo_expediente'] = ['expediente_sessao_plenaria']
"""
mapa['autor'] = ['tipo_autor', 'partido', 'comissao', 'parlamentar']
mapa['parlamentar'] = ['autor', 'autoria', 'composicao_comissao',
'composicao_mesa', 'dependente', 'filiacao',
'mandato', 'mesa_sessao_plenaria', 'oradores',
'oradores_expediente', 'ordem_dia_presenca',
'registro_votacao_parlamentar', 'relatoria',
'sessao_plenaria_presenca', 'unidade_tramitacao']
"""
def get_ids_excluidos(cursor, query):
"""
recupera as PKs de registros com ind_excluido = 1 da tabela principal
"""
cursor.execute(query)
excluidos = cursor.fetchall()
# flat tuple of tuples with map transformation into string
excluidos = [str(val) for sublist in excluidos for val in sublist]
return excluidos
def remove_tabelas(cursor, tabela_principal, pk, query_dependentes=None):
QUERY = SELECT_EXCLUIDOS % (pk, tabela_principal, pk)
ids_excluidos = get_ids_excluidos(cursor, QUERY)
print("\nRegistros da tabela '%s' com ind_excluido = 1: %s" %
(tabela_principal.upper(), len(ids_excluidos)))
"""
Remove registros de tabelas que dependem da tabela principal,
e que se encontram com ind_excluido = 0 (nao excluidas), se
tais registros existirem.
"""
if ids_excluidos:
print("Dependencias inconsistentes")
for tabela in mapa[tabela_principal]:
QUERY_DEP = REGISTROS_INCONSISTENTES % (
tabela, pk, ','.join(ids_excluidos))
# Trata caso especifico de norma_juridica
if query_dependentes:
QUERY_DEP = query_dependentes % (','.join(ids_excluidos),
','.join(ids_excluidos))
print(tabela.upper(), cursor.execute(QUERY_DEP))
"""
Remove todos os registros com ind_excluido = 1 das tabelas
dependentes e da tabela principal, nesta ordem.
"""
print("\n\nRegistros com ind_excluido = 1")
for tabela in mapa[tabela_principal] + [tabela_principal]:
QUERY = EXCLUI_REGISTRO % tabela
print(tabela.upper(), cursor.execute(QUERY))
def remove_excluidas(cursor):
cursor.execute("SHOW_TABLES")
for row in cursor.fetchall():
print(row)
def remove_proposicao_invalida(cursor):
return cursor.execute(
"DELETE FROM proposicao WHERE cod_mat_ou_doc is null")
def remove_materia_assunto_invalida(cursor):
return cursor.execute(
"DELETE FROM materia_assunto WHERE cod_assunto = 0")
def shotgun_remove(cursor):
for tabela in get_ids_excluidos(cursor, "SHOW TABLES"):
try:
cursor.execute("DELETE FROM %s WHERE ind_excluido = 1" % tabela)
except:
pass
if __name__ == '__main__':
connection = pymysql.connect(host=HOST,
user=USER,
password=PASSWORD,
db=DB)
cursor = connection.cursor()
# TIPO AUTOR
remove_tabelas(cursor, 'tipo_autor', 'tip_autor')
# MATERIA LEGISLATIVA
remove_tabelas(cursor, 'materia_legislativa', 'cod_materia')
# NORMA JURIDICA
remove_tabelas(cursor, 'norma_juridica', 'cod_norma', NORMA_DEP)
# COMISSAO
remove_tabelas(cursor, 'comissao', 'cod_comissao')
# SESSAO LEGISLATIVA
remove_tabelas(cursor, 'sessao_legislativa', 'cod_sessao_leg')
# EXPEDIENTE SESSAO
remove_tabelas(cursor, 'tipo_expediente', 'cod_expediente')
# AUTOR
remove_tabelas(cursor, 'autor', 'cod_autor')
# PARLAMENTAR
remove_tabelas(cursor, 'parlamentar', 'cod_parlamentar')
# PROPOSICAO
remove_proposicao_invalida(cursor)
# MATERIA_ASSUNTO
remove_materia_assunto_invalida(cursor)
# shotgun_remove(cursor)
cursor.close()

15
sapl/legacy_migration_settings.py

@ -12,14 +12,20 @@ INSTALLED_APPS += (
'sapl.legacy', # legacy reversed model definitions
)
DATABASES['legacy'] = config('DATABASE_URL', cast=db_url,)
DATABASES['legacy'] = config('DATABASE_URL_FONTE', cast=db_url,)
DATABASES['default'] = config(
'DATABASE_URL_DESTINO',
cast=lambda v: v if isinstance(v, dict) else db_url(v),
default=DATABASES['default'])
# Sobrescreve o nome dos bancos caso a variável de ambiente seja definida
# Útil para migração em lote de vários bancos
DATABASE_NAME_OVERRIDE = os.environ.get('DATABASE_NAME')
if DATABASE_NAME_OVERRIDE:
for db in DATABASES.values():
db['NAME'] = DATABASE_NAME_OVERRIDE
DATABASES['legacy']['NAME'] = DATABASE_NAME_OVERRIDE
# não altera o nome se o destino é um banco em memória
if not DATABASES['default']['NAME'] == ':memory:':
DATABASES['default']['NAME'] = DATABASE_NAME_OVERRIDE
DATABASE_ROUTERS = ['sapl.legacy.router.LegacyRouter', ]
@ -28,3 +34,6 @@ DEBUG = True
MOMMY_CUSTOM_FIELDS_GEN = {
'django.db.models.ForeignKey': 'sapl.legacy.migration.make_with_log'
}
# delisga indexação fulltext em tempo real
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.BaseSignalProcessor'

3
sapl/materia/apps.py

@ -6,3 +6,6 @@ class AppConfig(apps.AppConfig):
name = 'sapl.materia'
label = 'materia'
verbose_name = _('Matéria')
def ready(self):
from . import receivers

6
sapl/materia/email_utils.py

@ -1,8 +1,9 @@
from datetime import datetime
from datetime import datetime as dt
from django.core.mail import EmailMultiAlternatives, get_connection, send_mail
from django.core.urlresolvers import reverse
from django.template import Context, loader
from django.utils import timezone
from sapl.base.models import CasaLegislativa
from sapl.settings import EMAIL_SEND_USER
@ -150,7 +151,8 @@ def criar_email_tramitacao(base_url, casa_legislativa, materia, status,
templates = load_email_templates(['email/tramitacao.txt',
'email/tramitacao.html'],
{"casa_legislativa": casa_nome,
"data_registro": datetime.now().strftime(
"data_registro": dt.strftime(
timezone.now(),
"%d/%m/%Y"),
"cod_materia": materia.id,
"logotipo": casa_legislativa.logotipo,

310
sapl/materia/forms.py

@ -1,8 +1,6 @@
import os
from datetime import date, datetime
import django_filters
from crispy_forms.bootstrap import Alert, FormActions, InlineRadios
from crispy_forms.helper import FormHelper
from crispy_forms.layout import (HTML, Button, Column, Div, Field, Fieldset,
@ -18,12 +16,13 @@ from django.forms import ModelChoiceField, ModelForm, widgets
from django.forms.forms import Form
from django.forms.models import ModelMultipleChoiceField
from django.forms.widgets import CheckboxSelectMultiple, HiddenInput, Select
from django.utils import timezone
from django.utils.encoding import force_text
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
import django_filters
import sapl
from sapl.base.models import Autor, TipoAutor
from sapl.comissoes.models import Comissao
from sapl.compilacao.models import (STATUS_TA_IMMUTABLE_PUBLIC,
@ -42,6 +41,7 @@ from sapl.utils import (RANGE_ANOS, YES_NO_CHOICES,
MateriaPesquisaOrderingFilter, RangeWidgetOverride,
autor_label, autor_modal, models_with_gr_for_model,
qs_override_django_filter)
import sapl
from .models import (AcompanhamentoMateria, Anexada, Autoria, DespachoInicial,
DocumentoAcessorio, Numeracao, Proposicao, Relatoria,
@ -54,8 +54,8 @@ def ANO_CHOICES():
def em_tramitacao():
return [('', 'Tanto Faz'),
(1, 'Sim'),
(0, 'Não')]
(True, 'Sim'),
(False, 'Não')]
class AdicionarVariasAutoriasFilterSet(django_filters.FilterSet):
@ -73,7 +73,7 @@ class AdicionarVariasAutoriasFilterSet(django_filters.FilterSet):
self.form.helper.form_method = 'GET'
self.form.helper.layout = Layout(
Fieldset(_('Filtrar Autores'),
row1, form_actions(save_label='Filtrar'))
row1, form_actions(label='Filtrar'))
)
@ -86,7 +86,7 @@ class ReceberProposicaoForm(Form):
self.helper.layout = Layout(
Fieldset(
_('Incorporar Proposição'), row1,
form_actions(save_label='Buscar Proposição')
form_actions(label='Buscar Proposição')
)
)
super(ReceberProposicaoForm, self).__init__(*args, **kwargs)
@ -113,7 +113,7 @@ class MateriaSimplificadaForm(ModelForm):
Fieldset(
_('Formulário Simplificado'),
row1, row2, row3, row4, row5,
form_actions(save_label='Salvar')
form_actions(label='Salvar')
)
)
super(MateriaSimplificadaForm, self).__init__(*args, **kwargs)
@ -151,7 +151,7 @@ class AcompanhamentoMateriaForm(ModelForm):
row1 = to_row([('email', 10)])
row1.append(
Column(form_actions(save_label='Cadastrar'), css_class='col-md-2')
Column(form_actions(label='Cadastrar'), css_class='col-md-2')
)
self.helper = FormHelper()
@ -215,24 +215,26 @@ class TramitacaoForm(ModelForm):
def __init__(self, *args, **kwargs):
super(TramitacaoForm, self).__init__(*args, **kwargs)
self.fields['data_tramitacao'].initial = datetime.now()
self.fields['data_tramitacao'].initial = timezone.now().date()
def clean(self):
super(TramitacaoForm, self).clean()
cleaned_data = super(TramitacaoForm, self).clean()
if 'data_encaminhamento' in self.data:
data_enc_form = self.cleaned_data['data_encaminhamento']
if 'data_fim_prazo' in self.data:
data_prazo_form = self.cleaned_data['data_fim_prazo']
if 'data_tramitacao' in self.data:
data_tram_form = self.cleaned_data['data_tramitacao']
if 'data_encaminhamento' in cleaned_data:
data_enc_form = cleaned_data['data_encaminhamento']
if 'data_fim_prazo' in cleaned_data:
data_prazo_form = cleaned_data['data_fim_prazo']
if 'data_tramitacao' in cleaned_data:
data_tram_form = cleaned_data['data_tramitacao']
if self.errors:
return self.errors
ultima_tramitacao = Tramitacao.objects.filter(
materia_id=self.instance.materia_id).exclude(
id=self.instance.id).last()
id=self.instance.id).order_by(
'-data_tramitacao',
'-id').first()
if not self.instance.data_tramitacao:
@ -243,7 +245,7 @@ class TramitacaoForm(ModelForm):
'destino da última adicionada!')
raise ValidationError(msg)
if self.cleaned_data['data_tramitacao'] > datetime.now().date():
if cleaned_data['data_tramitacao'] > timezone.now().date():
msg = _(
'A data de tramitação deve ser ' +
'menor ou igual a data de hoje!')
@ -267,7 +269,7 @@ class TramitacaoForm(ModelForm):
'maior que a data de tramitação!')
raise ValidationError(msg)
return self.cleaned_data
return cleaned_data
class TramitacaoUpdateForm(TramitacaoForm):
@ -296,13 +298,26 @@ class TramitacaoUpdateForm(TramitacaoForm):
}
def clean(self):
super(TramitacaoUpdateForm, self).clean()
ultima_tramitacao = Tramitacao.objects.filter(
materia_id=self.instance.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:
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')
local = self.instance.unidade_tramitacao_local
data_tram = self.instance.data_tramitacao
self.cleaned_data['data_tramitacao'] = \
self.instance.data_tramitacao
self.cleaned_data['unidade_tramitacao_local'] = \
self.instance.unidade_tramitacao_local
self.cleaned_data['data_tramitacao'] = data_tram
self.cleaned_data['unidade_tramitacao_local'] = local
return super(TramitacaoUpdateForm, self).clean()
@ -587,7 +602,7 @@ class MateriaLegislativaFilterSet(django_filters.FilterSet):
HTML(autor_label),
HTML(autor_modal),
row4, row5, row6, row7, row8, row9, row10,
form_actions(save_label='Pesquisar'))
form_actions(label='Pesquisar'))
)
@property
@ -674,7 +689,7 @@ class AutoriaForm(ModelForm):
self.helper = FormHelper()
self.helper.layout = Layout(
Fieldset(_('Autoria'),
row1, 'data_relativa', form_actions(save_label='Salvar')))
row1, 'data_relativa', form_actions(label='Salvar')))
if not kwargs['instance']:
self.fields['autor'].choices = []
@ -732,7 +747,7 @@ class AutoriaMultiCreateForm(Form):
self.helper.layout = Layout(
Fieldset(
_('Autorias'), row1, row2, 'data_relativa', 'autores',
form_actions(save_label='Incluir Autores Selecionados')))
form_actions(label='Incluir Autores Selecionados')))
self.fields['autor'].choices = []
@ -777,7 +792,7 @@ class AcessorioEmLoteFilterSet(django_filters.FilterSet):
self.form.helper.form_method = 'GET'
self.form.helper.layout = Layout(
Fieldset(_('Documentos Acessórios em Lote'),
row1, row2, form_actions(save_label='Pesquisar')))
row1, row2, form_actions(label='Pesquisar')))
class PrimeiraTramitacaoEmLoteFilterSet(django_filters.FilterSet):
@ -809,7 +824,7 @@ class PrimeiraTramitacaoEmLoteFilterSet(django_filters.FilterSet):
self.form.helper.form_method = 'GET'
self.form.helper.layout = Layout(
Fieldset(_('Primeira Tramitação'),
row1, row2, form_actions(save_label='Pesquisar')))
row1, row2, form_actions(label='Pesquisar')))
class TramitacaoEmLoteFilterSet(django_filters.FilterSet):
@ -850,7 +865,7 @@ class TramitacaoEmLoteFilterSet(django_filters.FilterSet):
self.form.helper.form_method = 'GET'
self.form.helper.layout = Layout(
Fieldset(_('Tramitação em Lote'),
row1, row2, form_actions(save_label='Pesquisar')))
row1, row2, form_actions(label='Pesquisar')))
class TipoProposicaoForm(ModelForm):
@ -924,6 +939,12 @@ class TipoProposicaoForm(ModelForm):
_('O Registro definido (%s) não está na base de %s.'
) % (cd['tipo_conteudo_related'], content_type))
"""
A unicidade de tipo proposição para tipo de conteudo
foi desabilitada pois existem casos em quem é o procedimento da
instituição convergir vários tipos de proposição
para um tipo de matéria.
unique_value = self._meta.model.objects.filter(
content_type=content_type, object_id=cd['tipo_conteudo_related'])
@ -938,7 +959,7 @@ class TipoProposicaoForm(ModelForm):
'que foi defindo como (%s) para (%s)'
) % (unique_value,
content_type,
unique_value.tipo_conteudo_related))
unique_value.tipo_conteudo_related))"""
return super().clean()
@ -1149,10 +1170,10 @@ class ProposicaoForm(forms.ModelForm):
return super().save(commit)
inst.ano = datetime.now().year
inst.ano = timezone.now().year
numero__max = Proposicao.objects.filter(
autor=inst.autor,
ano=datetime.now().year).aggregate(Max('numero_proposicao'))
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
@ -1162,6 +1183,84 @@ class ProposicaoForm(forms.ModelForm):
return inst
class DevolverProposicaoForm(ProposicaoForm):
justificativa_devolucao = forms.CharField(
required=False, widget=widgets.Textarea(attrs={'rows': 5}))
class Meta:
model = Proposicao
fields = [
'justificativa_devolucao',
]
def __init__(self, *args, **kwargs):
# esta chamada isola o __init__ de ProposicaoForm
super(ProposicaoForm, self).__init__(*args, **kwargs)
fields = []
fields.append(
Fieldset(
_('Registro de Devolução'),
to_column(('justificativa_devolucao', 12)),
to_column(
(form_actions(label=_('Devolver'),
name='devolver',
css_class='btn-danger pull-right'), 12)
)
)
)
self.helper = FormHelper()
self.helper.layout = Layout(*fields)
def clean(self):
super(DevolverProposicaoForm, self).clean()
numeracao = sapl.base.models.AppConfig.attr('sequencia_numeracao')
if not numeracao:
raise ValidationError("A sequência de numeração (por ano ou geral)"
" não foi configurada para a aplicação em "
"tabelas auxiliares")
cd = ProposicaoForm.clean(self)
cd = self.cleaned_data
if 'justificativa_devolucao' not in cd or\
not cd['justificativa_devolucao']:
# TODO Implementar notificação ao autor por email
raise ValidationError(
_('Adicione uma Justificativa para devolução.'))
return cd
@transaction.atomic
def save(self, commit=False):
# TODO Implementar workflow entre protocolo e autores
cd = self.cleaned_data
self.instance.data_devolucao = timezone.now()
self.instance.data_recebimento = None
self.instance.data_envio = None
self.instance.save()
if self.instance.texto_articulado.exists():
ta = self.instance.texto_articulado.first()
ta.privacidade = STATUS_TA_PRIVATE
ta.editing_locked = False
ta.save()
self.instance.results = {
'messages': {
'success': [_('Devolução efetuada com sucesso.'), ]
},
'url': reverse('sapl.materia:receber-proposicao')
}
return self.instance
class ConfirmarProposicaoForm(ProposicaoForm):
tipo_readonly = forms.CharField(
@ -1174,9 +1273,6 @@ class ConfirmarProposicaoForm(ProposicaoForm):
required=False, widget=widgets.TextInput(
attrs={'readonly': 'readonly'}))
justificativa_devolucao = forms.CharField(
required=False, widget=widgets.Textarea(attrs={'rows': 5}))
regime_tramitacao = forms.ModelChoiceField(
required=False, queryset=RegimeTramitacao.objects.all())
@ -1195,7 +1291,6 @@ class ConfirmarProposicaoForm(ProposicaoForm):
fields = [
'data_envio',
'descricao',
'justificativa_devolucao',
'gerar_protocolo',
'numero_de_paginas'
]
@ -1282,20 +1377,16 @@ class ConfirmarProposicaoForm(ProposicaoForm):
if self.proposicao_incorporacao_obrigatoria != 'N':
itens_incorporacao.append(to_column(('numero_de_paginas', 4)))
itens_incorporacao.append(to_column((FormActions(Submit(
'incorporar', _('Incorporar'), css_class='pull-right')), 12)))
itens_incorporacao.append(
to_column(
(form_actions(label=_('Incorporar'),
name='incorporar'), 12)
)
)
fields.append(
Fieldset(_('Registro de Incorporação'), *itens_incorporacao))
fields.append(
Fieldset(
_('Registro de Devolução'),
to_column(('justificativa_devolucao', 12)),
to_column((FormActions(Submit(
'devolver', _('Devolver'),
css_class='btn-danger pull-right')), 12))
))
self.helper = FormHelper()
self.helper.layout = Layout(*fields)
@ -1325,7 +1416,7 @@ class ConfirmarProposicaoForm(ProposicaoForm):
raise ValidationError("A sequência de numeração (por ano ou geral)"
" não foi configurada para a aplicação em "
"tabelas auxiliares")
if 'incorporar' in self.data:
cd = ProposicaoForm.clean(self)
if self.instance.tipo.content_type.model_class() ==\
@ -1342,17 +1433,6 @@ class ConfirmarProposicaoForm(ProposicaoForm):
_('Documentos não podem ser incorporados sem definir '
'para qual Matéria Legislativa ele se destina.'))
elif 'devolver' in self.data:
cd = self.cleaned_data
if 'justificativa_devolucao' not in cd or\
not cd['justificativa_devolucao']:
# TODO Implementar notificação ao autor por email
raise ValidationError(
_('Adicione uma Justificativa para devolução.'))
else:
raise ValidationError(
_('Dados de Confirmação invalidos.'))
return cd
@transaction.atomic
@ -1360,30 +1440,9 @@ class ConfirmarProposicaoForm(ProposicaoForm):
# TODO Implementar workflow entre protocolo e autores
cd = self.cleaned_data
if 'devolver' in self.data:
self.instance.data_devolucao = datetime.now()
self.instance.data_recebimento = None
self.instance.data_envio = None
self.instance.save()
if self.instance.texto_articulado.exists():
ta = self.instance.texto_articulado.first()
ta.privacidade = STATUS_TA_PRIVATE
ta.editing_locked = False
ta.save()
self.instance.results = {
'messages': {
'success': [_('Devolução efetuada com sucesso.'), ]
},
'url': reverse('sapl.materia:receber-proposicao')
}
return self.instance
elif 'incorporar' in self.data:
self.instance.justificativa_devolucao = ''
self.instance.data_devolucao = None
self.instance.data_recebimento = datetime.now()
self.instance.data_recebimento = timezone.now()
self.instance.materia_de_vinculo = cd['materia_de_vinculo']
if self.instance.texto_articulado.exists():
@ -1420,7 +1479,7 @@ class ConfirmarProposicaoForm(ProposicaoForm):
) == TipoMateriaLegislativa:
numero__max = MateriaLegislativa.objects.filter(
tipo=proposicao.tipo.tipo_conteudo_related,
ano=datetime.now().year).aggregate(Max('numero'))
ano=timezone.now().year).aggregate(Max('numero'))
numero__max = numero__max['numero__max']
# dados básicos
@ -1428,8 +1487,8 @@ class ConfirmarProposicaoForm(ProposicaoForm):
materia.numero = (numero__max + 1) if numero__max else 1
materia.tipo = proposicao.tipo.tipo_conteudo_related
materia.ementa = proposicao.descricao
materia.ano = datetime.now().year
materia.data_apresentacao = datetime.now()
materia.ano = timezone.now().year
materia.data_apresentacao = timezone.now()
materia.em_tramitacao = True
materia.regime_tramitacao = cd['regime_tramitacao']
@ -1468,7 +1527,7 @@ class ConfirmarProposicaoForm(ProposicaoForm):
anexada = Anexada()
anexada.materia_principal = proposicao.materia_de_vinculo
anexada.materia_anexada = materia
anexada.data_anexacao = datetime.now()
anexada.data_anexacao = timezone.now()
anexada.save()
self.instance.results['messages']['success'].append(_(
@ -1540,18 +1599,18 @@ class ConfirmarProposicaoForm(ProposicaoForm):
numeracao = sapl.base.models.AppConfig.attr('sequencia_numeracao')
if numeracao == 'A':
nm = Protocolo.objects.filter(
ano=date.today().year).aggregate(Max('numero'))
ano=timezone.now().year).aggregate(Max('numero'))
elif numeracao == 'U':
nm = Protocolo.objects.all().aggregate(Max('numero'))
protocolo = Protocolo()
protocolo.numero = (nm['numero__max'] + 1) if nm['numero__max'] else 1
protocolo.ano = date.today().year
protocolo.data = date.today()
protocolo.hora = datetime.now().time()
protocolo.ano = timezone.now().year
protocolo.data = timezone.now()
protocolo.hora = timezone.now().time()
# TODO transformar campo timestamp em auto_now_add
protocolo.timestamp = datetime.now()
protocolo.timestamp = timezone.now()
protocolo.tipo_protocolo = '1'
protocolo.interessado = str(proposicao.autor)
@ -1641,7 +1700,7 @@ class EtiquetaPesquisaForm(forms.Form):
Fieldset(
('Formulário de Etiqueta'),
row1, row2,
form_actions(save_label='Pesquisar')
form_actions(label='Pesquisar')
)
)
@ -1679,3 +1738,70 @@ class EtiquetaPesquisaForm(forms.Form):
return cleaned_data
class FichaPesquisaForm(forms.Form):
tipo_materia = forms.ModelChoiceField(
label=TipoMateriaLegislativa._meta.verbose_name,
queryset=TipoMateriaLegislativa.objects.all(),
empty_label='Selecione')
data_inicial = forms.DateField(
label='Data Inicial',
widget=forms.DateInput(format='%d/%m/%Y')
)
data_final = forms.DateField(
label='Data Final',
widget=forms.DateInput(format='%d/%m/%Y')
)
def __init__(self, *args, **kwargs):
super(FichaPesquisaForm, self).__init__(*args, **kwargs)
row1 = to_row(
[('tipo_materia', 6),
('data_inicial', 3),
('data_final', 3)])
self.helper = FormHelper()
self.helper.layout = Layout(
Fieldset(
('Formulário de Ficha'),
row1,
form_actions(label='Pesquisar')
)
)
def clean(self):
cleaned_data = super(FichaPesquisaForm, self).clean()
if not self.is_valid():
return cleaned_data
if cleaned_data['data_final'] < cleaned_data['data_inicial']:
raise ValidationError(_(
'A Data Final não pode ser menor que a Data Inicial'))
return cleaned_data
class FichaSelecionaForm(forms.Form):
materia = forms.ModelChoiceField(
widget=forms.RadioSelect,
queryset=MateriaLegislativa.objects.all(),
label='')
def __init__(self, *args, **kwargs):
super(FichaSelecionaForm, self).__init__(*args, **kwargs)
row1 = to_row(
[('materia', 12)])
self.helper = FormHelper()
self.helper.layout = Layout(
Fieldset(
('Selecione a ficha que deseja imprimir'),
row1,
form_actions(label='Gerar Impresso')
)
)

26
sapl/materia/migrations/0012_auto_20170815_1238.py

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.12 on 2017-08-15 12:38
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('materia', '0011_auto_20170808_1034'),
]
operations = [
migrations.AlterField(
model_name='proposicao',
name='tipo',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='materia.TipoProposicao', verbose_name='Tipo'),
),
migrations.AlterField(
model_name='tramitacao',
name='status',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='materia.StatusTramitacao', verbose_name='Status'),
),
]

21
sapl/materia/migrations/0013_auto_20170816_1136.py

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.12 on 2017-08-16 11:36
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('materia', '0012_auto_20170815_1238'),
]
operations = [
migrations.AlterField(
model_name='tramitacao',
name='unidade_tramitacao_destino',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='tramitacoes_destino', to='materia.UnidadeTramitacao', verbose_name='Unidade Destino'),
),
]

19
sapl/materia/migrations/0014_auto_20170905_0818.py

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2017-09-05 08:18
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('materia', '0013_adiciona_status_tramitacao'),
]
operations = [
migrations.AlterModelOptions(
name='unidadetramitacao',
options={'ordering': ['orgao', 'comissao', 'parlamentar'], 'verbose_name': 'Unidade de Tramitação', 'verbose_name_plural': 'Unidades de Tramitação'},
),
]

37
sapl/materia/migrations/0015_auto_20170908_1024.py

@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.13 on 2017-09-08 10:24
from __future__ import unicode_literals
from django.db import migrations
from sapl.materia.models import TipoProposicao
class AlterUniqueTogetherFixConstraintInexistente(
migrations.AlterUniqueTogether):
def database_forwards(self,
app_label, schema_editor, from_state, to_state):
constraint_names = schema_editor._constraint_names(
TipoProposicao, ['content_type_id', 'object_id'], unique=True)
if constraint_names:
# por alguma razão a constraint não existe em alguns bancos
# se ela existir continua a exetução normal
super(AlterUniqueTogetherFixConstraintInexistente,
self).database_forwards(
app_label, schema_editor, from_state, to_state
)
class Migration(migrations.Migration):
dependencies = [
('materia', '0014_auto_20170905_0818'),
]
operations = [
AlterUniqueTogetherFixConstraintInexistente(
name='tipoproposicao',
unique_together=set([]),
),
]

16
sapl/materia/migrations/0016_merge.py

@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.13 on 2017-09-08 11:57
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('materia', '0015_auto_20170908_1024'),
('materia', '0013_auto_20170816_1136'),
]
operations = [
]

20
sapl/materia/migrations/0017_auto_20170918_1257.py

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.13 on 2017-09-18 12:57
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('materia', '0016_merge'),
]
operations = [
migrations.AlterField(
model_name='proposicao',
name='data_envio',
field=models.DateTimeField(null=True, verbose_name='Data de Envio'),
),
]

101
sapl/materia/migrations/0018_auto_20171113_1339.py

@ -0,0 +1,101 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.11 on 2017-11-13 15:39
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('materia', '0017_auto_20170918_1257'),
]
operations = [
migrations.AlterField(
model_name='anexada',
name='materia_anexada',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='materia_anexada_set', to='materia.MateriaLegislativa'),
),
migrations.AlterField(
model_name='anexada',
name='materia_principal',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='materia_principal_set', to='materia.MateriaLegislativa'),
),
migrations.AlterField(
model_name='autoria',
name='autor',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='base.Autor', verbose_name='Autor'),
),
migrations.AlterField(
model_name='autoria',
name='materia',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='materia.MateriaLegislativa', verbose_name='Matéria Legislativa'),
),
migrations.AlterField(
model_name='despachoinicial',
name='comissao',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='comissoes.Comissao'),
),
migrations.AlterField(
model_name='despachoinicial',
name='materia',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='materia.MateriaLegislativa'),
),
migrations.AlterField(
model_name='documentoacessorio',
name='materia',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='materia.MateriaLegislativa'),
),
migrations.AlterField(
model_name='materiaassunto',
name='assunto',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='materia.AssuntoMateria', verbose_name='Assunto'),
),
migrations.AlterField(
model_name='materiaassunto',
name='materia',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='materia.MateriaLegislativa', verbose_name='Matéria'),
),
migrations.AlterField(
model_name='numeracao',
name='materia',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='materia.MateriaLegislativa'),
),
migrations.AlterField(
model_name='parecer',
name='materia',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='materia.MateriaLegislativa'),
),
migrations.AlterField(
model_name='parecer',
name='relatoria',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='materia.Relatoria'),
),
migrations.AlterField(
model_name='proposicao',
name='materia_de_vinculo',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='proposicao_set', to='materia.MateriaLegislativa', verbose_name='Matéria anexadora'),
),
migrations.AlterField(
model_name='relatoria',
name='comissao',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comissoes.Comissao', verbose_name='Comissão'),
),
migrations.AlterField(
model_name='relatoria',
name='materia',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='materia.MateriaLegislativa'),
),
migrations.AlterField(
model_name='relatoria',
name='parlamentar',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='parlamentares.Parlamentar', verbose_name='Parlamentar'),
),
migrations.AlterField(
model_name='tramitacao',
name='materia',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='materia.MateriaLegislativa'),
),
]

26
sapl/materia/migrations/0019_auto_20171127_1500.py

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.13 on 2017-11-27 17:00
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('materia', '0018_auto_20171113_1339'),
]
operations = [
migrations.AlterField(
model_name='anexada',
name='materia_anexada',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='materia_anexada_set', to='materia.MateriaLegislativa', verbose_name='Matéria Anexada'),
),
migrations.AlterField(
model_name='anexada',
name='materia_principal',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='materia_principal_set', to='materia.MateriaLegislativa', verbose_name='Matéria Principal'),
),
]

27
sapl/materia/migrations/0020_auto_20171204_1658.py

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.11 on 2017-12-04 18:58
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('materia', '0019_auto_20171127_1500'),
]
operations = [
migrations.AlterModelOptions(
name='orgao',
options={'ordering': ['nome'], 'verbose_name': 'Órgão', 'verbose_name_plural': 'Órgãos'},
),
migrations.AlterModelOptions(
name='tipodocumento',
options={'ordering': ['descricao'], 'verbose_name': 'Tipo de Documento', 'verbose_name_plural': 'Tipos de Documento'},
),
migrations.AlterModelOptions(
name='unidadetramitacao',
options={'verbose_name': 'Unidade de Tramitação', 'verbose_name_plural': 'Unidades de Tramitação'},
),
]

80
sapl/materia/models.py

@ -1,4 +1,3 @@
from datetime import datetime
import reversion
from django.contrib.auth.models import Group
@ -6,7 +5,8 @@ from django.contrib.contenttypes.fields import GenericRelation
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
from django.db import models
from django.utils import formats
from django.db.models.functions import Concat
from django.utils import formats, timezone
from django.utils.translation import ugettext_lazy as _
from model_utils import Choices
@ -19,8 +19,8 @@ from sapl.utils import (RANGE_ANOS, YES_NO_CHOICES, SaplGenericForeignKey,
SaplGenericRelation, restringe_tipos_de_arquivo_txt,
texto_upload_path)
EM_TRAMITACAO = [(1, 'Sim'),
(0, 'Não')]
EM_TRAMITACAO = [(True, 'Sim'),
(False, 'Não')]
def grupo_autor():
@ -66,7 +66,6 @@ class TipoProposicao(models.Model):
class Meta:
verbose_name = _('Tipo de Proposição')
verbose_name_plural = _('Tipos de Proposições')
unique_together = (('content_type', 'object_id'), )
def __str__(self):
return self.descricao
@ -289,9 +288,9 @@ class MateriaLegislativa(models.Model):
class Autoria(models.Model):
autor = models.ForeignKey(Autor,
verbose_name=_('Autor'),
on_delete=models.PROTECT)
on_delete=models.CASCADE)
materia = models.ForeignKey(
MateriaLegislativa, on_delete=models.PROTECT,
MateriaLegislativa, on_delete=models.CASCADE,
verbose_name=_('Matéria Legislativa'))
primeiro_autor = models.BooleanField(verbose_name=_('Primeiro Autor'),
choices=YES_NO_CHOICES,
@ -304,14 +303,14 @@ class Autoria(models.Model):
ordering = ('-primeiro_autor', 'autor__nome')
def __str__(self):
return _('%(autor)s - %(materia)s') % {
return _('Autoria: %(autor)s - %(materia)s') % {
'autor': self.autor, 'materia': self.materia}
@reversion.register()
class AcompanhamentoMateria(models.Model):
usuario = models.CharField(max_length=50)
materia = models.ForeignKey(MateriaLegislativa)
materia = models.ForeignKey(MateriaLegislativa, on_delete=models.CASCADE)
email = models.EmailField(
max_length=100, verbose_name=_('E-mail'))
data_cadastro = models.DateField(auto_now_add=True)
@ -332,10 +331,12 @@ class AcompanhamentoMateria(models.Model):
class Anexada(models.Model):
materia_principal = models.ForeignKey(
MateriaLegislativa, related_name='materia_principal_set',
on_delete=models.PROTECT)
on_delete=models.CASCADE,
verbose_name=_('Matéria Principal'))
materia_anexada = models.ForeignKey(
MateriaLegislativa, related_name='materia_anexada_set',
on_delete=models.PROTECT)
on_delete=models.CASCADE,
verbose_name=_('Matéria Anexada'))
data_anexacao = models.DateField(verbose_name=_('Data Anexação'))
data_desanexacao = models.DateField(
blank=True, null=True, verbose_name=_('Data Desanexação'))
@ -374,8 +375,8 @@ class DespachoInicial(models.Model):
# TODO M2M?
# TODO Despachos não são necessáriamente comissoes, podem ser outros
# órgãos, ex: procuradorias
materia = models.ForeignKey(MateriaLegislativa, on_delete=models.PROTECT)
comissao = models.ForeignKey(Comissao, on_delete=models.PROTECT)
materia = models.ForeignKey(MateriaLegislativa, on_delete=models.CASCADE)
comissao = models.ForeignKey(Comissao, on_delete=models.CASCADE)
class Meta:
verbose_name = _('Despacho Inicial')
@ -402,6 +403,7 @@ class TipoDocumento(models.Model):
class Meta:
verbose_name = _('Tipo de Documento')
verbose_name_plural = _('Tipos de Documento')
ordering = ['descricao']
def __str__(self):
return self.descricao
@ -409,7 +411,7 @@ class TipoDocumento(models.Model):
@reversion.register()
class DocumentoAcessorio(models.Model):
materia = models.ForeignKey(MateriaLegislativa, on_delete=models.PROTECT)
materia = models.ForeignKey(MateriaLegislativa, on_delete=models.CASCADE)
tipo = models.ForeignKey(TipoDocumento,
on_delete=models.PROTECT,
verbose_name=_('Tipo'))
@ -479,11 +481,11 @@ class MateriaAssunto(models.Model):
# TODO M2M ??
assunto = models.ForeignKey(
AssuntoMateria,
on_delete=models.PROTECT,
on_delete=models.CASCADE,
verbose_name=_('Assunto'))
materia = models.ForeignKey(
MateriaLegislativa,
on_delete=models.PROTECT,
on_delete=models.CASCADE,
verbose_name=_('Matéria'))
class Meta:
@ -497,7 +499,7 @@ class MateriaAssunto(models.Model):
@reversion.register()
class Numeracao(models.Model):
materia = models.ForeignKey(MateriaLegislativa, on_delete=models.PROTECT)
materia = models.ForeignKey(MateriaLegislativa, on_delete=models.CASCADE)
tipo_materia = models.ForeignKey(
TipoMateriaLegislativa,
on_delete=models.PROTECT,
@ -545,6 +547,7 @@ class Orgao(models.Model):
class Meta:
verbose_name = _('Órgão')
verbose_name_plural = _('Órgãos')
ordering = ['nome']
def __str__(self):
return _(
@ -566,9 +569,9 @@ class TipoFimRelatoria(models.Model):
@reversion.register()
class Relatoria(models.Model):
materia = models.ForeignKey(MateriaLegislativa, on_delete=models.PROTECT)
materia = models.ForeignKey(MateriaLegislativa, on_delete=models.CASCADE)
parlamentar = models.ForeignKey(Parlamentar,
on_delete=models.PROTECT,
on_delete=models.CASCADE,
verbose_name=_('Parlamentar'))
tipo_fim_relatoria = models.ForeignKey(
TipoFimRelatoria,
@ -578,7 +581,7 @@ class Relatoria(models.Model):
verbose_name=_('Motivo Fim Relatoria'))
comissao = models.ForeignKey(
Comissao, blank=True, null=True,
on_delete=models.PROTECT, verbose_name=_('Comissão'))
on_delete=models.CASCADE, verbose_name=_('Comissão'))
data_designacao_relator = models.DateField(
verbose_name=_('Data Designação'))
data_destituicao_relator = models.DateField(
@ -597,8 +600,8 @@ class Relatoria(models.Model):
@reversion.register()
class Parecer(models.Model):
relatoria = models.ForeignKey(Relatoria, on_delete=models.PROTECT)
materia = models.ForeignKey(MateriaLegislativa, on_delete=models.PROTECT)
relatoria = models.ForeignKey(Relatoria, on_delete=models.CASCADE)
materia = models.ForeignKey(MateriaLegislativa, on_delete=models.CASCADE)
tipo_conclusao = models.CharField(max_length=3, blank=True)
tipo_apresentacao = models.CharField(
max_length=1, choices=TIPO_APRESENTACAO_CHOICES)
@ -621,11 +624,13 @@ class Proposicao(models.Model):
blank=True,
on_delete=models.PROTECT)
tipo = models.ForeignKey(TipoProposicao, on_delete=models.PROTECT,
blank=False,
null=True,
verbose_name=_('Tipo'))
# XXX data_envio was not null, but actual data said otherwise!!!
data_envio = models.DateTimeField(
blank=True, null=True, verbose_name=_('Data de Envio'))
blank=False, null=True, verbose_name=_('Data de Envio'))
data_recebimento = models.DateTimeField(
blank=True, null=True, verbose_name=_('Data de Recebimento'))
data_devolucao = models.DateTimeField(
@ -680,7 +685,7 @@ class Proposicao(models.Model):
# retire o comentário quando resolver
materia_de_vinculo = models.ForeignKey(
MateriaLegislativa, blank=True, null=True,
on_delete=models.PROTECT,
on_delete=models.CASCADE,
verbose_name=_('Matéria anexadora'),
related_name=_('proposicao_set'))
@ -710,7 +715,7 @@ class Proposicao(models.Model):
def title_type(self):
return '%s nº _____ %s' % (
self.tipo, formats.date_format(
self.data_envio if self.data_envio else datetime.now(),
self.data_envio if self.data_envio else timezone.now(),
"\d\e d \d\e F \d\e Y"))
class Meta:
@ -788,6 +793,19 @@ class StatusTramitacao(models.Model):
'descricao': self.descricao}
class UnidadeTramitacaoManager(models.Manager):
"""
Esta classe permite ordenar alfabeticamente a unidade de tramitacao
através da concatenação de 3 fields
"""
def get_queryset(self):
return super(UnidadeTramitacaoManager, self).get_queryset().annotate(
nome_composto=Concat('orgao__nome',
'comissao__sigla',
'parlamentar__nome_parlamentar')
).order_by('nome_composto')
@reversion.register()
class UnidadeTramitacao(models.Model):
comissao = models.ForeignKey(
@ -800,6 +818,8 @@ class UnidadeTramitacao(models.Model):
Parlamentar, blank=True, null=True,
on_delete=models.PROTECT, verbose_name=_('Parlamentar'))
objects = UnidadeTramitacaoManager()
class Meta:
verbose_name = _('Unidade de Tramitação')
verbose_name_plural = _('Unidades de Tramitação')
@ -844,8 +864,12 @@ class Tramitacao(models.Model):
)
status = models.ForeignKey(StatusTramitacao, on_delete=models.PROTECT,
# TODO PÓS MIGRACAO INICIAL (vide #1381)
# não nulo quando todas as
# bases tiverem sido corrigidas
null=True,
verbose_name=_('Status'))
materia = models.ForeignKey(MateriaLegislativa, on_delete=models.PROTECT)
materia = models.ForeignKey(MateriaLegislativa, on_delete=models.CASCADE)
data_tramitacao = models.DateField(verbose_name=_('Data Tramitação'))
unidade_tramitacao_local = models.ForeignKey(
UnidadeTramitacao,
@ -856,6 +880,10 @@ class Tramitacao(models.Model):
blank=True, null=True, verbose_name=_('Data Encaminhamento'))
unidade_tramitacao_destino = models.ForeignKey(
UnidadeTramitacao,
# TODO PÓS MIGRACAO INICIAL (vide #1381)
# não nulo quando todas as
# bases tiverem sido corrigidas
null=True,
related_name='tramitacoes_destino',
on_delete=models.PROTECT,
verbose_name=_('Unidade Destino'))

10
sapl/materia/receivers.py

@ -1,5 +1,7 @@
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver
from sapl.materia.models import Tramitacao
from sapl.materia.signals import tramitacao_signal
from sapl.utils import get_base_url
@ -17,3 +19,11 @@ def handle_tramitacao_signal(sender, **kwargs):
materia,
tramitacao.status,
tramitacao.unidade_tramitacao_destino)
@receiver(post_delete, sender=Tramitacao)
def status_tramitacao_materia(sender, instance, **kwargs):
if instance.status.indicador == 'F':
materia = instance.materia
materia.em_tramitacao = True
materia.save()

3
sapl/materia/tests/test_materia.py

@ -1,9 +1,9 @@
import pytest
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.urlresolvers import reverse
from model_mommy import mommy
import pytest
from sapl.base.models import Autor, TipoAutor
from sapl.comissoes.models import Comissao, TipoComissao
@ -18,6 +18,7 @@ from sapl.norma.models import (LegislacaoCitada, NormaJuridica,
from sapl.utils import models_with_gr_for_model
@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)

68
sapl/materia/tests/test_materia_form.py

@ -0,0 +1,68 @@
import pytest
from django.utils.translation import ugettext as _
from model_mommy import mommy
from sapl.materia import forms
from sapl.materia.models import MateriaLegislativa, TipoMateriaLegislativa
@pytest.mark.django_db(transaction=False)
def test_valida_campos_obrigatorios_ficha_pesquisa_form():
form = forms.FichaPesquisaForm(data={})
assert not form.is_valid()
errors = form.errors
assert errors['tipo_materia'] == [_('Este campo é obrigatório.')]
assert errors['data_inicial'] == [_('Este campo é obrigatório.')]
assert errors['data_final'] == [_('Este campo é obrigatório.')]
assert len(errors) == 3
@pytest.mark.django_db(transaction=False)
def test_ficha_pesquisa_form_datas_invalidas():
tipo = mommy.make(TipoMateriaLegislativa)
form = forms.FichaPesquisaForm(data={'tipo_materia': str(tipo.pk),
'data_inicial': '10/11/2017',
'data_final': '09/11/2017'
})
assert not form.is_valid()
assert form.errors['__all__'] == [_('A Data Final não pode ser menor que '
'a Data Inicial')]
@pytest.mark.django_db(transaction=False)
def test_ficha_pesquisa_form_invalido():
tipo = mommy.make(TipoMateriaLegislativa)
form = forms.FichaPesquisaForm(data={'tipo_materia': str(tipo.pk),
'data_inicial': '10/11/2017',
'data_final': '09/11/2017'
})
assert not form.is_valid()
@pytest.mark.django_db(transaction=False)
def test_valida_campos_obrigatorios_ficha_seleciona_form():
form = forms.FichaSelecionaForm(data={})
assert not form.is_valid()
errors = form.errors
assert errors['materia'] == [_('Este campo é obrigatório.')]
assert len(errors) == 1
@pytest.mark.django_db(transaction=False)
def test_ficha_seleciona_form_valido():
materia = mommy.make(MateriaLegislativa)
form = forms.FichaSelecionaForm(data={'materia': str(materia.pk)})
assert form.is_valid()

14
sapl/materia/urls.py

@ -8,7 +8,8 @@ from sapl.materia.views import (AcompanhamentoConfirmarView,
CriarProtocoloMateriaView, DespachoInicialCrud,
DocumentoAcessorioCrud,
DocumentoAcessorioEmLoteView,
ImpressosView, EtiquetaPesquisaView,
EtiquetaPesquisaView, FichaPesquisaView,
FichaSelecionaView, ImpressosView,
LegislacaoCitadaCrud, MateriaAssuntoCrud,
MateriaLegislativaCrud,
MateriaLegislativaPesquisaView, MateriaTaView,
@ -24,6 +25,8 @@ from sapl.materia.views import (AcompanhamentoConfirmarView,
TramitacaoEmLoteView, UnidadeTramitacaoCrud,
proposicao_texto, recuperar_materia)
from sapl.norma.views import NormaPesquisaSimplesView
from .apps import AppConfig
app_name = AppConfig.name
@ -35,6 +38,15 @@ urlpatterns_impressos = [
url(r'^materia/impressos/etiqueta-pesquisa/$',
EtiquetaPesquisaView.as_view(),
name='impressos_etiqueta'),
url(r'^materia/impressos/ficha-pesquisa/$',
FichaPesquisaView.as_view(),
name='impressos_ficha_pesquisa'),
url(r'^materia/impressos/ficha-seleciona/$',
FichaSelecionaView.as_view(),
name='impressos_ficha_seleciona'),
url(r'^materia/impressos/norma-pesquisa/$',
NormaPesquisaSimplesView.as_view(),
name='impressos_norma_pesquisa'),
]
urlpatterns_materia = [

304
sapl/materia/views.py

@ -12,15 +12,15 @@ from django.core.urlresolvers import reverse
from django.http import HttpResponse, JsonResponse
from django.http.response import Http404, HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect
from django.template import Context, loader, RequestContext
from django.utils import formats
from django.template import RequestContext, loader
from django.utils import formats, timezone
from django.utils.translation import ugettext_lazy as _
from django.views.generic import CreateView, ListView, TemplateView, UpdateView
from django.views.generic.base import RedirectView
from django.views.generic.edit import FormView
from django_filters.views import FilterView
import weasyprint
import sapl
from sapl.base.models import Autor, CasaLegislativa
from sapl.comissoes.models import Comissao, Participacao
from sapl.compilacao.models import (STATUS_TA_IMMUTABLE_RESTRICT,
@ -35,18 +35,20 @@ from sapl.materia.forms import (AnexadaForm, AutoriaForm,
AutoriaMultiCreateForm,
ConfirmarProposicaoForm, LegislacaoCitadaForm,
ProposicaoForm, TipoProposicaoForm,
TramitacaoForm, TramitacaoUpdateForm)
TramitacaoForm, TramitacaoUpdateForm,
DevolverProposicaoForm)
from sapl.norma.models import LegislacaoCitada
from sapl.protocoloadm.models import Protocolo
from sapl.utils import (TURNO_TRAMITACAO_CHOICES, YES_NO_CHOICES, autor_label,
autor_modal, gerar_hash_arquivo, get_base_url,
montar_row_autor)
montar_row_autor, show_results_filter_set, get_mime_type_from_file_extension)
import sapl
from .email_utils import do_envia_email_confirmacao
from .forms import (AcessorioEmLoteFilterSet, AcompanhamentoMateriaForm,
AdicionarVariasAutoriasFilterSet, DespachoInicialForm,
DocumentoAcessorioForm, EtiquetaPesquisaForm,
MateriaAssuntoForm,
FichaPesquisaForm, FichaSelecionaForm, MateriaAssuntoForm,
MateriaLegislativaFilterSet, MateriaSimplificadaForm,
PrimeiraTramitacaoEmLoteFilterSet, ReceberProposicaoForm,
RelatoriaForm, TramitacaoEmLoteFilterSet,
@ -61,7 +63,6 @@ from .models import (AcompanhamentoMateria, Anexada, AssuntoMateria, Autoria,
TipoProposicao, Tramitacao, UnidadeTramitacao)
from .signals import tramitacao_signal
import weasyprint
AssuntoMateriaCrud = Crud.build(AssuntoMateria, 'assunto_materia')
@ -81,9 +82,9 @@ TipoFimRelatoriaCrud = CrudAux.build(
def autores_ja_adicionados(materia_pk):
autorias = Autoria.objects.filter(materia_id=materia_pk)
pks = [a.autor.pk for a in autorias]
return pks
autorias = Autoria.objects.filter(materia_id=materia_pk).values_list(
'autor_id', flat=True)
return autorias
def proposicao_texto(request, pk):
@ -96,12 +97,7 @@ def proposicao_texto(request, pk):
arquivo = proposicao.texto_original
ext = arquivo.name.split('.')[-1]
mime = ''
if ext == 'odt':
mime = 'application/vnd.oasis.opendocument.text'
else:
mime = "application/%s" % (ext,)
mime = get_mime_type_from_file_extension(arquivo.name)
with open(arquivo.path, 'rb') as f:
data = f.read()
@ -138,6 +134,9 @@ class AdicionarVariasAutorias(PermissionRequiredForAppCrudMixin, FilterView):
context['title'] = _('Pesquisar Autores')
qr = self.request.GET.copy()
context['filter_url'] = ('&' + qr.urlencode()) if len(qr) > 0 else ''
context['show_results'] = show_results_filter_set(qr)
context['pk_materia'] = self.kwargs['pk']
return context
@ -173,14 +172,14 @@ class CriarProtocoloMateriaView(CreateView):
except ObjectDoesNotExist:
raise Http404()
numero = 1
try:
materias_ano = MateriaLegislativa.objects.filter(
ano=protocolo.ano,
tipo=protocolo.tipo_materia).order_by('-numero')
if materias_ano:
numero = materias_ano.first().numero + 1
else:
numero = 1
tipo=protocolo.tipo_materia).latest('numero')
numero = materias_ano.numero + 1
except ObjectDoesNotExist:
pass # numero ficou com o valor padrão 1 acima
context['form'].fields['tipo'].initial = protocolo.tipo_materia
context['form'].fields['numero'].initial = numero
@ -282,7 +281,7 @@ def recuperar_materia(request):
ano = request.GET.get('ano', '')
param = {'tipo': tipo}
param['data_apresentacao__year'] = ano if ano else datetime.now().year
param['data_apresentacao__year'] = ano if ano else timezone.now().year
materia = MateriaLegislativa.objects.filter(**param).order_by(
'tipo', 'ano', 'numero').values_list('numero', 'ano').last()
@ -291,7 +290,7 @@ def recuperar_materia(request):
'ano': materia[1]})
else:
response = JsonResponse(
{'numero': 1, 'ano': ano if ano else datetime.now().year})
{'numero': 1, 'ano': ano if ano else timezone.now().year})
return response
@ -321,14 +320,14 @@ def criar_materia_proposicao(proposicao):
tipo_materia = TipoMateriaLegislativa.objects.get(
descricao=proposicao.tipo.descricao)
numero = MateriaLegislativa.objects.filter(
ano=datetime.now().year).order_by('numero').last().numero + 1
ano=timezone.now().year).order_by('numero').last().numero + 1
regime = RegimeTramitacao.objects.get(descricao='Normal')
return MateriaLegislativa.objects.create(
tipo=tipo_materia,
ano=datetime.now().year,
ano=timezone.now().year,
numero=numero,
data_apresentacao=datetime.now(),
data_apresentacao=timezone.now(),
regime_tramitacao=regime,
em_tramitacao=True,
ementa=proposicao.descricao,
@ -473,7 +472,7 @@ class ConfirmarProposicao(PermissionRequiredForAppCrudMixin, UpdateView):
app_label = sapl.protocoloadm.apps.AppConfig.label
template_name = "materia/confirmar_proposicao.html"
model = Proposicao
form_class = ConfirmarProposicaoForm
form_class = ConfirmarProposicaoForm, DevolverProposicaoForm
def get_success_url(self):
msgs = self.object.results['messages']
@ -486,7 +485,8 @@ class ConfirmarProposicao(PermissionRequiredForAppCrudMixin, UpdateView):
def get_object(self, queryset=None):
try:
"""Não deve haver acesso na rotina de confirmação a proposições:
"""
Não deve haver acesso na rotina de confirmação a proposições:
recebidas -> data_recebimento != None
não enviadas -> data_envio == None
"""
@ -494,11 +494,9 @@ class ConfirmarProposicao(PermissionRequiredForAppCrudMixin, UpdateView):
data_envio__isnull=False,
data_recebimento__isnull=True)
self.object = None
# FIXME implementar hash para texto eletrônico
if proposicao.texto_articulado.exists():
ta = proposicao.texto_articulado.first()
# FIXME hash para textos articulados
hasher = 'P' + ta.hash() + '/' + str(proposicao.id)
else:
hasher = gerar_hash_arquivo(
@ -520,10 +518,25 @@ class ConfirmarProposicao(PermissionRequiredForAppCrudMixin, UpdateView):
context['subnav_template_name'] = ''
return context
def get_form(self, form_class=None):
if form_class is None:
form_class = self.get_form_class()
if self.request.POST:
if 'justificativa_devolucao' in self.request.POST:
return form_class[1](**self.get_form_kwargs())
else:
return form_class[0](**self.get_form_kwargs())
else:
forms = []
for form in form_class:
forms.append(form(**self.get_form_kwargs()))
return forms
class UnidadeTramitacaoCrud(CrudAux):
model = UnidadeTramitacao
help_path = 'unidade_tramitacao'
help_topic = 'unidade_tramitacao'
class BaseMixin(Crud.BaseMixin):
list_field_names = ['comissao', 'orgao', 'parlamentar']
@ -549,7 +562,7 @@ class UnidadeTramitacaoCrud(CrudAux):
class ProposicaoCrud(Crud):
model = Proposicao
help_path = ''
help_topic = 'proposicao'
container_field = 'autor__user'
class BaseMixin(Crud.BaseMixin):
@ -614,7 +627,7 @@ class ProposicaoCrud(Crud):
'Texto associado.')
else:
p.data_devolucao = None
p.data_envio = datetime.now()
p.data_envio = timezone.now()
p.save()
if p.texto_articulado.exists():
@ -836,7 +849,7 @@ class ReciboProposicaoView(TemplateView):
class RelatoriaCrud(MasterDetailCrud):
model = Relatoria
parent_field = 'materia'
help_path = ''
help_topic = 'tramitacao_relatoria'
public = [RP_LIST, RP_DETAIL]
class CreateView(MasterDetailCrud.CreateView):
@ -898,11 +911,9 @@ class RelatoriaCrud(MasterDetailCrud):
participacao = Participacao.objects.filter(
composicao=composicao)
parlamentares = []
for p in participacao:
if p.titular:
parlamentares.append(
[p.parlamentar.id, p.parlamentar.nome_parlamentar])
parlamentares = [[p.parlamentar.id, p.parlamentar.nome_parlamentar] for
p in participacao if p.titular]
context['form'].fields['parlamentar'].choices = parlamentares
return context
@ -911,7 +922,7 @@ class RelatoriaCrud(MasterDetailCrud):
class TramitacaoCrud(MasterDetailCrud):
model = Tramitacao
parent_field = 'materia'
help_path = ''
help_topic = 'tramitacao_relatoria'
public = [RP_LIST, RP_DETAIL]
class BaseMixin(MasterDetailCrud.BaseMixin):
@ -930,27 +941,30 @@ class TramitacaoCrud(MasterDetailCrud):
def get_initial(self):
local = MateriaLegislativa.objects.get(
pk=self.kwargs['pk']).tramitacao_set.order_by(
'-data_tramitacao').first()
'-data_tramitacao',
'-id').first()
if local:
self.initial['unidade_tramitacao_local'
] = local.unidade_tramitacao_destino.pk
else:
self.initial['unidade_tramitacao_local'] = ''
self.initial['data_tramitacao'] = datetime.now()
self.initial['data_tramitacao'] = timezone.now().date()
return self.initial
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
primeira_tramitacao = not(Tramitacao.objects.filter(
materia_id=int(kwargs['root_pk'])).exists())
ultima_tramitacao = Tramitacao.objects.filter(
materia_id=self.kwargs['pk']).order_by(
'-data_tramitacao',
'-id').first()
# Se não for a primeira tramitação daquela matéria, o campo
# não pode ser modificado
if not primeira_tramitacao:
if ultima_tramitacao:
context['form'].fields[
'unidade_tramitacao_local'].widget.attrs['disabled'] = True
'unidade_tramitacao_local'].choices = [
(ultima_tramitacao.unidade_tramitacao_destino.pk,
ultima_tramitacao.unidade_tramitacao_destino)]
return context
def form_valid(self, form):
@ -1009,7 +1023,8 @@ class TramitacaoCrud(MasterDetailCrud):
def get_queryset(self):
qs = super(MasterDetailCrud.ListView, self).get_queryset()
kwargs = {self.crud.parent_field: self.kwargs['pk']}
return qs.filter(**kwargs).order_by('-data_tramitacao', '-id')
return qs.filter(**kwargs).order_by('-data_tramitacao',
'-id')
class DeleteView(MasterDetailCrud.DeleteView):
@ -1019,7 +1034,11 @@ class TramitacaoCrud(MasterDetailCrud):
url = reverse('sapl.materia:tramitacao_list',
kwargs={'pk': tramitacao.materia.id})
if tramitacao.pk != materia.tramitacao_set.last().pk:
ultima_tramitacao = materia.tramitacao_set.order_by(
'-data_tramitacao',
'-id').first()
if tramitacao.pk != ultima_tramitacao.pk:
msg = _('Somente a última tramitação pode ser deletada!')
messages.add_message(request, messages.ERROR, msg)
return HttpResponseRedirect(url)
@ -1050,7 +1069,7 @@ def montar_helper_documento_acessorio(self):
class DocumentoAcessorioCrud(MasterDetailCrud):
model = DocumentoAcessorio
parent_field = 'materia'
help_path = ''
help_topic = 'despacho_autoria'
public = [RP_LIST, RP_DETAIL]
class BaseMixin(MasterDetailCrud.BaseMixin):
@ -1063,7 +1082,7 @@ class DocumentoAcessorioCrud(MasterDetailCrud):
super(MasterDetailCrud.CreateView, self).__init__(**kwargs)
def get_initial(self):
self.initial['data'] = datetime.now().date()
self.initial['data'] = timezone.now().date()
return self.initial
@ -1086,7 +1105,7 @@ class DocumentoAcessorioCrud(MasterDetailCrud):
class AutoriaCrud(MasterDetailCrud):
model = Autoria
parent_field = 'materia'
help_path = ''
help_topic = 'despacho_autoria'
public = [RP_LIST, RP_DETAIL]
list_field_names = ['autor', 'autor__tipo__descricao', 'primeiro_autor']
@ -1161,7 +1180,7 @@ class AutoriaMultiCreateView(PermissionRequiredForAppCrudMixin, FormView):
class DespachoInicialCrud(MasterDetailCrud):
model = DespachoInicial
parent_field = 'materia'
help_path = ''
help_topic = 'despacho_autoria'
public = [RP_LIST, RP_DETAIL]
class CreateView(MasterDetailCrud.CreateView):
@ -1174,7 +1193,7 @@ class DespachoInicialCrud(MasterDetailCrud):
class LegislacaoCitadaCrud(MasterDetailCrud):
model = LegislacaoCitada
parent_field = 'materia'
help_path = ''
help_topic = 'legislacao_cita_matanexada'
public = [RP_LIST, RP_DETAIL]
class BaseMixin(MasterDetailCrud.BaseMixin):
@ -1185,55 +1204,6 @@ class LegislacaoCitadaCrud(MasterDetailCrud):
return reverse('%s:%s' % (namespace, self.url_name(suffix)),
args=args)
def has_permission(self):
perms = self.get_permission_required()
# Torna a view pública se não possuir conteudo
# no atributo permission_required
return self.request.user.has_module_perms('materia')\
if len(perms) else True
def permission(self, rad):
return '%s%s%s' % ('norma' if rad.endswith('_') else '',
rad,
self.model_name if rad.endswith('_') else '')
@property
def detail_create_url(self):
obj = self.crud if hasattr(self, 'crud') else self
if self.request.user.has_module_perms('materia'):
parent_field = obj.parent_field.split('__')[0]
parent_object = getattr(self.object, parent_field)
root_pk = parent_object.pk
return self.resolve_url(ACTION_CREATE, args=(root_pk,))
return ''
@property
def list_url(self):
return self.resolve_url(ACTION_LIST, args=(self.kwargs['pk'],))\
if self.request.user.has_module_perms('materia') else ''
@property
def create_url(self):
return self.resolve_url(ACTION_CREATE, args=(self.kwargs['pk'],))\
if self.request.user.has_module_perms('materia') else ''
@property
def detail_url(self):
return self.resolve_url(ACTION_DETAIL, args=(self.object.id,))\
if self.request.user.has_module_perms('materia') else ''
@property
def update_url(self):
return self.resolve_url(ACTION_UPDATE, args=(self.kwargs['pk'],))\
if self.request.user.has_module_perms('materia') else ''
@property
def delete_url(self):
return self.resolve_url(ACTION_DELETE, args=(self.object.id,))\
if self.request.user.has_module_perms('materia') else ''
class CreateView(MasterDetailCrud.CreateView):
form_class = LegislacaoCitadaForm
@ -1259,14 +1229,14 @@ class LegislacaoCitadaCrud(MasterDetailCrud):
class NumeracaoCrud(MasterDetailCrud):
model = Numeracao
parent_field = 'materia'
help_path = ''
help_topic = 'numeracao_docsacess'
public = [RP_LIST, RP_DETAIL]
class AnexadaCrud(MasterDetailCrud):
model = Anexada
parent_field = 'materia_principal'
help_path = ''
help_topic = 'materia_anexada'
public = [RP_LIST, RP_DETAIL]
class BaseMixin(MasterDetailCrud.BaseMixin):
@ -1294,7 +1264,7 @@ class AnexadaCrud(MasterDetailCrud):
class MateriaAssuntoCrud(MasterDetailCrud):
model = MateriaAssunto
parent_field = 'materia'
help_path = ''
help_topic = ''
public = [RP_LIST, RP_DETAIL]
class BaseMixin(MasterDetailCrud.BaseMixin):
@ -1318,7 +1288,7 @@ class MateriaAssuntoCrud(MasterDetailCrud):
class MateriaLegislativaCrud(Crud):
model = MateriaLegislativa
help_path = 'materia_legislativa'
help_topic = 'materia_legislativa'
public = [RP_LIST, RP_DETAIL]
class BaseMixin(Crud.BaseMixin):
@ -1507,6 +1477,8 @@ class MateriaLegislativaPesquisaView(FilterView):
context['filter_url'] = ('&' + qr.urlencode()) if len(qr) > 0 else ''
context['show_results'] = show_results_filter_set(qr)
return context
@ -1608,6 +1580,9 @@ class DocumentoAcessorioEmLoteView(PermissionRequiredMixin, FilterView):
context['object_list'] = context['object_list'].order_by(
'ano', 'numero')
context['filter_url'] = ('&' + qr.urlencode()) if len(qr) > 0 else ''
context['show_results'] = show_results_filter_set(qr)
return context
def post(self, request, *args, **kwargs):
@ -1620,13 +1595,16 @@ class DocumentoAcessorioEmLoteView(PermissionRequiredMixin, FilterView):
tipo = TipoDocumento.objects.get(descricao=request.POST['tipo'])
tz = timezone.get_current_timezone()
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 = datetime.strptime(request.POST['data'], "%d/%m/%Y")
doc.data = tz.localize(datetime.strptime(
request.POST['data'], "%d/%m/%Y"))
doc.autor = request.POST['autor']
doc.ementa = request.POST['ementa']
doc.save()
@ -1675,25 +1653,30 @@ class PrimeiraTramitacaoEmLoteView(PermissionRequiredMixin, FilterView):
'ano', 'numero')
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')
tz = timezone.get_current_timezone()
if len(marcadas) == 0:
msg = _('Nenhuma máteria foi selecionada.')
messages.add_message(request, messages.ERROR, msg)
return self.get(request, self.kwargs)
if request.POST['data_encaminhamento']:
data_encaminhamento = datetime.strptime(
request.POST['data_encaminhamento'], "%d/%m/%Y")
data_encaminhamento = tz.localize(datetime.strptime(
request.POST['data_encaminhamento'], "%d/%m/%Y"))
else:
data_encaminhamento = None
if request.POST['data_fim_prazo']:
data_fim_prazo = datetime.strptime(
request.POST['data_fim_prazo'], "%d/%m/%Y")
data_fim_prazo = tz.localize(datetime.strptime(
request.POST['data_fim_prazo'], "%d/%m/%Y"))
else:
data_fim_prazo = None
@ -1704,8 +1687,8 @@ class PrimeiraTramitacaoEmLoteView(PermissionRequiredMixin, FilterView):
for materia_id in marcadas:
t = Tramitacao(
materia_id=materia_id,
data_tramitacao=datetime.strptime(
request.POST['data_tramitacao'], "%d/%m/%Y"),
data_tramitacao=tz.localize(datetime.strptime(
request.POST['data_tramitacao'], "%d/%m/%Y")),
data_encaminhamento=data_encaminhamento,
data_fim_prazo=data_fim_prazo,
unidade_tramitacao_local_id=request.POST[
@ -1718,6 +1701,12 @@ class PrimeiraTramitacaoEmLoteView(PermissionRequiredMixin, FilterView):
texto=request.POST['texto']
)
t.save()
if request.POST['turno'] == 'F':
for materia in MateriaLegislativa.objects.filter(id__in=marcadas):
materia.em_tramitacao = False
materia.save()
msg = _('Tramitação completa.')
messages.add_message(request, messages.SUCCESS, msg)
return self.get(request, self.kwargs)
@ -1751,8 +1740,9 @@ class ImpressosView(PermissionRequiredMixin, TemplateView):
template_name = 'materia/impressos/impressos.html'
permission_required = ('materia.can_access_impressos', )
def gerar_pdf_impressos(request, context):
template = loader.get_template('materia/impressos/pdf.html')
def gerar_pdf_impressos(request, context, template_name):
template = loader.get_template(template_name)
html = template.render(RequestContext(request, context))
response = HttpResponse(content_type="application/pdf")
weasyprint.HTML(
@ -1796,6 +1786,82 @@ class EtiquetaPesquisaView(PermissionRequiredMixin, FormView):
context['materias'] = materias
return gerar_pdf_impressos(self.request, context)
return gerar_pdf_impressos(self.request, context,
'materia/impressos/etiqueta_pdf.html')
class FichaPesquisaView(PermissionRequiredMixin, FormView):
form_class = FichaPesquisaForm
template_name = 'materia/impressos/ficha.html'
permission_required = ('materia.can_access_impressos', )
def form_valid(self, form):
tipo_materia = form.data['tipo_materia']
data_inicial = form.data['data_inicial']
data_final = form.data['data_final']
url = reverse('sapl.materia:impressos_ficha_seleciona')
url = url + '?tipo=%s&data_inicial=%s&data_final=%s' % (
tipo_materia, data_inicial, data_final)
return HttpResponseRedirect(url)
class FichaSelecionaView(PermissionRequiredMixin, FormView):
form_class = FichaSelecionaForm
template_name = 'materia/impressos/ficha_seleciona.html'
permission_required = ('materia.can_access_impressos', )
def get_context_data(self, **kwargs):
if ('tipo' not in self.request.GET or
'data_inicial' not in self.request.GET or
'data_final' not in self.request.GET):
return HttpResponseRedirect(reverse(
'sapl.materia:impressos_ficha_pesquisa'))
context = super(FichaSelecionaView, self).get_context_data(
**kwargs)
tipo = self.request.GET['tipo']
data_inicial = datetime.strptime(
self.request.GET['data_inicial'], "%d/%m/%Y").date()
data_final = datetime.strptime(
self.request.GET['data_final'], "%d/%m/%Y").date()
materia_list = MateriaLegislativa.objects.filter(
tipo=tipo,
data_apresentacao__range=(data_inicial, data_final))
context['quantidade'] = len(materia_list)
materia_list = materia_list[:20]
context['form'].fields['materia'].choices = [
(m.id, str(m)) for m in materia_list]
if context['quantidade'] > 20:
messages.info(self.request, _('Sua pesquisa retornou mais do que '
'20 impressos. Por questões de '
'performance, foram retornados '
'apenas os 20 primeiros. Caso '
'queira outros, tente fazer uma '
'pesquisa mais específica'))
return context
def form_valid(self, form):
context = {}
try:
materia = MateriaLegislativa.objects.get(
id=form.data['materia'])
except ObjectDoesNotExist:
mensagem = _('Esta Máteria não existe!')
self.messages.add_message(self.request, messages.INFO, mensagem)
return self.render_to_response(context)
context['materia'] = materia
context['despachos'] = materia.despachoinicial_set.all().values_list(
'comissao__nome', flat=True)
return gerar_pdf_impressos(self.request, context,
'materia/impressos/ficha_pdf.html')

119
sapl/norma/fixtures/pre_popula_tipo_vinculo_norma.json

@ -0,0 +1,119 @@
[
{
"fields": {
"descricao_ativa": "Altera o(a)",
"descricao_passiva": "Alterado(a) pelo(a)",
"sigla": "A"
},
"model": "norma.TipoVinculoNormaJuridica",
"pk": "1"
},
{
"fields": {
"descricao_ativa": "Revoga integralmente o(a)",
"descricao_passiva": "Revogado(a) integralmente pelo(a)",
"sigla": "R"
},
"model": "norma.TipoVinculoNormaJuridica",
"pk": "2"
},
{
"fields": {
"descricao_ativa": "Revoga parcialmente o(a)",
"descricao_passiva": "Revogado(a) parcialmente pelo(a)",
"sigla": "P"
},
"model": "norma.TipoVinculoNormaJuridica",
"pk": "3"
},
{
"fields": {
"descricao_ativa": "Revoga integralmente por consolida\u00e7\u00e3o",
"descricao_passiva": "Revogado(a) integralmente por consolida\u00e7\u00e3o",
"sigla": "T"
},
"model": "norma.TipoVinculoNormaJuridica",
"pk": "4"
},
{
"fields": {
"descricao_ativa": "Norma correlata",
"descricao_passiva": "Norma correlata",
"sigla": "C"
},
"model": "norma.TipoVinculoNormaJuridica",
"pk": "5"
},
{
"fields": {
"descricao_ativa": "Ressalva o(a)",
"descricao_passiva": "Ressalvada pelo(a)",
"sigla": "S"
},
"model": "norma.TipoVinculoNormaJuridica",
"pk": "6"
},
{
"fields": {
"descricao_ativa": "Reedita o(a)",
"descricao_passiva": "Reeditada pelo(a)",
"sigla": "E"
},
"model": "norma.TipoVinculoNormaJuridica",
"pk": "7"
},
{
"fields": {
"descricao_ativa": "Reedita com altera\u00e7\u00e3o o(a)",
"descricao_passiva": "Reeditada com altera\u00e7\u00e3o pelo(a)",
"sigla": "I"
},
"model": "norma.TipoVinculoNormaJuridica",
"pk": "8"
},
{
"fields": {
"descricao_ativa": "Regulamenta o(a)",
"descricao_passiva": "Regulamentada pelo(a)",
"sigla": "G"
},
"model": "norma.TipoVinculoNormaJuridica",
"pk": "9"
},
{
"fields": {
"descricao_ativa": "Suspende parcialmente o(a)",
"descricao_passiva": "Suspenso(a) parcialmente pelo(a)",
"sigla": "K"
},
"model": "norma.TipoVinculoNormaJuridica",
"pk": "10"
},
{
"fields": {
"descricao_ativa": "Suspende integralmente o(a)",
"descricao_passiva": "Suspenso(a) integralmente pelo(a)",
"sigla": "L"
},
"model": "norma.TipoVinculoNormaJuridica",
"pk": "11"
},
{
"fields": {
"descricao_ativa": "Julga integralmente inconstitucional",
"descricao_passiva": "Julgada integralmente inconstitucional",
"sigla": "N"
},
"model": "norma.TipoVinculoNormaJuridica",
"pk": "12"
},
{
"fields": {
"descricao_ativa": "Julga parcialmente inconstitucional",
"descricao_passiva": "Julgada parcialmente inconstitucional",
"sigla": "O"
},
"model": "norma.TipoVinculoNormaJuridica",
"pk": "13"
}
]

70
sapl/norma/forms.py

@ -1,4 +1,3 @@
from datetime import datetime
import django_filters
from crispy_forms.helper import FormHelper
@ -7,6 +6,7 @@ from django import forms
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.db import models
from django.forms import ModelForm, widgets
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from sapl.crispy_layout_mixin import form_actions, to_row
@ -72,7 +72,7 @@ class NormaFilterSet(django_filters.FilterSet):
self.form.helper.layout = Layout(
Fieldset(_('Pesquisa de Norma'),
row1, row2, row3,
form_actions(save_label='Pesquisar'))
form_actions(label='Pesquisar'))
)
@ -131,11 +131,13 @@ class NormaJuridicaForm(ModelForm):
tipo_id=cleaned_data['tipo_materia'],
numero=cleaned_data['numero_materia'],
ano=cleaned_data['ano_materia'])
except ObjectDoesNotExist:
raise forms.ValidationError(
_("Matéria %s/%s é inexistente." % (
_("Matéria Legislativa %s/%s (%s) é inexistente." % (
self.cleaned_data['numero_materia'],
self.cleaned_data['ano_materia'])))
self.cleaned_data['ano_materia'],
cleaned_data['tipo_materia'].descricao)))
else:
cleaned_data['materia'] = materia
@ -154,7 +156,7 @@ class NormaJuridicaForm(ModelForm):
def save(self, commit=False):
norma = self.instance
norma.timestamp = datetime.now()
norma.timestamp = timezone.now()
norma.materia = self.cleaned_data['materia']
norma = super(NormaJuridicaForm, self).save(commit=True)
return norma
@ -206,3 +208,61 @@ class NormaRelacionadaForm(ModelForm):
relacionada.norma_relacionada = self.cleaned_data['norma_relacionada']
relacionada.save()
return relacionada
class NormaPesquisaSimplesForm(forms.Form):
tipo_norma = forms.ModelChoiceField(
label=TipoNormaJuridica._meta.verbose_name,
queryset=TipoNormaJuridica.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')
)
def __init__(self, *args, **kwargs):
super(NormaPesquisaSimplesForm, self).__init__(*args, **kwargs)
row1 = to_row(
[('tipo_norma', 6),
('data_inicial', 3),
('data_final', 3)])
self.helper = FormHelper()
self.helper.layout = Layout(
Fieldset(
('Índice de Normas'),
row1,
form_actions(label='Pesquisar')
)
)
def clean(self):
super(NormaPesquisaSimplesForm, self).clean()
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):
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:
raise ValidationError(_('Caso pesquise por data, os campos de Data Inicial e ' +
'Data Final devem ser preenchidos obrigatoriamente'))
return cleaned_data

39
sapl/norma/migrations/0008_normajuridica_popula_tipo_vinculo_norma.py

@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import os
from django.core.management import call_command
from django.db import migrations
def gera_tipo_vinculo(apps, schema_editor):
TipoVinculoNormaJuridica = apps.get_model("norma", "TipoVinculoNormaJuridica")
db_alias = schema_editor.connection.alias
tipo_vinculos = TipoVinculoNormaJuridica.objects.all().exists()
if tipo_vinculos:
# Caso haja algum TipoVinculoNormaJuridica cadastrado na base de dados,
# a migração não deve ser carregada para evitar duplicações de dados.
print("Carga de {} não efetuada. Já Existem {} cadastrados...".format(
TipoVinculoNormaJuridica._meta.verbose_name,
TipoVinculoNormaJuridica._meta.verbose_name_plural
)
)
else:
fixture_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '../fixtures'))
# pega tipo_vinculo_norma_juridica listados em fixtures/pre_popula_tipo_vinculo_norma.json
fixture_filename = 'pre_popula_tipo_vinculo_norma.json'
fixture_file = os.path.join(fixture_dir, fixture_filename)
call_command('loaddata', fixture_file)
class Migration(migrations.Migration):
dependencies = [
('norma', '0007_auto_20170904_1708'),
]
operations = [
migrations.RunPython(gera_tipo_vinculo),
]

26
sapl/norma/migrations/0009_auto_20171113_1339.py

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.11 on 2017-11-13 15:39
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('norma', '0008_normajuridica_popula_tipo_vinculo_norma'),
]
operations = [
migrations.AlterField(
model_name='legislacaocitada',
name='materia',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='materia.MateriaLegislativa'),
),
migrations.AlterField(
model_name='legislacaocitada',
name='norma',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='norma.NormaJuridica'),
),
]

4
sapl/norma/models.py

@ -174,8 +174,8 @@ class NormaJuridica(models.Model):
@reversion.register()
class LegislacaoCitada(models.Model):
materia = models.ForeignKey(MateriaLegislativa, on_delete=models.PROTECT)
norma = models.ForeignKey(NormaJuridica, on_delete=models.PROTECT)
materia = models.ForeignKey(MateriaLegislativa, on_delete=models.CASCADE)
norma = models.ForeignKey(NormaJuridica, on_delete=models.CASCADE)
disposicoes = models.CharField(
max_length=15, blank=True, verbose_name=_('Disposição'))
parte = models.CharField(

10
sapl/norma/tests/test_norma.py

@ -1,13 +1,11 @@
import pytest
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
from model_mommy import mommy
from sapl.materia.models import MateriaLegislativa, TipoMateriaLegislativa
from sapl.norma.forms import NormaJuridicaForm, NormaRelacionadaForm
from sapl.norma.models import (NormaJuridica, TipoNormaJuridica)
from sapl.norma.models import NormaJuridica, TipoNormaJuridica
@pytest.mark.django_db(transaction=False)
@ -78,7 +76,7 @@ def test_norma_form_invalida():
def test_norma_juridica_materia_inexistente():
tipo = mommy.make(TipoNormaJuridica)
tipo_materia = mommy.make(TipoMateriaLegislativa)
tipo_materia = mommy.make(TipoMateriaLegislativa, descricao='VETO')
form = NormaJuridicaForm(data={'tipo': str(tipo.pk),
'numero': '1',
@ -93,14 +91,14 @@ def test_norma_juridica_materia_inexistente():
assert not form.is_valid()
assert form.errors['__all__'] == [_("Matéria 2/2017 é inexistente.")]
assert form.errors['__all__'] == [_("Matéria Legislativa 2/2017 (VETO) é inexistente.")]
@pytest.mark.django_db(transaction=False)
def test_norma_juridica_materia_existente():
tipo = mommy.make(TipoNormaJuridica)
tipo_materia = mommy.make(TipoMateriaLegislativa)
materia = mommy.make(MateriaLegislativa,
mommy.make(MateriaLegislativa,
numero=2,
ano=2017,
tipo=tipo_materia)

74
sapl/norma/views.py

@ -1,17 +1,26 @@
from datetime import datetime
from django.core.exceptions import ObjectDoesNotExist
from django.core.urlresolvers import reverse
from django.http import JsonResponse
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from django.views.generic.base import RedirectView
from django_filters.views import FilterView
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.http import HttpResponse, JsonResponse
from django.views.generic.edit import FormView
from django.views.generic import CreateView, ListView, TemplateView, UpdateView
from django.template import RequestContext, loader
import weasyprint
from sapl.base.models import AppConfig
from sapl.compilacao.views import IntegracaoTaView
from sapl.crud.base import (RP_DETAIL, RP_LIST, Crud, CrudAux,
MasterDetailCrud, make_pagination)
from sapl.utils import show_results_filter_set
from .forms import NormaFilterSet, NormaJuridicaForm, NormaRelacionadaForm
from .forms import NormaFilterSet, NormaJuridicaForm, NormaRelacionadaForm, NormaPesquisaSimplesForm
from .models import (AssuntoNorma, NormaJuridica, NormaRelacionada,
TipoNormaJuridica, TipoVinculoNormaJuridica)
@ -31,7 +40,7 @@ TipoVinculoNormaJuridicaCrud = CrudAux.build(
class NormaRelacionadaCrud(MasterDetailCrud):
model = NormaRelacionada
parent_field = 'norma_principal'
help_path = ''
help_topic = 'norma_juridica'
public = [RP_LIST, RP_DETAIL]
class BaseMixin(MasterDetailCrud.BaseMixin):
@ -62,6 +71,13 @@ class NormaPesquisaView(FilterView):
filterset_class = NormaFilterSet
paginate_by = 10
def get_queryset(self):
qs = super().get_queryset()
qs.select_related('tipo', 'materia')
return qs
def get_context_data(self, **kwargs):
context = super(NormaPesquisaView, self).get_context_data(**kwargs)
@ -80,6 +96,8 @@ class NormaPesquisaView(FilterView):
context['filter_url'] = ('&' + qr.urlencode()) if len(qr) > 0 else ''
context['show_results'] = show_results_filter_set(qr)
return context
@ -112,7 +130,7 @@ class NormaTaView(IntegracaoTaView):
class NormaCrud(Crud):
model = NormaJuridica
help_path = 'norma_juridica'
help_topic = 'norma_juridica'
public = [RP_LIST, RP_DETAIL]
class BaseMixin(Crud.BaseMixin):
@ -146,7 +164,6 @@ class NormaCrud(Crud):
def layout_key(self):
return 'NormaJuridicaCreate'
class ListView(Crud.ListView, RedirectView):
def get_redirect_url(self, *args, **kwargs):
@ -194,7 +211,7 @@ def recuperar_numero_norma(request):
ano = request.GET.get('ano', '')
param = {'tipo': tipo}
param['ano'] = ano if ano else datetime.now().year
param['ano'] = ano if ano else timezone.now().year
norma = NormaJuridica.objects.filter(**param).order_by(
'tipo', 'ano', 'numero').values_list('numero', 'ano').last()
@ -206,3 +223,48 @@ def recuperar_numero_norma(request):
{'numero': 1, 'ano': ano})
return response
class ImpressosView(PermissionRequiredMixin, TemplateView):
template_name = 'materia/impressos/impressos.html'
permission_required = ('materia.can_access_impressos', )
def gerar_pdf_impressos(request, context, template_name):
template = loader.get_template(template_name)
html = template.render(RequestContext(request, context))
response = HttpResponse(content_type="application/pdf")
weasyprint.HTML(
string=html,
base_url=request.build_absolute_uri()).write_pdf(
response)
return response
class NormaPesquisaSimplesView(PermissionRequiredMixin, FormView):
form_class = NormaPesquisaSimplesForm
template_name = 'materia/impressos/norma.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'
if form.cleaned_data['tipo_norma']:
normas = normas.filter(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'])
qtd_resultados = len(normas)
if qtd_resultados > 2000:
normas = normas[:2000]
context = {'quantidade': qtd_resultados,
'normas': normas}
return gerar_pdf_impressos(self.request, context, template_norma)

6
sapl/painel/urls.py

@ -3,7 +3,7 @@ from django.conf.urls import url
from .apps import AppConfig
from .views import (cronometro_painel, get_dados_painel, painel_mensagem_view,
painel_parlamentar_view, painel_view, painel_votacao_view,
votante_view)
switch_painel, verifica_painel, votante_view)
app_name = AppConfig.name
@ -14,7 +14,11 @@ urlpatterns = [
url(r'^painel/mensagem$', painel_mensagem_view, name="painel_mensagem"),
url(r'^painel/parlamentar$', painel_parlamentar_view,
name='painel_parlamentar'),
url(r'^painel/switch-painel$', switch_painel,
name="switch_painel"),
url(r'^painel/votacao$', painel_votacao_view, name='painel_votacao'),
url(r'^painel/verifica-painel$', verifica_painel,
name="verifica_painel"),
url(r'^painel/cronometro$', cronometro_painel, name='cronometro_painel'),
# url(r'^painel/cronometro$', include(CronometroPainelCrud.get_urls())),

387
sapl/painel/views.py

@ -1,4 +1,4 @@
import json
from django.contrib import messages
from django.contrib.auth.decorators import user_passes_test
@ -8,15 +8,19 @@ from django.db.models import Q
from django.http import HttpResponse, JsonResponse
from django.http.response import Http404, HttpResponseRedirect
from django.shortcuts import render
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from sapl.base.models import AppConfig as ConfiguracoesAplicacao
from sapl.base.models import CasaLegislativa
from sapl.crud.base import Crud
from sapl.painel.apps import AppConfig
from sapl.parlamentares.models import Filiacao, Votante
from sapl.sessao.models import (ExpedienteMateria, OrdemDia, PresencaOrdemDia,
from sapl.parlamentares.models import Legislatura, Parlamentar, Votante
from sapl.sessao.models import (ExpedienteMateria, OrdemDia, OradorExpediente,
PresencaOrdemDia,
RegistroVotacao, SessaoPlenaria,
SessaoPlenariaPresenca, VotoParlamentar)
from sapl.utils import get_client_ip
from sapl.utils import filiacao_data, get_client_ip, sort_lista_chave
from .models import Cronometro
@ -224,6 +228,28 @@ def painel_view(request, pk):
return render(request, 'painel/index.html', context)
@user_passes_test(check_permission)
def switch_painel(request):
sessao = SessaoPlenaria.objects.get(id=request.POST['pk_sessao'])
switch = json.loads(request.POST['aberto'])
if switch:
sessao.painel_aberto = True
else:
sessao.painel_aberto = False
sessao.save()
return JsonResponse({})
@user_passes_test(check_permission)
def verifica_painel(request):
sessao = SessaoPlenaria.objects.get(id=request.GET['pk_sessao'])
status = sessao.painel_aberto
resposta = JsonResponse(dict(status=status))
return resposta
@user_passes_test(check_permission)
def painel_mensagem_view(request):
return render(request, 'painel/mensagem.html')
@ -252,8 +278,6 @@ def get_cronometro_status(request, name):
cronometro = ''
return cronometro
# ##############################ORDEM DO DIA##################################
def get_materia_aberta(pk):
return OrdemDia.objects.filter(
@ -261,254 +285,155 @@ def get_materia_aberta(pk):
def get_presentes(pk, response, materia):
filiacao = Filiacao.objects.filter(
data_desfiliacao__isnull=True, parlamentar__ativo=True)
parlamentar_partido = {}
for f in filiacao:
parlamentar_partido[
f.parlamentar.nome_parlamentar] = f.partido.sigla
sessao_plenaria_presenca = SessaoPlenariaPresenca.objects.filter(
if type(materia) == OrdemDia:
presentes = PresencaOrdemDia.objects.filter(
sessao_plenaria_id=pk)
presentes_sessao_plenaria = [
p.parlamentar.nome_parlamentar for p in sessao_plenaria_presenca]
num_presentes_sessao_plen = len(presentes_sessao_plenaria)
presenca_ordem_dia = PresencaOrdemDia.objects.filter(
else:
presentes = SessaoPlenariaPresenca.objects.filter(
sessao_plenaria_id=pk)
presentes_ordem_dia = []
for p in presenca_ordem_dia:
nome_parlamentar = p.parlamentar.nome_parlamentar
try:
parlamentar_partido[nome_parlamentar]
except KeyError:
presentes_ordem_dia.append(
{'id': p.id,
'nome': nome_parlamentar,
'partido': str(_('Sem Registro')),
sessao = SessaoPlenaria.objects.get(id=pk)
num_presentes = len(presentes)
data_sessao = sessao.data_inicio
oradores = OradorExpediente.objects.filter(
sessao_plenaria_id=pk).order_by('numero_ordem')
oradores_list = []
for o in oradores:
oradores_list.append(
{
'nome': o.parlamentar.nome_parlamentar,
'numero': o.numero_ordem
})
presentes_list = []
for p in presentes:
now_year = timezone.now().year
# Recupera a legislatura vigente
legislatura = Legislatura.objects.get(data_inicio__year__lte = now_year,
data_fim__year__gte = now_year)
# Recupera os mandatos daquele parlamentar
mandatos = p.parlamentar.mandato_set.filter(legislatura=legislatura)
if p.parlamentar.ativo and mandatos:
filiacao = filiacao_data(p.parlamentar, data_sessao, data_sessao)
if not filiacao:
partido = 'Sem Registro'
else:
presentes_ordem_dia.append(
partido = filiacao
presentes_list.append(
{'id': p.id,
'nome': nome_parlamentar,
'partido': parlamentar_partido[nome_parlamentar],
'parlamentar_id': p.parlamentar.id,
'nome': p.parlamentar.nome_parlamentar,
'partido': partido,
'voto': ''
})
num_presentes_ordem_dia = len(presentes_ordem_dia)
elif not p.parlamentar.ativo or not mandatos:
num_presentes += -1
if materia:
if materia.tipo_votacao == 1:
tipo_votacao = str(_('Simbólica'))
response = get_votos(response, materia)
tipo_votacao = 'Simbólica'
elif materia.tipo_votacao == 2:
tipo_votacao = 'Nominal'
response = get_votos_nominal(response, materia)
elif materia.tipo_votacao == 3:
tipo_votacao = 'Secreta'
response = get_votos(response, materia)
response.update({
'presentes_ordem_dia': presentes_ordem_dia,
'num_presentes_ordem_dia': num_presentes_ordem_dia,
'presentes_sessao_plenaria': presentes_sessao_plenaria,
'num_presentes_sessao_plenaria': num_presentes_sessao_plen,
'status_painel': 'ABERTO',
'msg_painel': str(_('Votação aberta!')),
'tipo_resultado': materia.resultado,
'tipo_votacao': tipo_votacao,
'observacao_materia': materia.observacao,
'materia_legislativa_texto': str(materia.materia)})
return response
# ########################EXPEDIENTE############################################
def get_materia_expediente_aberta(pk):
return ExpedienteMateria.objects.filter(
sessao_plenaria_id=pk, votacao_aberta=True).last()
def get_presentes_expediente(pk, response, materia):
filiacao = Filiacao.objects.filter(
data_desfiliacao__isnull=True, parlamentar__ativo=True)
parlamentar_partido = {}
for f in filiacao:
parlamentar_partido[
f.parlamentar.nome_parlamentar] = f.partido.sigla
sessao_plenaria_presenca = SessaoPlenariaPresenca.objects.filter(
sessao_plenaria_id=pk)
presentes_sessao_plenaria = [
p.parlamentar.nome_parlamentar for p in sessao_plenaria_presenca]
num_presentes_sessao_plen = len(presentes_sessao_plenaria)
presenca_expediente = SessaoPlenariaPresenca.objects.filter(
sessao_plenaria_id=pk)
presentes_expediente = []
for p in presenca_expediente:
nome_parlamentar = p.parlamentar.nome_parlamentar
try:
parlamentar_partido[nome_parlamentar]
except KeyError:
presentes_expediente.append(
{'id': p.id,
'nome': nome_parlamentar,
'partido': str(_('Sem Registro')),
})
else:
presentes_expediente.append(
{'id': p.id,
'nome': nome_parlamentar,
'partido': parlamentar_partido[nome_parlamentar],
'tipo_votacao': tipo_votacao,
'materia_legislativa_texto': str(materia.materia),
'oradores': oradores_list
})
num_presentes_expediente = len(presentes_expediente)
if materia.tipo_votacao == 1:
tipo_votacao = 'Simbólica'
response = get_votos(response, materia)
elif materia.tipo_votacao == 2:
tipo_votacao = 'Nominal'
response = get_votos_nominal(response, materia)
elif materia.tipo_votacao == 3:
tipo_votacao = 'Secreta'
response = get_votos(response, materia)
presentes_list = sort_lista_chave(presentes_list, 'nome')
response.update({
'presentes_expediente': presentes_expediente,
'num_presentes_expediente': num_presentes_expediente,
'presentes_sessao_plenaria': presentes_sessao_plenaria,
'num_presentes_sessao_plenaria': num_presentes_sessao_plen,
'status_painel': str(_('ABERTO')),
'presentes': presentes_list,
'num_presentes': num_presentes,
'msg_painel': str(_('Votação aberta!')),
'tipo_resultado': tipo_votacao,
'observacao_materia': materia.observacao,
'materia_legislativa_texto': str(materia.materia)})
})
return response
# ##########################GENERAL FUNCTIONS#############################
def get_materia_expediente_aberta(pk):
return ExpedienteMateria.objects.filter(
sessao_plenaria_id=pk, votacao_aberta=True).last()
def response_nenhuma_materia(response):
response.update({
'status_painel': 'FECHADO',
'msg_painel': str(_('Nenhuma matéria disponivel para votação.'))})
return JsonResponse(response)
def get_votos(response, materia):
if materia.tipo_votacao == 1:
tipo_votacao = 'Simbólica'
elif materia.tipo_votacao == 2:
tipo_votacao = 'Nominal'
elif materia.tipo_votacao == 3:
tipo_votacao = 'Secreta'
if type(materia) == OrdemDia:
registro = RegistroVotacao.objects.filter(
ordem=materia, materia=materia.materia).last()
else:
registro = RegistroVotacao.objects.filter(
expediente=materia, materia=materia.materia).last()
if registro:
total = (registro.numero_votos_sim +
registro.numero_votos_nao +
registro.numero_abstencoes)
response.update({
'numero_votos_sim': registro.numero_votos_sim,
'numero_votos_nao': registro.numero_votos_nao,
'numero_abstencoes': registro.numero_abstencoes,
'total_votos': total,
'tipo_votacao': tipo_votacao,
'tipo_resultado': registro.tipo_resultado_votacao.nome,
'natureza_resultado': registro.tipo_resultado_votacao.natureza,
})
else:
response.update({
'numero_votos_sim': 0,
'numero_votos_nao': 0,
'numero_abstencoes': 0,
'total_votos': 0,
'tipo_votacao': tipo_votacao,
'tipo_resultado': 'Ainda não foi votada.',
'natureza_resultado': None,
})
return response
def get_votos_nominal(response, materia):
votos = []
if materia.tipo_votacao == 1:
tipo_votacao = 'Simbólica'
elif materia.tipo_votacao == 2:
tipo_votacao = 'Nominal'
elif materia.tipo_votacao == 3:
tipo_votacao = 'Secreta'
if type(materia) == OrdemDia:
registro = RegistroVotacao.objects.filter(
ordem=materia, materia=materia.materia).last()
else:
tipo = 'ordem'
elif type(materia) == ExpedienteMateria:
registro = RegistroVotacao.objects.filter(
expediente=materia, materia=materia.materia).last()
tipo = 'expediente'
if not registro:
response.update({
'numero_votos_sim': 0,
'numero_votos_nao': 0,
'numero_abstencoes': 0,
'registro': None,
'total_votos': 0,
'tipo_votacao': tipo_votacao,
'tipo_resultado': 'Não foi votado ainda',
'natureza_resultado': None,
'votos': None
'tipo_resultado': 'Ainda não foi votada.',
})
if materia.tipo_votacao == 2:
if tipo == 'ordem':
votos_parlamentares = VotoParlamentar.objects.filter(
ordem_id=materia.id).order_by(
'parlamentar__nome_parlamentar')
else:
votos_parlamentares = VotoParlamentar.objects.filter(
votacao_id=registro.id).order_by('parlamentar__nome_parlamentar')
expediente_id=materia.id).order_by(
'parlamentar__nome_parlamentar')
filiacao = Filiacao.objects.filter(
data_desfiliacao__isnull=True, parlamentar__ativo=True)
parlamentar_partido = {}
for f in filiacao:
parlamentar_partido[
f.parlamentar.nome_parlamentar] = f.partido.sigla
for v in votos_parlamentares:
for i, p in enumerate(response['presentes']):
try:
parlamentar_partido[v.parlamentar.nome_parlamentar]
except KeyError:
votos.append({
'parlamentar': v.parlamentar.nome_parlamentar,
'voto': str(v.voto),
'partido': str(_('Sem Registro'))
})
else:
votos.append({
'parlamentar': v.parlamentar.nome_parlamentar,
'voto': str(v.voto),
'partido': parlamentar_partido[
v.parlamentar.nome_parlamentar]
})
if votos_parlamentares.get(parlamentar_id=p['parlamentar_id']).voto:
response['presentes'][i]['voto'] = 'Voto Informado'
except ObjectDoesNotExist:
response['presentes'][i]['voto'] = ''
else:
total = (registro.numero_votos_sim +
registro.numero_votos_nao +
registro.numero_abstencoes)
if materia.tipo_votacao == 2:
votos_parlamentares = VotoParlamentar.objects.filter(
votacao_id=registro.id).order_by(
'parlamentar__nome_parlamentar')
for i, p in enumerate(response['presentes']):
try:
response['presentes'][i]['voto'] = votos_parlamentares.get(
parlamentar_id=p['parlamentar_id']).voto
except ObjectDoesNotExist:
response['presentes'][i]['voto'] = None
response.update({
'numero_votos_sim': registro.numero_votos_sim,
'numero_votos_nao': registro.numero_votos_nao,
'numero_abstencoes': registro.numero_abstencoes,
'registro': True,
'total_votos': total,
'tipo_votacao': tipo_votacao,
'tipo_resultado': registro.tipo_resultado_votacao.nome,
'natureza_resultado': registro.tipo_resultado_votacao.natureza,
'votos': votos
})
return response
@ -517,79 +442,71 @@ def get_votos_nominal(response, materia):
@user_passes_test(check_permission)
def get_dados_painel(request, pk):
sessao = SessaoPlenaria.objects.get(id=pk)
cronometro_discurso = get_cronometro_status(request, 'discurso')
cronometro_aparte = get_cronometro_status(request, 'aparte')
cronometro_ordem = get_cronometro_status(request, 'ordem')
casa = CasaLegislativa.objects.first()
app_config = ConfiguracoesAplicacao.objects.first()
brasao = None
if casa and app_config and (bool(casa.logotipo)):
brasao = casa.logotipo.url \
if app_config.mostrar_brasao_painel else None
response = {
'sessao_plenaria': str(sessao),
'sessao_plenaria_data': sessao.data_inicio.strftime('%d/%m/%Y'),
'sessao_plenaria_hora_inicio': sessao.hora_inicio,
"cronometro_aparte": cronometro_aparte,
"cronometro_discurso": cronometro_discurso,
"cronometro_ordem": cronometro_ordem,
'cronometro_aparte': get_cronometro_status(request, 'aparte'),
'cronometro_discurso': get_cronometro_status(request, 'discurso'),
'cronometro_ordem': get_cronometro_status(request, 'ordem'),
'status_painel': sessao.painel_aberto,
'brasao': brasao
}
ordem_dia = get_materia_aberta(pk)
expediente = get_materia_expediente_aberta(pk)
# Caso tenha alguma matéria com votação aberta, ela é mostrada no painel
# com prioridade para Ordem do Dia.
if ordem_dia:
return JsonResponse(get_presentes(pk, response, ordem_dia))
return JsonResponse(get_votos(
get_presentes(pk, response, ordem_dia),
ordem_dia))
elif expediente:
return JsonResponse(get_presentes_expediente(pk, response, expediente))
return JsonResponse(get_votos(
get_presentes(pk, response, expediente),
expediente))
# Ultimo voto em ordem e ultimo voto em expediente
# Caso não tenha nenhuma aberta,
# a matéria a ser mostrada no Painel deve ser a última votada
last_ordem_voto = RegistroVotacao.objects.filter(
ordem__sessao_plenaria=sessao).last()
last_expediente_voto = RegistroVotacao.objects.filter(
expediente__sessao_plenaria=sessao).last()
# Ultimas materias votadas
if last_ordem_voto:
ultima_ordem_votada = last_ordem_voto.ordem
if last_expediente_voto:
ultimo_expediente_votado = last_expediente_voto.expediente
# Caso não tenha nenhuma votação aberta
if last_ordem_voto or last_expediente_voto:
# Se alguma ordem E algum expediente já tiver sido votado...
if last_ordem_voto and last_expediente_voto:
# Verifica se o último resultado é um uma ordem do dia
if last_ordem_voto.pk >= last_expediente_voto.pk:
if ultima_ordem_votada.tipo_votacao in [1, 3]:
return JsonResponse(
get_votos(get_presentes(
pk, response, ultima_ordem_votada),
ultima_ordem_votada))
elif ultima_ordem_votada.tipo_votacao == 2:
return JsonResponse(
get_votos_nominal(get_presentes(
pk, response, ultima_ordem_votada),
ultima_ordem_votada))
# Caso não seja, verifica se é um expediente
else:
if ultimo_expediente_votado.tipo_votacao in [1, 3]:
return JsonResponse(
get_votos(get_presentes_expediente(
pk, response, ultimo_expediente_votado),
ultimo_expediente_votado))
elif ultimo_expediente_votado.tipo_votacao == 2:
return JsonResponse(
get_votos_nominal(get_presentes_expediente(
pk, response,
ultimo_expediente_votado),
ultimo_expediente_votado))
materia = ultima_ordem_votada\
if last_ordem_voto.pk >= last_expediente_voto.pk\
else ultimo_expediente_votado
# Caso somente um deles tenha resultado, prioriza a Ordem do Dia
if last_ordem_voto:
return JsonResponse(get_presentes(
pk, response, ultima_ordem_votada))
elif last_ordem_voto:
materia = ultima_ordem_votada
# Caso a Ordem do dia não tenha resultado, mostra o último expediente
if last_expediente_voto:
return JsonResponse(get_presentes_expediente(
pk, response,
ultimo_expediente_votado))
elif last_expediente_voto:
materia = ultimo_expediente_votado
return JsonResponse(get_votos(
get_presentes(pk, response, materia),
materia))
# Retorna que não há nenhuma matéria já votada ou aberta
return response_nenhuma_materia(response)
return response_nenhuma_materia(get_presentes(pk, response, None))

56
sapl/parlamentares/forms.py

@ -1,4 +1,4 @@
from datetime import date, timedelta
from datetime import timedelta
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Fieldset, Layout
@ -9,6 +9,7 @@ from django.core.exceptions import ValidationError
from django.db import transaction
from django.db.models import Q
from django.forms import ModelForm
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from floppyforms.widgets import ClearableFileInput
@ -108,52 +109,16 @@ class LegislaturaForm(ModelForm):
data_fim = data['data_fim']
data_eleicao = data['data_eleicao']
if data_eleicao.year >= data_inicio.year:
raise ValidationError(_("Data eleição não pode ser inferior a "
"data início da legislatura"))
pk = self.instance.pk
if data_inicio > data_fim or (data_fim.year - data_inicio.year != 4):
raise ValidationError(_("Intervalo de início e fim inválido para "
"legislatura."))
return data
class LegislaturaCreateForm(LegislaturaForm):
def clean(self):
super(LegislaturaCreateForm, self).clean()
cleaned_data = self.cleaned_data
if not self.is_valid():
return cleaned_data
eleicao = cleaned_data['data_eleicao']
inicio = cleaned_data['data_inicio']
fim = cleaned_data['data_fim']
valida_datas = validar_datas_legislatura(eleicao, inicio, fim)
valida_datas = validar_datas_legislatura(data_eleicao,
data_inicio,
data_fim,
pk=pk)
if not valida_datas[0]:
raise ValidationError(valida_datas[1])
return cleaned_data
class LegislaturaUpdateForm(LegislaturaCreateForm):
def clean(self):
super(LegislaturaUpdateForm, self).clean()
cleaned_data = super(LegislaturaCreateForm, self).clean()
eleicao = cleaned_data['data_eleicao']
inicio = cleaned_data['data_inicio']
fim = cleaned_data['data_fim']
valida_datas = validar_datas_legislatura(
eleicao, inicio, fim, pk=self.instance.pk)
if not valida_datas[0]:
raise ValidationError(valida_datas[1])
return cleaned_data
return data
class ParlamentarForm(ModelForm):
@ -204,7 +169,8 @@ def validar_datas(data_filiacao, data_desfiliacao, parlamentar, filiacao):
return [True, '']
# data ficticia de desfiliacao
df_desfiliacao = data_desfiliacao if data_desfiliacao else date.today()
today = timezone.now()
df_desfiliacao = data_desfiliacao if data_desfiliacao else today
# se não puder haver filiação no mesmo dia de desfiliação, basta
# retirar os timedelta abaixo
@ -332,7 +298,7 @@ class VotanteForm(ModelForm):
self.helper = FormHelper()
self.helper.layout = Layout(
Fieldset(_('Votante'),
row1, form_actions(save_label='Salvar'))
row1, form_actions(label='Salvar'))
)
super(VotanteForm, self).__init__(*args, **kwargs)

27
sapl/parlamentares/migrations/0009_auto_20170905_1617.py

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.13 on 2017-09-05 16:17
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('parlamentares', '0008_adiciona_cargos_mesa'),
]
operations = [
migrations.AlterModelOptions(
name='situacaomilitar',
options={'ordering': ['descricao'], 'verbose_name': 'Tipo Situação Militar', 'verbose_name_plural': 'Tipos Situações Militares'},
),
migrations.AlterModelOptions(
name='tipoafastamento',
options={'ordering': ['descricao'], 'verbose_name': 'Tipo de Afastamento', 'verbose_name_plural': 'Tipos de Afastamento'},
),
migrations.AlterModelOptions(
name='tipodependente',
options={'ordering': ['descricao'], 'verbose_name': 'Tipo de Dependente', 'verbose_name_plural': 'Tipos de Dependente'},
),
]

36
sapl/parlamentares/migrations/0010_corrige_data_inicio_mandato.py

@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations
def altera_data_inicio_mandato(apps, schema_editor):
Mandato = apps.get_model("parlamentares", "Mandato")
mandatos = Mandato.objects.all()
for mandato in mandatos:
data_inicio = mandato.data_inicio_mandato
data_inicio_legislatura = mandato.legislatura.data_inicio
days = abs((data_inicio - data_inicio_legislatura
).days) if data_inicio else 60
if days >= 60:
mandato.data_inicio_mandato = data_inicio_legislatura
mandato.save()
class Migration(migrations.Migration):
dependencies = [
# A dependencia real desse script é o arquivo 0001_initial.py, mas
# isso gera um erro (Conflicting migrations detected; multiple leaf
# nodes in the migration graph). para não ocasionar problemas de migração,
# vamos manter a ordem padrão do django.
('parlamentares', '0009_auto_20170905_1617'),
]
operations = [
migrations.RunPython(altera_data_inicio_mandato),
]

20
sapl/parlamentares/migrations/0011_auto_20171010_1433.py

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.3 on 2017-10-10 17:33
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('parlamentares', '0010_corrige_data_inicio_mandato'),
]
operations = [
migrations.AlterField(
model_name='mandato',
name='data_inicio_mandato',
field=models.DateField(null=True, verbose_name='Início do Mandato'),
),
]

25
sapl/parlamentares/migrations/0012_auto_20171020_1045.py

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.11 on 2017-10-20 12:45
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('parlamentares', '0011_auto_20171010_1433'),
]
operations = [
migrations.AlterField(
model_name='dependente',
name='nome',
field=models.CharField(max_length=150, verbose_name='Nome'),
),
migrations.AlterField(
model_name='tipodependente',
name='descricao',
field=models.CharField(max_length=150, verbose_name='Descrição'),
),
]

15
sapl/parlamentares/models.py

@ -1,7 +1,7 @@
from datetime import datetime
import reversion
from django.db import models
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from model_utils import Choices
@ -26,7 +26,7 @@ class Legislatura(models.Model):
verbose_name_plural = _('Legislaturas')
def atual(self):
current_year = datetime.now().year
current_year = timezone.now().year
return self.data_inicio.year <= current_year <= self.data_fim.year
@vigencia_atual
@ -190,6 +190,7 @@ class SituacaoMilitar(models.Model):
class Meta:
verbose_name = _('Tipo Situação Militar')
verbose_name_plural = _('Tipos Situações Militares')
ordering = ['descricao']
def __str__(self):
return self.descricao
@ -355,11 +356,12 @@ class Parlamentar(models.Model):
@reversion.register()
class TipoDependente(models.Model):
descricao = models.CharField(max_length=50, verbose_name=_('Descrição'))
descricao = models.CharField(max_length=150, verbose_name=_('Descrição'))
class Meta:
verbose_name = _('Tipo de Dependente')
verbose_name_plural = _('Tipos de Dependente')
ordering = ['descricao']
def __str__(self):
return self.descricao
@ -375,7 +377,7 @@ class Dependente(models.Model):
tipo = models.ForeignKey(TipoDependente, on_delete=models.PROTECT,
verbose_name=_('Tipo'))
parlamentar = models.ForeignKey(Parlamentar, on_delete=models.PROTECT)
nome = models.CharField(max_length=50, verbose_name=_('Nome'))
nome = models.CharField(max_length=150, verbose_name=_('Nome'))
sexo = models.CharField(
max_length=1, verbose_name=_('Sexo'), choices=SEXO_CHOICE)
data_nascimento = models.DateField(
@ -432,6 +434,7 @@ class TipoAfastamento(models.Model):
class Meta:
verbose_name = _('Tipo de Afastamento')
verbose_name_plural = _('Tipos de Afastamento')
ordering = ['descricao']
def __str__(self):
return self.descricao
@ -450,7 +453,7 @@ class Mandato(models.Model):
# TODO what is this field??????
tipo_causa_fim_mandato = models.PositiveIntegerField(blank=True, null=True)
data_inicio_mandato = models.DateField(verbose_name=_('Início do Mandato'),
blank=True,
blank=False,
null=True)
data_fim_mandato = models.DateField(verbose_name=_('Fim do Mandato'),
blank=True,
@ -486,7 +489,7 @@ class Mandato(models.Model):
self.legislatura.data_inicio,
self.legislatura.data_fim,
f.data,
f.data_desfiliacao or datetime.max.date())]
f.data_desfiliacao or timezone.datetime.max.date())]
@reversion.register()

24
sapl/parlamentares/tests/test_parlamentares.py

@ -3,7 +3,7 @@ from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
from model_mommy import mommy
from sapl.parlamentares.forms import (FrenteForm, LegislaturaForm, MandatoForm)
from sapl.parlamentares.forms import FrenteForm, LegislaturaForm, MandatoForm
from sapl.parlamentares.models import (Dependente, Filiacao, Legislatura,
Mandato, Parlamentar, Partido,
TipoDependente)
@ -128,6 +128,8 @@ def test_mandato_submit(admin_client):
kwargs={'pk': 14}),
{'parlamentar': 14, # hidden field
'legislatura': 5,
'data_inicio_mandato': \
Legislatura.objects.get(id=5).data_inicio,
'data_expedicao_diploma': '2016-03-22',
'observacao': 'Observação do mandato',
'salvar': 'salvar'},
@ -171,12 +173,14 @@ def test_mandato_form_duplicado():
Mandato.objects.create(parlamentar=parlamentar,
legislatura=legislatura,
data_expedicao_diploma='2017-07-25')
data_expedicao_diploma='2017-07-25',
data_inicio_mandato=legislatura.data_inicio,)
form = MandatoForm(data={
'parlamentar': str(parlamentar.pk),
'legislatura': str(legislatura.pk),
'data_expedicao_diploma': '01/07/2015'
'data_expedicao_diploma': '01/07/2015',
'data_inicio_mandato': legislatura.data_inicio,
})
assert not form.is_valid()
@ -225,10 +229,10 @@ def test_legislatura_form_invalido():
errors = legislatura_form.errors
errors['numero'] == [_('Este campo é obrigatório.')]
errors['data_inicio'] == [_('Este campo é obrigatório.')]
errors['data_fim'] == [_('Este campo é obrigatório.')]
errors['data_eleicao'] == [_('Este campo é obrigatório.')]
assert errors['numero'] == [_('Este campo é obrigatório.')]
assert errors['data_inicio'] == [_('Este campo é obrigatório.')]
assert errors['data_fim'] == [_('Este campo é obrigatório.')]
assert errors['data_eleicao'] == [_('Este campo é obrigatório.')]
assert len(errors) == 4
@ -244,7 +248,8 @@ def test_legislatura_form_datas_invalidas():
assert not legislatura_form.is_valid()
expected = \
_("Data eleição não pode ser inferior a data início da legislatura")
_("A data início deve ser menor que a data fim, "
"e a data eleição deve ser menor que a data início")
assert legislatura_form.errors['__all__'] == [expected]
legislatura_form = LegislaturaForm(data={'numero': '1',
@ -255,8 +260,7 @@ def test_legislatura_form_datas_invalidas():
assert not legislatura_form.is_valid()
assert legislatura_form.errors['__all__'] == \
[_("Intervalo de início e fim inválido para legislatura.")]
assert legislatura_form.errors['__all__'] == [expected]
@pytest.mark.django_db(transaction=False)

3
sapl/parlamentares/urls.py

@ -17,7 +17,7 @@ from sapl.parlamentares.views import (CargoMesaCrud, ColigacaoCrud,
frente_atualiza_lista_parlamentares,
insere_parlamentar_composicao,
parlamentares_frente_selected,
remove_parlamentar_composicao)
remove_parlamentar_composicao, MunicipioCrud)
from .apps import AppConfig
@ -60,6 +60,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/municipio/', include(MunicipioCrud.get_urls())),
url(r'^sistema/mesa-diretora/sessao-legislativa/',
include(SessaoLegislativaCrud.get_urls())),

61
sapl/parlamentares/views.py

@ -1,5 +1,5 @@
from datetime import datetime
import json
from datetime import datetime
from django.contrib import messages
from django.contrib.contenttypes.models import ContentType
@ -10,6 +10,7 @@ from django.db.models.aggregates import Count
from django.http import JsonResponse
from django.http.response import HttpResponseRedirect
from django.templatetags.static import static
from django.utils import timezone
from django.utils.datastructures import MultiValueDictKeyError
from django.utils.translation import ugettext_lazy as _
from django.views.decorators.clickjacking import xframe_options_exempt
@ -25,13 +26,12 @@ from sapl.materia.models import Autoria, Proposicao, Relatoria
from sapl.parlamentares.apps import AppConfig
from sapl.utils import parlamentares_ativos
from .forms import (FiliacaoForm, LegislaturaCreateForm, LegislaturaUpdateForm,
MandatoForm, ParlamentarCreateForm, ParlamentarForm,
VotanteForm)
from .forms import (FiliacaoForm, LegislaturaForm, MandatoForm,
ParlamentarCreateForm, ParlamentarForm, VotanteForm)
from .models import (CargoMesa, Coligacao, ComposicaoColigacao, ComposicaoMesa,
Dependente, Filiacao, Frente, Legislatura, Mandato,
NivelInstrucao, Parlamentar, Partido, SessaoLegislativa,
SituacaoMilitar, TipoAfastamento, TipoDependente, Votante)
SituacaoMilitar, TipoAfastamento, TipoDependente, Votante, Municipio)
CargoMesaCrud = CrudAux.build(CargoMesa, 'cargo_mesa')
PartidoCrud = CrudAux.build(Partido, 'partidos')
@ -40,6 +40,7 @@ TipoDependenteCrud = CrudAux.build(TipoDependente, 'tipo_dependente')
NivelInstrucaoCrud = CrudAux.build(NivelInstrucao, 'nivel_instrucao')
TipoAfastamentoCrud = CrudAux.build(TipoAfastamento, 'tipo_afastamento')
TipoMilitarCrud = CrudAux.build(SituacaoMilitar, 'tipo_situa_militar')
MunicipioCrud = CrudAux.build(Municipio, 'municipio')
DependenteCrud = MasterDetailCrud.build(
Dependente, 'parlamentar', 'dependente')
@ -89,7 +90,7 @@ class FrenteList(MasterDetailCrud):
class RelatoriaParlamentarCrud(CrudBaseForListAndDetailExternalAppView):
model = Relatoria
parent_field = 'parlamentar'
help_path = 'relatoria_parlamentar'
help_topic = 'tramitacao_relatoria'
namespace = AppConfig.name
class BaseMixin(CrudBaseForListAndDetailExternalAppView.BaseMixin):
@ -181,7 +182,7 @@ class ParticipacaoParlamentarCrud(CrudBaseForListAndDetailExternalAppView):
class ColigacaoCrud(CrudAux):
model = Coligacao
help_path = 'tabelas_auxiliares#coligacao'
help_topic = 'coligacao'
class ListView(CrudAux.ListView):
ordering = ('-numero_votos', 'nome')
@ -222,22 +223,20 @@ class ColigacaoCrud(CrudAux):
def json_date_convert(date):
'''
"""
:param date: recebe a data de uma chamada ajax no formato de
string "dd/mm/yyyy"
:return:
'''
dia, mes, ano = date.split('/')
return datetime.date(day=int(dia),
month=int(mes),
year=int(ano))
"""
return datetime.strptime(date, "%d/%m/%Y").date()
def frente_atualiza_lista_parlamentares(request):
'''
"""
:param request: recebe os parâmetros do GET da chamada Ajax
:return: retorna a lista atualizada dos parlamentares
'''
"""
ativos = json.loads(request.GET['ativos'])
parlamentares = Parlamentar.objects.all()
@ -259,9 +258,9 @@ def frente_atualiza_lista_parlamentares(request):
def parlamentares_frente_selected(request):
'''
"""
:return: Lista com o id dos parlamentares em uma frente
'''
"""
try:
frente = Frente.objects.get(id=int(request.GET['frente_id']))
except ObjectDoesNotExist:
@ -274,7 +273,7 @@ def parlamentares_frente_selected(request):
class FrenteCrud(CrudAux):
model = Frente
help_path = 'tabelas_auxiliares#tipo_situa_militar'
help_topic = 'tipo_situa_militar'
list_field_names = ['nome', 'data_criacao', 'parlamentares']
class CreateView(CrudAux.CreateView):
@ -339,7 +338,7 @@ class MandatoCrud(MasterDetailCrud):
class ComposicaoColigacaoCrud(MasterDetailCrud):
model = ComposicaoColigacao
parent_field = 'coligacao'
help_path = ''
help_topic = 'coligacao'
class BaseMixin(MasterDetailCrud.BaseMixin):
@ -355,10 +354,10 @@ class ComposicaoColigacaoCrud(MasterDetailCrud):
class LegislaturaCrud(CrudAux):
model = Legislatura
help_path = 'tabelas_auxiliares#legislatura'
help_topic = 'legislatura'
class CreateView(CrudAux.CreateView):
form_class = LegislaturaCreateForm
form_class = LegislaturaForm
def get_initial(self):
try:
@ -369,7 +368,7 @@ class LegislaturaCrud(CrudAux):
return {'numero': numero}
class UpdateView(CrudAux.UpdateView):
form_class = LegislaturaUpdateForm
form_class = LegislaturaForm
class DetailView(CrudAux.DetailView):
@ -393,7 +392,7 @@ class LegislaturaCrud(CrudAux):
class FiliacaoCrud(MasterDetailCrud):
model = Filiacao
parent_field = 'parlamentar'
help_path = ''
help_topic = 'filiacoes_partidarias'
public = [RP_LIST, RP_DETAIL]
class BaseMixin(MasterDetailCrud.BaseMixin):
@ -446,12 +445,12 @@ class ParlamentarCrud(Crud):
return 'ParlamentarCreate'
def form_valid(self, form):
'''
"""
Reimplementa form_valid devido ao save de ParlamentarCreateForm
ser específico, sendo necessário isolar padrão do crud que aciona
form.save(commit=False) para registrar dados de auditoria se
o model implementá-los, bem como de container se também implement.
'''
"""
return super(Crud.CreateView, self).form_valid(form)
class ListView(Crud.ListView):
@ -661,8 +660,8 @@ class MesaDiretoraView(FormView):
sessoes = SessaoLegislativa.objects.filter(
legislatura=legislatura).order_by("data_inicio")
today = datetime.now()
sessao_atual = sessoes.filter(data_inicio__year=today.year).first()
year = timezone.now().year
sessao_atual = sessoes.filter(data_inicio__year=year).first()
mesa = sessao_atual.composicaomesa_set.all() if sessao_atual else []
@ -716,9 +715,9 @@ def altera_field_mesa(request):
# Caso a mudança tenha sido no campo legislatura, a sessão
# atual deve ser a primeira daquela legislatura
else:
today = datetime.now()
year = timezone.now().year
try:
sessao_selecionada = sessoes.get(data_inicio__year=today.year).id
sessao_selecionada = sessoes.get(data_inicio__year=year).id
except ObjectDoesNotExist:
sessao_selecionada = sessoes.first().id
@ -884,8 +883,8 @@ def altera_field_mesa_public_view(request):
# atual deve ser a primeira daquela legislatura
else:
try:
today = datetime.now()
sessao_selecionada = sessoes.get(data_inicio__year=today.year).id
year = timezone.now().year
sessao_selecionada = sessoes.get(data_inicio__year=year).id
except ObjectDoesNotExist as e:
sessao_selecionada = sessoes.first().id

99
sapl/protocoloadm/forms.py

@ -1,17 +1,18 @@
from datetime import datetime
import django_filters
from crispy_forms.bootstrap import InlineRadios
from crispy_forms.helper import FormHelper
from crispy_forms.layout import HTML, Button, Fieldset, Layout, Submit
from crispy_forms.layout import HTML, Button, Fieldset, Layout
from django import forms
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.exceptions import (MultipleObjectsReturned,
ObjectDoesNotExist, ValidationError)
from django.db import models
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.crispy_layout_mixin import form_actions, to_row, SaplFormLayout
from sapl.crispy_layout_mixin import SaplFormLayout, form_actions, to_row
from sapl.materia.models import (MateriaLegislativa, TipoMateriaLegislativa,
UnidadeTramitacao)
from sapl.utils import (RANGE_ANOS, AnoNumeroOrderingFilter,
@ -21,7 +22,6 @@ from .models import (DocumentoAcessorioAdministrativo, DocumentoAdministrativo,
Protocolo, TipoDocumentoAdministrativo,
TramitacaoAdministrativo)
TIPOS_PROTOCOLO = [('0', 'Recebido'), ('1', 'Enviado'), ('', 'Ambos')]
TIPOS_PROTOCOLO_CREATE = [('0', 'Recebido'), ('1', 'Enviado')]
@ -123,7 +123,7 @@ class ProtocoloFilterSet(django_filters.FilterSet):
HTML(autor_label),
HTML(autor_modal),
row4, row5, row6,
form_actions(save_label='Pesquisar'))
form_actions(label='Pesquisar'))
)
@ -154,7 +154,7 @@ class DocumentoAdministrativoFilterSet(django_filters.FilterSet):
model = DocumentoAdministrativo
fields = ['tipo',
'numero',
'numero_protocolo',
'protocolo__numero',
'data',
'tramitacaoadministrativo__unidade_tramitacao_destino',
'tramitacaoadministrativo__status']
@ -173,7 +173,7 @@ class DocumentoAdministrativoFilterSet(django_filters.FilterSet):
row2 = to_row(
[('ano', 4),
('numero_protocolo', 4),
('protocolo__numero', 4),
('data', 4)])
row3 = to_row(
@ -194,7 +194,7 @@ class DocumentoAdministrativoFilterSet(django_filters.FilterSet):
Fieldset(_('Pesquisar Documento'),
row1, row2,
row3, row4, row5,
form_actions(save_label='Pesquisar'))
form_actions(label='Pesquisar'))
)
@ -276,7 +276,7 @@ class AnularProcoloAdmForm(ModelForm):
row1,
row2,
HTML("&nbsp;"),
form_actions(save_label='Anular')
form_actions(label='Anular')
)
)
super(AnularProcoloAdmForm, self).__init__(
@ -339,7 +339,7 @@ class ProtocoloDocumentForm(ModelForm):
row4,
row5,
HTML("&nbsp;"),
form_actions(save_label=_('Protocolar Documento'))
form_actions(label=_('Protocolar Documento'))
)
)
super(ProtocoloDocumentForm, self).__init__(
@ -407,7 +407,7 @@ class ProtocoloMateriaForm(ModelForm):
self.helper.layout = Layout(
Fieldset(_('Identificação da Matéria'),
row1, row3,
row4, form_actions(save_label='Protocolar Matéria')))
row4, form_actions(label='Protocolar Matéria')))
super(ProtocoloMateriaForm, self).__init__(
*args, **kwargs)
@ -442,28 +442,26 @@ class TramitacaoAdmForm(ModelForm):
'texto',
]
widgets = {
'data_tramitacao': forms.DateInput(format='%d/%m/%Y'),
'data_encaminhamento': forms.DateInput(format='%d/%m/%Y'),
'data_fim_prazo': forms.DateInput(format='%d/%m/%Y'),
}
def clean(self):
super(TramitacaoAdmForm, self).clean()
cleaned_data = super(TramitacaoAdmForm, self).clean()
data_enc_form = self.cleaned_data['data_encaminhamento']
data_prazo_form = self.cleaned_data['data_fim_prazo']
data_tram_form = self.cleaned_data['data_tramitacao']
if 'data_encaminhamento' in cleaned_data:
data_enc_form = cleaned_data['data_encaminhamento']
if 'data_fim_prazo' in cleaned_data:
data_prazo_form = cleaned_data['data_fim_prazo']
if 'data_tramitacao' in cleaned_data:
data_tram_form = cleaned_data['data_tramitacao']
if self.errors:
return self.errors
if not self.is_valid():
return cleaned_data
ultima_tramitacao = TramitacaoAdministrativo.objects.filter(
documento_id=self.instance.documento_id).exclude(
id=self.instance.id).last()
id=self.instance.id).order_by(
'-data_tramitacao',
'-id').first()
if not self.instance.data_tramitacao:
if ultima_tramitacao:
destino = ultima_tramitacao.unidade_tramitacao_destino
if (destino != self.cleaned_data['unidade_tramitacao_local']):
@ -471,7 +469,7 @@ class TramitacaoAdmForm(ModelForm):
'destino da última adicionada!')
raise ValidationError(msg)
if self.cleaned_data['data_tramitacao'] > datetime.now().date():
if self.cleaned_data['data_tramitacao'] > timezone.now().date():
msg = _(
'A data de tramitação deve ser ' +
'menor ou igual a data de hoje!')
@ -517,25 +515,33 @@ class TramitacaoAdmEditForm(TramitacaoAdmForm):
'texto',
]
widgets = {
'data_encaminhamento': forms.DateInput(format='%d/%m/%Y'),
'data_fim_prazo': forms.DateInput(format='%d/%m/%Y'),
}
def clean(self):
super(TramitacaoAdmEditForm, self).clean()
local = self.instance.unidade_tramitacao_local
data_tram = self.instance.data_tramitacao
ultima_tramitacao = TramitacaoAdministrativo.objects.filter(
documento_id=self.instance.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:
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
self.cleaned_data['data_tramitacao'] = data_tram
self.cleaned_data['unidade_tramitacao_local'] = local
return super(TramitacaoAdmEditForm, self).clean()
class DocumentoAdministrativoForm(ModelForm):
data = forms.DateField(initial=datetime.today)
data = forms.DateField(initial=timezone.now)
ano_protocolo = forms.ChoiceField(required=False,
label=Protocolo._meta.
@ -544,6 +550,10 @@ class DocumentoAdministrativoForm(ModelForm):
widget=forms.Select(
attrs={'class': 'selector'}))
numero_protocolo = forms.IntegerField(required=False,
label=Protocolo._meta.
get_field('numero').verbose_name)
class Meta:
model = DocumentoAdministrativo
fields = ['tipo',
@ -572,8 +582,8 @@ class DocumentoAdministrativoForm(ModelForm):
if not self.is_valid():
return cleaned_data
numero_protocolo = cleaned_data['numero_protocolo']
ano_protocolo = cleaned_data['ano_protocolo']
numero_protocolo = self.data['numero_protocolo']
ano_protocolo = self.data['ano_protocolo']
# campos opcionais, mas que se informados devem ser válidos
if numero_protocolo and ano_protocolo:
@ -585,6 +595,11 @@ class DocumentoAdministrativoForm(ModelForm):
msg = _('Protocolo %s/%s inexistente.' % (
numero_protocolo, ano_protocolo))
raise ValidationError(msg)
except MultipleObjectsReturned:
msg = _(
'Existe mais de um Protocolo com este ano e número.' % (
numero_protocolo, ano_protocolo))
raise ValidationError(msg)
return self.cleaned_data

19
sapl/protocoloadm/migrations/0002_remove_documentoadministrativo_numero_protocolo.py

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.3 on 2017-09-20 21:52
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('protocoloadm', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='documentoadministrativo',
name='numero_protocolo',
),
]

3
sapl/protocoloadm/models.py

@ -120,8 +120,7 @@ class DocumentoAdministrativo(models.Model):
on_delete=models.PROTECT,
verbose_name=_('Protocolo'))
data = models.DateField(verbose_name=_('Data'))
numero_protocolo = models.PositiveIntegerField(
blank=True, null=True, verbose_name=_('Núm. Protocolo'))
interessado = models.CharField(
max_length=50, blank=True, verbose_name=_('Interessado'))
autor = models.ForeignKey(Autor, blank=True, null=True,

33
sapl/protocoloadm/tests/test_protocoloadm.py

@ -1,4 +1,4 @@
import datetime
from datetime import date, timedelta
import pytest
from django.core.urlresolvers import reverse
@ -9,8 +9,7 @@ from model_mommy import mommy
from sapl.materia.models import UnidadeTramitacao
from sapl.protocoloadm.forms import (AnularProcoloAdmForm,
DocumentoAdministrativoForm,
MateriaLegislativa,
ProtocoloDocumentForm,
MateriaLegislativa, ProtocoloDocumentForm,
ProtocoloMateriaForm)
from sapl.protocoloadm.models import (DocumentoAdministrativo, Protocolo,
StatusTramitacaoAdministrativo,
@ -146,7 +145,7 @@ def test_create_tramitacao(admin_client):
unidade_tramitacao_destino=unidade_tramitacao_destino_1,
status=status,
documento=documento_adm,
data_tramitacao=datetime.date(2016, 8, 21))
data_tramitacao=date(2016, 8, 21))
response = admin_client.post(
reverse(
@ -156,7 +155,7 @@ def test_create_tramitacao(admin_client):
'unidade_tramitacao_destino': unidade_tramitacao_local_1.pk,
'documento': documento_adm.pk,
'status': status.pk,
'data_tramitacao': datetime.date(2016, 8, 21)},
'data_tramitacao': date(2016, 8, 21)},
follow=True)
msg = force_text(_('A origem da nova tramitação deve ser igual ao '
@ -174,7 +173,7 @@ def test_create_tramitacao(admin_client):
'unidade_tramitacao_destino': unidade_tramitacao_destino_2.pk,
'documento': documento_adm.pk,
'status': status.pk,
'data_tramitacao': datetime.date(2016, 8, 20)},
'data_tramitacao': date(2016, 8, 20)},
follow=True)
msg = _('A data da nova tramitação deve ser ' +
@ -192,7 +191,7 @@ def test_create_tramitacao(admin_client):
'unidade_tramitacao_destino': unidade_tramitacao_destino_2.pk,
'documento': documento_adm.pk,
'status': status.pk,
'data_tramitacao': datetime.date.today() + datetime.timedelta(
'data_tramitacao': date.today() + timedelta(
days=1)},
follow=True)
@ -211,8 +210,8 @@ def test_create_tramitacao(admin_client):
'unidade_tramitacao_destino': unidade_tramitacao_destino_2.pk,
'documento': documento_adm.pk,
'status': status.pk,
'data_tramitacao': datetime.date(2016, 8, 21),
'data_encaminhamento': datetime.date(2016, 8, 20)},
'data_tramitacao': date(2016, 8, 21),
'data_encaminhamento': date(2016, 8, 20)},
follow=True)
msg = force_text(_('A data de encaminhamento deve ser ' +
@ -230,8 +229,8 @@ def test_create_tramitacao(admin_client):
'unidade_tramitacao_destino': unidade_tramitacao_destino_2.pk,
'documento': documento_adm.pk,
'status': status.pk,
'data_tramitacao': datetime.date(2016, 8, 21),
'data_fim_prazo': datetime.date(2016, 8, 20)},
'data_tramitacao': date(2016, 8, 21),
'data_fim_prazo': date(2016, 8, 20)},
follow=True)
msg = _('A data fim de prazo deve ser ' +
@ -249,12 +248,12 @@ def test_create_tramitacao(admin_client):
'unidade_tramitacao_destino': unidade_tramitacao_destino_2.pk,
'documento': documento_adm.pk,
'status': status.pk,
'data_tramitacao': datetime.date(2016, 8, 21)},
'data_tramitacao': date(2016, 8, 21)},
follow=True)
tramitacao = TramitacaoAdministrativo.objects.last()
# Verifica se a tramitacao que obedece as regras de negócios é criada
assert tramitacao.data_tramitacao == datetime.date(2016, 8, 21)
assert tramitacao.data_tramitacao == date(2016, 8, 21)
@pytest.mark.django_db(transaction=False)
@ -288,7 +287,7 @@ def test_anular_protocolo_form_anula_protocolo_inexistente():
@pytest.mark.django_db(transaction=False)
def test_anular_protocolo_form_anula_protocolo_anulado():
protocolo = mommy.make(Protocolo, numero=1, ano=2017, anulado=True)
mommy.make(Protocolo, numero=1, ano=2017, anulado=True)
form = AnularProcoloAdmForm(data={'numero': '1',
'ano': '2017',
@ -305,13 +304,13 @@ def test_anular_protocolo_form_anula_protocolo_anulado():
def test_anular_protocolo_form_anula_protocolo_com_doc_vinculado():
tipo_materia = mommy.make(TipoMateriaLegislativa)
protocolo_materia = mommy.make(Protocolo,
mommy.make(Protocolo,
numero=1,
ano=2017,
tipo_materia=tipo_materia,
anulado=False)
materia_legislativa = mommy.make(MateriaLegislativa,
mommy.make(MateriaLegislativa,
ano=2017,
numero_protocolo=1)
@ -334,7 +333,7 @@ def test_anular_protocolo_form_anula_protocolo_com_doc_vinculado():
tipo_documento=tipo_documento,
anulado=False)
documento_administrativo = mommy.make(DocumentoAdministrativo,
mommy.make(DocumentoAdministrativo,
protocolo=protocolo_documento)
form = AnularProcoloAdmForm(data={'numero': '2',

122
sapl/protocoloadm/views.py

@ -1,4 +1,3 @@
from datetime import date, datetime
from braces.views import FormValidMessageMixin
from django.contrib import messages
@ -8,21 +7,22 @@ from django.core.exceptions import ObjectDoesNotExist
from django.core.urlresolvers import reverse
from django.db.models import Max, Q
from django.http import Http404, HttpResponse, JsonResponse
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 CreateView, ListView
from django.views.generic.base import TemplateView, RedirectView
from django.views.generic.base import RedirectView, TemplateView
from django_filters.views import FilterView
import sapl
from sapl.base.models import Autor
from sapl.comissoes.models import Comissao
from sapl.crud.base import Crud, CrudAux, MasterDetailCrud, make_pagination
from sapl.materia.models import MateriaLegislativa, TipoMateriaLegislativa
from sapl.parlamentares.models import Legislatura, Parlamentar
from sapl.protocoloadm.models import Protocolo
from sapl.utils import create_barcode, get_client_ip
import sapl
from sapl.utils import create_barcode, get_client_ip, show_results_filter_set, get_mime_type_from_file_extension
from .forms import (AnularProcoloAdmForm, DocumentoAcessorioAdministrativoForm,
DocumentoAdministrativoFilterSet,
DocumentoAdministrativoForm, ProtocoloDocumentForm,
@ -32,7 +32,6 @@ from .models import (DocumentoAcessorioAdministrativo, DocumentoAdministrativo,
StatusTramitacaoAdministrativo,
TipoDocumentoAdministrativo, TramitacaoAdministrativo)
TipoDocumentoAdministrativoCrud = CrudAux.build(
TipoDocumentoAdministrativo, '')
@ -55,12 +54,7 @@ def doc_texto_integral(request, pk):
if documento.texto_integral:
arquivo = documento.texto_integral
ext = arquivo.name.split('.')[-1]
mime = ''
if ext == 'odt':
mime = 'application/vnd.oasis.opendocument.text'
else:
mime = "application/%s" % (ext,)
mime = get_mime_type_from_file_extension(arquivo.name)
with open(arquivo.path, 'rb') as f:
data = f.read()
@ -84,11 +78,11 @@ class DocumentoAdministrativoMixin:
class DocumentoAdministrativoCrud(Crud):
model = DocumentoAdministrativo
help_path = ''
help_topic = 'numeracao_docsacess'
class BaseMixin(Crud.BaseMixin):
list_field_names = ['tipo', 'numero', 'ano', 'data',
'numero_protocolo', 'assunto',
'protocolo__numero', 'assunto',
'interessado', 'tramitacao', 'texto_integral']
@property
@ -142,7 +136,7 @@ class DocumentoAdministrativoCrud(Crud):
class StatusTramitacaoAdministrativoCrud(CrudAux):
model = StatusTramitacaoAdministrativo
help_path = ''
help_topic = 'status_tramitacao'
class BaseMixin(CrudAux.BaseMixin):
list_field_names = ['sigla', 'indicador', 'descricao']
@ -163,9 +157,7 @@ class ProtocoloPesquisaView(PermissionRequiredMixin, FilterView):
kwargs = {'data': self.request.GET or None}
qs = self.get_queryset().order_by('ano', 'numero')
qs = qs.distinct()
qs = self.get_queryset().order_by('ano', 'numero').distinct()
if 'o' in self.request.GET and not self.request.GET['o']:
qs = qs.order_by('-ano', '-numero')
@ -197,7 +189,7 @@ class ProtocoloPesquisaView(PermissionRequiredMixin, FilterView):
# Provavelmente você criou um novo campo no Form/FilterSet
# Então a ordem da URL está diferente
data = self.filterset.data
if (data and data.get('numero') is not None):
if data and data.get('numero') is not None:
url = "&" + str(self.request.environ['QUERY_STRING'])
if url.startswith("&page"):
ponto_comeco = url.find('numero=') - 1
@ -213,6 +205,9 @@ class ProtocoloPesquisaView(PermissionRequiredMixin, FilterView):
numero_res=len(self.object_list)
)
context['show_results'] = show_results_filter_set(
self.request.GET.copy())
return self.render_to_response(context)
@ -293,7 +288,7 @@ class ProtocoloDocumentoView(PermissionRequiredMixin,
if numeracao == 'A':
numero = Protocolo.objects.filter(
ano=date.today().year).aggregate(Max('numero'))
ano=timezone.now().year).aggregate(Max('numero'))
elif numeracao == 'L':
legislatura = Legislatura.objects.last()
data_inicio = legislatura.data_inicio
@ -307,10 +302,10 @@ class ProtocoloDocumentoView(PermissionRequiredMixin,
f.tipo_processo = '0' # TODO validar o significado
f.anulado = False
f.numero = (numero['numero__max'] + 1) if numero['numero__max'] else 1
f.ano = datetime.now().year
f.data = datetime.now().date()
f.hora = datetime.now().time()
f.timestamp = datetime.now()
f.ano = timezone.now().year
f.data = timezone.now()
f.hora = timezone.now().time()
f.timestamp = timezone.now()
f.assunto_ementa = self.request.POST['assunto']
f.save()
@ -332,7 +327,7 @@ class CriarDocumentoProtocolo(PermissionRequiredMixin, CreateView):
kwargs={'pk': self.kwargs['pk']})
def criar_documento(self, protocolo):
curr_year = datetime.now().date().year
curr_year = timezone.now().year
numero_max = DocumentoAdministrativo.objects.filter(
tipo=protocolo.tipo_documento, ano=curr_year
@ -341,7 +336,7 @@ class CriarDocumentoProtocolo(PermissionRequiredMixin, CreateView):
doc = {}
doc['tipo'] = protocolo.tipo_documento
doc['ano'] = curr_year
doc['data'] = datetime.today()
doc['data'] = timezone.now()
doc['numero_protocolo'] = protocolo.numero
doc['ano_protocolo'] = protocolo.ano
doc['protocolo'] = protocolo.id
@ -428,7 +423,7 @@ class ProtocoloMateriaView(PermissionRequiredMixin, CreateView):
if numeracao == 'A':
numero = Protocolo.objects.filter(
ano=date.today().year).aggregate(Max('numero'))
ano=timezone.now().year).aggregate(Max('numero'))
elif numeracao == 'U':
numero = Protocolo.objects.all().aggregate(Max('numero'))
@ -439,10 +434,10 @@ class ProtocoloMateriaView(PermissionRequiredMixin, CreateView):
protocolo.numero = (
numero['numero__max'] + 1) if numero['numero__max'] else 1
protocolo.ano = datetime.now().year
protocolo.data = datetime.now().date()
protocolo.hora = datetime.now().time()
protocolo.timestamp = datetime.now()
protocolo.ano = timezone.now().year
protocolo.data = timezone.now().date()
protocolo.hora = timezone.now().time()
protocolo.timestamp = timezone.now()
protocolo.tipo_protocolo = 0
protocolo.tipo_processo = '1' # TODO validar o significado
@ -480,7 +475,7 @@ class ProtocoloMateriaView(PermissionRequiredMixin, CreateView):
lista_comissoes = Comissao.objects.filter(Q(
data_extincao__isnull=True) | Q(
data_extincao__gt=date.today())).values_list('id', flat=True)
data_extincao__gt=timezone.now())).values_list('id', flat=True)
model_comissao = ContentType.objects.get_for_model(Comissao)
autor_comissoes = Autor.objects.filter(
content_type=model_comissao, object_id__in=lista_comissoes)
@ -549,7 +544,7 @@ class PesquisarDocumentoAdministrativoView(DocumentoAdministrativoMixin,
# Provavelmente você criou um novo campo no Form/FilterSet
# Então a ordem da URL está diferente
data = self.filterset.data
if (data and data.get('tipo') is not None):
if data and data.get('tipo') is not None:
url = "&" + str(self.request.environ['QUERY_STRING'])
if url.startswith("&page"):
ponto_comeco = url.find('tipo=') - 1
@ -565,13 +560,16 @@ class PesquisarDocumentoAdministrativoView(DocumentoAdministrativoMixin,
numero_res=len(self.object_list)
)
context['show_results'] = show_results_filter_set(
self.request.GET.copy())
return self.render_to_response(context)
class TramitacaoAdmCrud(MasterDetailCrud):
model = TramitacaoAdministrativo
parent_field = 'documento'
help_path = ''
help_topic = 'unidade_tramitacao'
class BaseMixin(MasterDetailCrud.BaseMixin):
list_field_names = ['data_tramitacao', 'unidade_tramitacao_local',
@ -580,6 +578,33 @@ class TramitacaoAdmCrud(MasterDetailCrud):
class CreateView(MasterDetailCrud.CreateView):
form_class = TramitacaoAdmForm
def get_initial(self):
local = DocumentoAdministrativo.objects.get(
pk=self.kwargs['pk']).tramitacaoadministrativo_set.order_by(
'-data_tramitacao',
'-id').first()
if local:
self.initial['unidade_tramitacao_local'
] = local.unidade_tramitacao_destino.pk
else:
self.initial['unidade_tramitacao_local'] = ''
self.initial['data_tramitacao'] = timezone.now().date()
return self.initial
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
primeira_tramitacao = not(TramitacaoAdministrativo.objects.filter(
documento_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
class UpdateView(MasterDetailCrud.UpdateView):
form_class = TramitacaoAdmEditForm
@ -588,17 +613,42 @@ class TramitacaoAdmCrud(MasterDetailCrud):
def get_queryset(self):
qs = super(MasterDetailCrud.ListView, self).get_queryset()
kwargs = {self.crud.parent_field: self.kwargs['pk']}
return qs.filter(**kwargs).order_by('-data_tramitacao', '-id')
return qs.filter(**kwargs).order_by('-data_tramitacao',
'-id')
class DetailView(DocumentoAdministrativoMixin,
MasterDetailCrud.DetailView):
pass
class DeleteView(MasterDetailCrud.DeleteView):
def delete(self, request, *args, **kwargs):
tramitacao = TramitacaoAdministrativo.objects.get(
id=self.kwargs['pk'])
documento = DocumentoAdministrativo.objects.get(
id=tramitacao.documento.id)
url = reverse(
'sapl.protocoloadm:tramitacaoadministrativo_list',
kwargs={'pk': tramitacao.documento.id})
ultima_tramitacao = \
documento.tramitacaoadministrativo_set.order_by(
'-data_tramitacao',
'-id').first()
if 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()
return HttpResponseRedirect(url)
class DocumentoAcessorioAdministrativoCrud(MasterDetailCrud):
model = DocumentoAcessorioAdministrativo
parent_field = 'documento'
help_path = ''
help_topic = 'numeracao_docsacess'
class BaseMixin(MasterDetailCrud.BaseMixin):
list_field_names = ['nome', 'tipo',

4
sapl/relatorios/templates/pdf_sessao_plenaria_gerar.py

@ -243,8 +243,8 @@ def votacao(lst_votacao):
tmp += '<blockTable style="repeater" repeatRows="1">\n'
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>Autor: </b>' + votacao['nom_autor'] + '</para></td>\n'
tmp += '<tr><td><para style="P3"><b>' + str(votacao['num_ordem']) + '</b> - ' + votacao['id_materia'] + '</para>\n' + '<para style="P3"><b>Turno:</b> ' + str(votacao[
'des_turno']) + '</para>\n' + '<para style="P3"><b>Autor: </b>' + str(votacao['nom_autor']) + '</para></td>\n'
txt_ementa = votacao['txt_ementa'].replace('&', '&amp;')
tmp += '<td><para style="P4">' + txt_ementa + '</para></td>\n'
tmp += '<td><para style="P3"><b>' + \

126
sapl/relatorios/views.py

@ -1,9 +1,10 @@
import html
import re
from datetime import datetime
from datetime import datetime as dt
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 sapl.base.models import Autor, CasaLegislativa
@ -88,7 +89,7 @@ def get_rodape(casa):
linha2 = linha2 + " - "
linha2 = linha2 + str(_("E-mail: ")) + casa.email
data_emissao = datetime.today().strftime("%d/%m/%Y")
data_emissao = dt.strftime(timezone.now(), "%d/%m/%Y")
return [linha1, linha2, data_emissao]
@ -562,21 +563,9 @@ def get_sessao_plenaria(sessao, casa):
str(numeracao.numero_materia) + '/' + str(
numeracao.ano_materia))
dic_expediente_materia["des_turno"] = ' '
tram = Tramitacao.objects.filter(
materia=materia).first()
if tram is not None:
if tram.turno != '':
for turno in [("P", _("Primeiro")),
("S", _("Segundo")),
("U", _("Único")),
("L", _("Suplementar")),
("A", _("Votação Única em Regime de Urgência")),
("B", _("1ª Votação")),
("C", _("2ª e 3ª Votações")),
("F", "Final")]:
if tram.turno == turno[0]:
dic_expediente_materia["des_turno"] = turno[1]
turno, _ = get_turno(dic_expediente_materia, materia, sessao.data_inicio)
dic_expediente_materia["des_turno"] = turno
dic_expediente_materia["txt_ementa"] = str(materia.ementa)
dic_expediente_materia["ordem_observacao"] = ' ' # TODO
@ -614,8 +603,8 @@ def get_sessao_plenaria(sessao, casa):
dic_expediente_materia["votacao_observacao"] = (
i.observacao)
else:
dic_expediente_materia["nom_resultado"] = _("Matéria não votada")
dic_expediente_materia["votacao_observacao"] = _(" ")
dic_expediente_materia["nom_resultado"] = 'Matéria não votada'
dic_expediente_materia["votacao_observacao"] = ' '
lst_expediente_materia.append(dic_expediente_materia)
# Lista dos oradores do Expediente
@ -676,22 +665,10 @@ def get_sessao_plenaria(sessao, casa):
str(numeracao.numero_materia) +
'/' +
str(numeracao.ano_materia))
dic_votacao["des_turno"] = ' '
tramitacao = Tramitacao.objects.filter(
materia=materia).first()
if tramitacao is not None:
if not tramitacao.turno:
for turno in [("P", _("Primeiro")),
("S", _("Segundo")),
("U", _("Único")),
("L", _("Suplementar")),
("F", _("Final")),
("A", _("Votação Única em Regime de Urgência")),
("B", _("1ª Votação")),
("C", _("2ª e 3ª Votações"))]:
if tramitacao.turno == turno[0]:
dic_votacao["des_turno"] = turno[1]
turno, _ = get_turno(dic_votacao, materia, sessao.data_inicio)
dic_votacao["des_turno"] = turno
# https://github.com/interlegis/sapl/issues/1009
dic_votacao["txt_ementa"] = html.unescape(materia.ementa)
@ -727,8 +704,8 @@ def get_sessao_plenaria(sessao, casa):
if votacao.observacao:
dic_votacao["votacao_observacao"] = i.observacao
else:
dic_votacao["nom_resultado"] = _("Matéria não votada")
dic_votacao["votacao_observacao"] = _(" ")
dic_votacao["nom_resultado"] = "Matéria não votada"
dic_votacao["votacao_observacao"] = " "
lst_votacao.append(dic_votacao)
# Lista dos oradores nas Explicações Pessoais
@ -760,6 +737,28 @@ def get_sessao_plenaria(sessao, casa):
lst_oradores)
def get_turno(dic, materia, sessao_data_inicio):
descricao_turno = ' '
descricao_tramitacao = ' '
tramitacao = Tramitacao.objects.filter(materia=materia,
turno__isnull=False,
data_tramitacao__lte=sessao_data_inicio,
).exclude(turno__exact=''
).select_related(
'materia',
'status',
'materia__tipo').order_by(
'-data_tramitacao'
).first()
if tramitacao is not None:
for t in Tramitacao.TURNO_CHOICES:
if t[0] == tramitacao.turno:
descricao_turno = t[1]
break
descricao_tramitacao = tramitacao.status.descricao if tramitacao.status else ' '
return (descricao_turno, descricao_tramitacao)
def relatorio_sessao_plenaria(request, pk):
'''
pdf_sessao_plenaria_gerar.py
@ -932,8 +931,11 @@ def get_etiqueta_protocolos(prots):
dic = {}
dic['titulo'] = str(p.numero) + '/' + str(p.ano)
tz_hora = timezone.localtime(p.timestamp)
dic['data'] = '<b>Data: </b>' + p.data.strftime(
"%d/%m/%Y") + ' - <b>Horário: </b>' + p.hora.strftime("%H:%M")
"%d/%m/%Y") + ' - <b>Horário: </b>' + tz_hora.strftime("%H:%M")
dic['txt_assunto'] = p.assunto_ementa
dic['txt_interessado'] = p.interessado
@ -952,7 +954,7 @@ def get_etiqueta_protocolos(prots):
dic['num_documento'] = ''
for documento in DocumentoAdministrativo.objects.filter(
numero_protocolo=p.numero):
protocolo=p):
dic['num_documento'] = str(documento)
dic['ident_processo'] = dic['num_materia'] or dic['num_documento']
@ -1060,28 +1062,12 @@ def get_pauta_sessao(sessao, casa):
elif autoria is None:
dic_expediente_materia["nom_autor"] = 'Desconhecido'
dic_expediente_materia["des_turno"] = ' '
dic_expediente_materia["des_situacao"] = ' '
turno, tramitacao = get_turno(dic_expediente_materia, materia, sessao.data_inicio)
dic_expediente_materia["des_turno"] = turno
dic_expediente_materia["des_situacao"] = tramitacao
tramitacao = Tramitacao.objects.filter(materia=materia)
if tramitacao is not None:
tramitacao = tramitacao.first()
if tramitacao.turno != '':
for turno in [("P", _("Primeiro")),
("S", _("Segundo")),
("U", _("Único")),
("F", _("Final")),
("L", _("Suplementar")),
("A", _("Votação Única em Regime de Urgência")),
("B", _("1ª Votação")),
("C", _("2ª e 3ª Votações"))]:
if tramitacao.turno == turno.first():
dic_expediente_materia["des_turno"] = turno.first()
dic_expediente_materia["des_situacao"] = tramitacao.status
if dic_expediente_materia["des_situacao"] is None:
dic_expediente_materia["des_situacao"] = ' '
lst_expediente_materia.append(dic_expediente_materia)
lst_votacao = []
@ -1125,25 +1111,9 @@ def get_pauta_sessao(sessao, casa):
elif autoria is None:
dic_votacao["nom_autor"] = 'Desconhecido'
dic_votacao["des_turno"] = ' '
dic_votacao["des_situacao"] = ' '
tramitacao = Tramitacao.objects.filter(materia=materia)
if tramitacao is not None:
tramitacao = tramitacao.first()
if tramitacao.turno != '':
for turno in [("P", _("Primeiro")),
("S", _("Segundo")),
("U", _("Único")),
("L", _("Suplementar")),
("A", _("Votação Única em Regime de Urgência")),
("B", _("1ª Votação")),
("C", _("2ª e 3ª Votações"))]:
if tramitacao.turno == turno.first():
dic_votacao["des_turno"] = turno.first()
dic_votacao["des_situacao"] = tramitacao.status
if dic_votacao["des_situacao"] is None:
dic_votacao["des_situacao"] = ' '
turno, tramitacao = get_turno(dic_expediente_materia, materia, sessao.data_inicio)
dic_votacao["des_turno"] = turno
dic_votacao["des_situacao"] = tramitacao
lst_votacao.append(dic_votacao)
return (lst_expediente_materia,

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

Loading…
Cancel
Save