diff --git a/Dockerfile b/Dockerfile index 015357a40..9c33fcd0f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -39,6 +39,7 @@ RUN rm -rf /var/interlegis/sapl/sapl/.env && \ rm -rf /var/interlegis/sapl/sapl.db RUN chmod +x /var/interlegis/sapl/start.sh && \ + chmod +x /var/interlegis/sapl/check_solr.sh && \ ln -sf /dev/stdout /var/log/nginx/access.log && \ ln -sf /dev/stderr /var/log/nginx/error.log && \ mkdir /var/log/sapl/ && touch /var/interlegis/sapl/sapl.log && \ diff --git a/check_solr.sh b/check_solr.sh index b3c4760c4..c600466bb 100644 --- a/check_solr.sh +++ b/check_solr.sh @@ -4,15 +4,22 @@ SOLR_URL=$1 +RETRY_COUNT=1 +RETRY_LIMIT=4 + echo "Waiting for solr connection at $SOLR_URL ..." -while true; do +while [[ $RETRY_COUNT < $RETRY_LIMIT ]]; do + echo "Attempt to connect to solr: $RETRY_COUNT of $RETRY_LIMIT" + let RETRY_COUNT=RETRY_COUNT+1; echo "$SOLR_URL/solr/admin/collections?action=LIST" RESULT=$(curl -s -o /dev/null -I "$SOLR_URL/solr/admin/collections?action=LIST" -w '%{http_code}') echo $RESULT - if [ "$RESULT" -eq '200' ]; then + if [ $RESULT == 200 ]; then echo "Solr server is up!" - break + exit 1 else sleep 3 fi done +echo "Solr connection failed." +exit 2 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index e2dfb6824..ca1e573fa 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,7 +11,8 @@ sapldb: ports: - "5432:5432" sapl: - image: interlegis/sapl:3.1.153 + image: interlegis/sapl:3.1.155 +# build: . restart: always environment: ADMIN_PASSWORD: interlegis @@ -23,11 +24,27 @@ sapl: EMAIL_HOST_USER: usuariosmtp EMAIL_SEND_USER: usuariosmtp EMAIL_HOST_PASSWORD: senhasmtp +# USE_SOLR: 'True' +# SOLR_COLLECTION: sapl +# SOLR_URL: http://saplsolr:8983 TZ: America/Sao_Paulo volumes: - sapl_data:/var/interlegis/sapl/data - sapl_media:/var/interlegis/sapl/media links: - sapldb +# - saplsolr ports: - "80:80" + +#saplsolr: +# image: solr:7.4-alpine +# restart: always +# command: bin/solr start -c -f +# volumes: +# - solr_data:/opt/solr/server/solr +# - solr_configsets:/opt/solr/server/solr/configsets +# ports: +# - "8983:8983" + + diff --git a/docs/solr.rst b/docs/solr.rst index ae8bbdb63..13bdbf02c 100644 --- a/docs/solr.rst +++ b/docs/solr.rst @@ -2,6 +2,8 @@ Instruções para instalar o Solr ================================ +**O servidor do Solr NÃO DEVE SER EXPOSTO NA INTERNET. Assim como o servidor de bancos de dados Postgres ele deve estar acessível pelo SAPL na rede interna (atrás de NATs/firewalls/proxies/etc).** + Solr é uma plataforma open source de indexação e busca textual utilizada pelo SAPL 3.1 para indexar documentos (normas jurídicas, matérias legislativas e documentos acessórios). Observação: Se a execução do SAPL for mediante containers Docker então use o arquivo *docker-compose.yml* disponível em @@ -45,11 +47,42 @@ Observação: Se a execução do SAPL for mediante containers Docker então use 8) Enquanto o Solr realiza a indexação da base de dados do SAPL, inicie em uma outra tela o SAPL; 9) Após realizados os passos com sucesso, nas telas de busca de Matéria Legislativa e Normas deverá aparecer um botão -de 'Busca Textual' próximo ao botão de busca tradicional. +de 'Pesquisa Textual' na tela de busca tradicional. **Observações:** * Para parar o Solr execute o comando **$SOLR_HOME/bin/solr stop** -* Para reindexar os dados do SAPL execute o comando **python3 manage.py rebuild_index** (isso irá apagar todos os dados -do Solr e indexar tudo novamente). + +* Comandos de manutenção da base textual do Solr: + +1. **python3 manage.py rebuild_index** : Apaga os dados da coleção `sapl` no Solr e reindexa tudo do início; + +2. **python3 manage.py clear_index** : Apaga todos os dados da coleção `sapl` do Solr. **Este comando não irá apagar os dados do BD Postgres, somente os dados do Solr serão apagados.** + +3. **python3 manage.py update_index** : atualiza os dados do Solr: + +3.1. **python3 manage.py update_index --remove** : remove objetos do Solr que não mais existem no BD Postgres (no caso do Postgres e Solr derem dessincronizados). + +3.2. **python3 manage.py update_index --age ** : reindexa os documentos inseridos/alterados nas últimas horas; + +3.3. **python3 manage.py update_index -s YYYY-MM-DDTHH:MM:SS -e YYYY-MM-DDTHH:MM:SS** : reindexa os documentos que foram inseridos/atualizados entre a data inicial (-s) e a data final (-e). Ambos os argumentos de início e fim são opcionais. + + +### FAQ + +1. Uma dúvida quanto a indexação do Solr, pelo que entendi de tempos e tempos tenho que rodar o comando para poder indexar novos arquivos certo? + + Errado. Cada novo documento inserido, atualizado, ou removido do SAPL dispara uma nova indexação somente daquele documento no Solr automaticamente. + +2. O comando **python3 solr_api.py -c sapl -u http://localhost:8983** indexa os novos arquivos? + + Não. Este comando é para construir a coleção do Solr a primeira vez e, por acaso, faz a indexação inicial. Não deve ser usado se a coleção já foi criada. + +3. Ou teria que reindexar do zero com *rebuild_index*? + + Pode acontecer do Postgres e o Solr se dessincronizarem (ex: o Solr ficou fora do ar por um dia e foram inseridos registros no SAPL). Ou por algum motivo se deseja refazer o índice do Solr. Neste caso pode-se refazer a indexação no Solr com o comando : **python3 manage.py rebuild_index** (direto na linha de comando, a partir da pasta raiz do SAPL). Mas existem maneiras de atualizar somente os documentos inseridos/alterados a partir de uma determinada data ao invés de atualizar tudo do zero de novo. + +4. Pergunto isso pois estou querendo criar um script para crontab para indexar esses novos arquivos + +Desnecessário. diff --git a/sapl/api/views.py b/sapl/api/views.py index 2eedb9464..13045d6bb 100644 --- a/sapl/api/views.py +++ b/sapl/api/views.py @@ -26,8 +26,8 @@ from sapl.materia.models import Proposicao, TipoMateriaLegislativa,\ MateriaLegislativa, Tramitacao from sapl.parlamentares.models import Parlamentar from sapl.protocoloadm.models import DocumentoAdministrativo,\ - DocumentoAcessorioAdministrativo, TramitacaoAdministrativo -from sapl.sessao.models import SessaoPlenaria + DocumentoAcessorioAdministrativo, TramitacaoAdministrativo, Anexado +from sapl.sessao.models import SessaoPlenaria, ExpedienteSessao from sapl.utils import models_with_gr_for_model, choice_anos_com_sessaoplenaria @@ -489,6 +489,20 @@ class _TramitacaoAdministrativoViewSet(BusinessRulesNotImplementedMixin): return qs +@customize(Anexado) +class _AnexadoViewSet(BusinessRulesNotImplementedMixin): + + permission_classes = ( + _DocumentoAdministrativoViewSet.DocumentoAdministrativoPermission, ) + + def get_queryset(self): + qs = super().get_queryset() + + if self.request.user.is_anonymous(): + qs = qs.exclude(documento__restrito=True) + return qs + + @customize(SessaoPlenaria) class _SessaoPlenariaViewSet: @@ -498,3 +512,17 @@ class _SessaoPlenariaViewSet: serializer = ChoiceSerializer(years, many=True) return Response(serializer.data) + + @action(detail=True) + def expedientes(self, request, *args, **kwargs): + + sessao = self.get_object() + + page = self.paginate_queryset(sessao.expedientesessao_set.all()) + if page is not None: + serializer = SaplApiViewSetConstrutor.get_class_for_model( + ExpedienteSessao).serializer_class(page, many=True) + return self.get_paginated_response(serializer.data) + + serializer = self.get_serializer(page, many=True) + return Response(serializer.data) diff --git a/sapl/base/forms.py b/sapl/base/forms.py index 78ceb96b4..b04fddbd6 100644 --- a/sapl/base/forms.py +++ b/sapl/base/forms.py @@ -1203,7 +1203,8 @@ class ConfiguracoesAppForm(ModelForm): class Meta: model = AppConfig fields = ['documentos_administrativos', - 'sequencia_numeracao', + 'sequencia_numeracao_protocolo', + 'sequencia_numeracao_proposicao', 'esfera_federacao', # 'painel_aberto', # TODO: a ser implementado na versão 3.2 'texto_articulado_proposicao', diff --git a/sapl/base/migrations/0033_auto_20190415_1050.py b/sapl/base/migrations/0033_auto_20190415_1050.py new file mode 100644 index 000000000..8c21764f3 --- /dev/null +++ b/sapl/base/migrations/0033_auto_20190415_1050.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-04-15 13:50 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0032_merge_20190219_0941'), + ] + + operations = [ + migrations.AlterField( + model_name='appconfig', + name='sequencia_numeracao', + field=models.CharField(choices=[('A', 'Sequencial por ano para cada autor'), ('B', 'Sequencial por ano indepententemente do autor'), ('L', 'Sequencial por legislatura'), ('U', 'Sequencial único')], default='A', max_length=1, verbose_name='Sequência de numeração'), + ), + ] diff --git a/sapl/base/migrations/0034_auto_20190417_0941.py b/sapl/base/migrations/0034_auto_20190417_0941.py new file mode 100644 index 000000000..01d7347b4 --- /dev/null +++ b/sapl/base/migrations/0034_auto_20190417_0941.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-04-17 12:41 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0033_auto_20190415_1050'), + ] + + operations = [ + migrations.RemoveField( + model_name='appconfig', + name='sequencia_numeracao', + ), + migrations.AddField( + model_name='appconfig', + name='sequencia_numeracao_proposicao', + field=models.CharField(choices=[('A', 'Sequencial por ano para cada autor'), ('B', 'Sequencial por ano indepententemente do autor'), ('L', 'Sequencial por legislatura'), ('U', 'Sequencial único')], default='A', max_length=1, verbose_name='Sequência de numeração de proposições'), + ), + migrations.AddField( + model_name='appconfig', + name='sequencia_numeracao_protocolo', + field=models.CharField(choices=[('A', 'Sequencial por ano para cada autor'), ('L', 'Sequencial por legislatura'), ('U', 'Sequencial único')], default='A', max_length=1, verbose_name='Sequência de numeração de protocolos'), + ), + ] diff --git a/sapl/base/migrations/0035_auto_20190417_1009.py b/sapl/base/migrations/0035_auto_20190417_1009.py new file mode 100644 index 000000000..ba76ca4d0 --- /dev/null +++ b/sapl/base/migrations/0035_auto_20190417_1009.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-04-17 13:09 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0034_auto_20190417_0941'), + ] + + operations = [ + migrations.AlterField( + model_name='appconfig', + name='sequencia_numeracao_proposicao', + field=models.CharField(choices=[('A', 'Sequencial por ano para cada autor'), ('B', 'Sequencial por ano indepententemente do autor')], default='A', max_length=1, verbose_name='Sequência de numeração de proposições'), + ), + ] diff --git a/sapl/base/migrations/0036_auto_20190417_1432.py b/sapl/base/migrations/0036_auto_20190417_1432.py new file mode 100644 index 000000000..47720668a --- /dev/null +++ b/sapl/base/migrations/0036_auto_20190417_1432.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-04-17 17:32 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0035_auto_20190417_1009'), + ] + + operations = [ + migrations.AlterField( + model_name='appconfig', + name='sequencia_numeracao_protocolo', + field=models.CharField(choices=[('A', 'Sequencial por ano'), ('L', 'Sequencial por legislatura'), ('U', 'Sequencial único')], default='A', max_length=1, verbose_name='Sequência de numeração de protocolos'), + ), + ] diff --git a/sapl/base/models.py b/sapl/base/models.py index 87e497e84..eea9241d9 100644 --- a/sapl/base/models.py +++ b/sapl/base/models.py @@ -18,10 +18,13 @@ TIPO_DOCUMENTO_ADMINISTRATIVO = ((DOC_ADM_OSTENSIVO, _('Ostensiva')), RELATORIO_ATOS_ACESSADOS = (('S', _('Sim')), ('N', _('Não'))) -SEQUENCIA_NUMERACAO = (('A', _('Sequencial por ano')), +SEQUENCIA_NUMERACAO_PROTOCOLO = (('A', _('Sequencial por ano')), ('L', _('Sequencial por legislatura')), ('U', _('Sequencial único'))) +SEQUENCIA_NUMERACAO_PROPOSICAO = (('A', _('Sequencial por ano para cada autor')), + ('B', _('Sequencial por ano indepententemente do autor'))) + ESFERA_FEDERACAO_CHOICES = (('M', _('Municipal')), ('E', _('Estadual')), ('F', _('Federal')), @@ -95,10 +98,15 @@ class AppConfig(models.Model): verbose_name=_('Estatísticas de acesso a normas'), choices=RELATORIO_ATOS_ACESSADOS, default='N') - sequencia_numeracao = models.CharField( + sequencia_numeracao_proposicao = models.CharField( + max_length=1, + verbose_name=_('Sequência de numeração de proposições'), + choices=SEQUENCIA_NUMERACAO_PROPOSICAO, default='A') + + sequencia_numeracao_protocolo = models.CharField( max_length=1, - verbose_name=_('Sequência de numeração'), - choices=SEQUENCIA_NUMERACAO, default='A') + verbose_name=_('Sequência de numeração de protocolos'), + choices=SEQUENCIA_NUMERACAO_PROTOCOLO, default='A') esfera_federacao = models.CharField( max_length=1, diff --git a/sapl/base/views.py b/sapl/base/views.py index 2312ab721..3226b4926 100644 --- a/sapl/base/views.py +++ b/sapl/base/views.py @@ -1081,9 +1081,7 @@ class ListarBancadaComissaoAutorExternoView(PermissionRequiredMixin, ListView): def autores_duplicados(): - return [autor.values() for autor in Autor.objects.values( - 'nome', 'tipo__descricao').order_by( - "nome").annotate(count=Count('nome')).filter(count__gt=1)] + return [autor for autor in Autor.objects.values('nome').annotate(count=Count('nome')).filter(count__gt=1)] class ListarAutoresDuplicadosView(PermissionRequiredMixin, ListView): @@ -1097,8 +1095,7 @@ class ListarAutoresDuplicadosView(PermissionRequiredMixin, ListView): return autores_duplicados() def get_context_data(self, **kwargs): - context = super( - ListarAutoresDuplicadosView, self).get_context_data(**kwargs) + context = super().get_context_data(**kwargs) paginator = context['paginator'] page_obj = context['page_obj'] context['page_range'] = make_pagination( diff --git a/sapl/compilacao/forms.py b/sapl/compilacao/forms.py index 222a86aed..e82d58f63 100644 --- a/sapl/compilacao/forms.py +++ b/sapl/compilacao/forms.py @@ -220,7 +220,7 @@ class NotaForm(ModelForm): publicacao = forms.DateField( label=Nota._meta.get_field('publicacao').verbose_name, - input_formats=['%d/%m/%Y'], + input_formats=['%d/%m/%Y', '%d%m%Y'], required=True, widget=forms.DateInput( format='%d/%m/%Y'), @@ -228,7 +228,7 @@ class NotaForm(ModelForm): ) efetividade = forms.DateField( label=Nota._meta.get_field('efetividade').verbose_name, - input_formats=['%d/%m/%Y'], + input_formats=['%d/%m/%Y', '%d%m%Y'], required=True, widget=forms.DateInput( format='%d/%m/%Y'), diff --git a/sapl/compilacao/migrations/0012_bug_auto_inserido.py b/sapl/compilacao/migrations/0012_bug_auto_inserido.py new file mode 100644 index 000000000..ac07c0a44 --- /dev/null +++ b/sapl/compilacao/migrations/0012_bug_auto_inserido.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.13 on 2018-03-19 13:41 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +def adjust_bug_auto_inserido(apps, schema_editor): + Dispositivo = apps.get_model('compilacao', 'Dispositivo') + + Dispositivo.objects.filter( + tipo_dispositivo__class_css__startswith='caput', + dispositivo_pai__tipo_dispositivo__class_css__startswith='artigo', + auto_inserido=False + ).update(auto_inserido=True) + + +class Migration(migrations.Migration): + + dependencies = [ + ('compilacao', '0011_tipotextoarticulado_rodape_global'), + ] + + operations = [ + migrations.RunPython(adjust_bug_auto_inserido), + ] diff --git a/sapl/compilacao/views.py b/sapl/compilacao/views.py index 120c66645..f3f2294ee 100644 --- a/sapl/compilacao/views.py +++ b/sapl/compilacao/views.py @@ -1545,7 +1545,7 @@ class ActionDeleteDispositivoMixin(ActionsCommonsMixin): if not anterior: self.logger.error("user=" + username + ". Não é possível excluir este Dispositivo (id={}) sem" - " excluir toda a sua estrutura!!!".format(base.ta_id)) + " excluir toda a sua estrutura!!!".format(base.id)) raise Exception( _('Não é possível excluir este Dispositivo sem' ' excluir toda a sua estrutura!!!')) @@ -1566,8 +1566,8 @@ class ActionDeleteDispositivoMixin(ActionsCommonsMixin): for candidato in parents: if candidato == base: self.logger.error("user=" + username + ". Não é possível excluir este " - "Dispositivo ({}) sem " - "excluir toda a sua estrutura!!!".format(candidato)) + "Dispositivo (id={}) sem " + "excluir toda a sua estrutura!!!".format(candidato.id)) raise Exception( _('Não é possível excluir este ' 'Dispositivo sem ' @@ -1604,8 +1604,8 @@ class ActionDeleteDispositivoMixin(ActionsCommonsMixin): break else: self.logger.error("user=" + username + ". Não é possível excluir este " - "Dispositivo ({}) sem excluir toda " - "a sua estrutura!!!".format(candidato)) + "Dispositivo (id={}) sem excluir toda " + "a sua estrutura!!!".format(candidato.id)) raise Exception( _('Não é possível excluir este ' 'Dispositivo sem ' @@ -1643,6 +1643,7 @@ class ActionDeleteDispositivoMixin(ActionsCommonsMixin): # excluir e renumerar irmaos profundidade_base = base.get_profundidade() + auto_inserido_base = base.auto_inserido base.delete() for irmao in irmaos_posteriores: @@ -1666,6 +1667,11 @@ class ActionDeleteDispositivoMixin(ActionsCommonsMixin): i.set_numero_completo([0, 0, 0, 0, 0, 0, ]) i.rotulo = i.rotulo_padrao(local_insert=1) i.save() + + if not irmaos.exists() and \ + auto_inserido_base and \ + pai_base.nivel: + self.remover_dispositivo(pai_base, False) else: # Renumerar Dispostivos de Contagem Contínua # de dentro da base se pai @@ -2224,9 +2230,15 @@ class ActionDispositivoCreateMixin(ActionsCommonsMixin): dispositivo_pai=dp.dispositivo_pai).count() if qtd_existente >= pp[0].quantidade_permitida: - data = {'pk': base.pk, - 'pai': [base.dispositivo_pai.pk, ]} - self.set_message(data, 'warning', + data = {'pk': None + if base.dispositivo_pai else + base.pk, + 'pai': [ + base.dispositivo_pai.pk if + base.dispositivo_pai else + base.pk, + ]} + self.set_message(data, 'danger', _('Limite de inserções de ' 'dispositivos deste tipo ' 'foi excedido.'), time=6000) @@ -2512,7 +2524,7 @@ class ActionsEditMixin(ActionDragAndMoveDispositivoAlteradoMixin, local_add=local_add, create_auto_inserts=True) - if data: + if data and data['pk']: ndp = Dispositivo.objects.get(pk=data['pk']) @@ -2539,6 +2551,9 @@ class ActionsEditMixin(ActionDragAndMoveDispositivoAlteradoMixin, data.update({'pk': ndp.pk, 'pai': [bloco_alteracao.pk, ]}) + else: + data.update({'pk': bloco_alteracao.pk, + 'pai': [bloco_alteracao.pk, ]}) return data diff --git a/sapl/legacy/test_renames.py b/sapl/legacy/test_renames.py index b92c5d0ac..9a5c4ebaf 100644 --- a/sapl/legacy/test_renames.py +++ b/sapl/legacy/test_renames.py @@ -13,9 +13,9 @@ from sapl.materia.models import (AcompanhamentoMateria, DocumentoAcessorio, from sapl.norma.models import (AnexoNormaJuridica, NormaJuridica, NormaRelacionada, TipoVinculoNormaJuridica) from sapl.parlamentares.models import (Frente, Mandato, Parlamentar, Partido, - TipoAfastamento, Votante) + TipoAfastamento, Votante, Bloco) from sapl.protocoloadm.models import DocumentoAdministrativo -from sapl.sessao.models import (Bancada, Bloco, CargoBancada, +from sapl.sessao.models import (Bancada, CargoBancada, ExpedienteMateria, Orador, OradorExpediente, OrdemDia, RegistroVotacao, ResumoOrdenacao, SessaoPlenaria, TipoResultadoVotacao, diff --git a/sapl/lexml/OAIServer.py b/sapl/lexml/OAIServer.py index 4c1d85c19..9783f2fa6 100644 --- a/sapl/lexml/OAIServer.py +++ b/sapl/lexml/OAIServer.py @@ -1,3 +1,4 @@ +import unicodedata from datetime import datetime import oaipmh @@ -11,6 +12,7 @@ from lxml.builder import ElementMaker from sapl.base.models import AppConfig, CasaLegislativa from sapl.lexml.models import LexmlPublicador, LexmlProvedor from sapl.norma.models import NormaJuridica +from sapl.utils import LISTA_DE_UFS class OAILEXML: @@ -122,22 +124,33 @@ class OAIServer: else: return None + @staticmethod + def remove_acentos(linha): + res = unicodedata.normalize('NFKD', linha).encode('ASCII', 'ignore') + res = res.decode("UTF-8") + remove_list = ["\'", "\"", "-"] + for i in remove_list: + res = res.replace(i, "") + return res + def monta_urn(self, norma, esfera): if norma: urn = 'urn:lex:br;' esferas = {'M': 'municipal', 'E': 'estadual'} - municipio = casa.municipio.lower() - uf = casa.uf.lower() + municipio = self.remove_acentos(casa.municipio.lower()) + uf_map = dict(LISTA_DE_UFS) + uf_desc = uf_map.get(casa.uf.upper(), '').lower() + uf_desc = self.remove_acentos(uf_desc) for x in [' ', '.de.', '.da.', '.das.', '.do.', '.dos.']: municipio = municipio.replace(x, '.') - uf = uf.replace(x, '.') + uf_desc = uf_desc.replace(x, '.') if esfera == 'M': - urn += '{};{}:'.format(uf, municipio) + urn += '{};{}:'.format(uf_desc, municipio) if norma.tipo.equivalente_lexml == 'regimento.interno' or norma.tipo.equivalente_lexml == 'resolucao': urn += 'camara.' urn += esferas[esfera] + ':' elif esfera == 'E': - urn += '{}:{}:'.format(uf, esferas[esfera]) + urn += '{}:{}:'.format(uf_desc, esferas[esfera]) else: urn += ':' if norma.tipo.equivalente_lexml: @@ -166,11 +179,14 @@ class OAIServer: return '' def monta_xml(self, urn, norma): + BASE_URL_SAPL = self.config['base_url'] + BASE_URL_SAPL = BASE_URL_SAPL[:BASE_URL_SAPL.find('/', 8)] + publicador = LexmlPublicador.objects.first() if norma and publicador: LEXML = ElementMaker(namespace=self.ns['lexml'], nsmap=self.ns) oai_lexml = LEXML.LexML() - oai_lexml.attrib['{{}}schemaLocation'.format(self.XSI_NS)] = '{} {}'.format( + oai_lexml.attrib['{{{pre}}}schemaLocation'.format(pre=self.XSI_NS)] = '{} {}'.format( 'http://www.lexml.gov.br/oai_lexml', 'http://projeto.lexml.gov.br/esquemas/oai_lexml.xsd') texto_integral = norma.texto_integral mime_types = {'doc': 'application/msword', @@ -178,20 +194,21 @@ class OAIServer: 'odt': 'application/vnd.oasis.opendocument.text', 'pdf': 'application/pdf', 'rtf': 'application/rtf'} + if texto_integral: - url_conteudo = self.config['base_url'] + texto_integral.url + url_conteudo = BASE_URL_SAPL + texto_integral.url extensao = texto_integral.url.split('.')[-1] formato = mime_types.get(extensao, 'application/octet-stream') else: formato = 'text/html' - url_conteudo = self.config['base_url'] + reverse('sapl.norma:normajuridica_detail', - kwargs={'pk': norma.numero}) + url_conteudo = BASE_URL_SAPL + reverse('sapl.norma:normajuridica_detail', + kwargs={'pk': norma.pk}) element_maker = ElementMaker() id_publicador = str(publicador.id_publicador) item_conteudo = element_maker.Item(url_conteudo, formato=formato, idPublicador=id_publicador, tipo='conteudo') oai_lexml.append(item_conteudo) - url = self.config['base_url'] + reverse('sapl.norma:normajuridica_detail', kwargs={'pk': norma.numero}) + url = BASE_URL_SAPL + reverse('sapl.norma:normajuridica_detail', kwargs={'pk': norma.pk}) item_metadado = element_maker.Item(url, formato='text/html', idPublicador=id_publicador, tipo='metadado') oai_lexml.append(item_metadado) documento_individual = element_maker.DocumentoIndividual(urn) diff --git a/sapl/materia/forms.py b/sapl/materia/forms.py index edbeed810..0b8a0e451 100644 --- a/sapl/materia/forms.py +++ b/sapl/materia/forms.py @@ -38,14 +38,15 @@ from sapl.materia.models import (AssuntoMateria, Autoria, MateriaAssunto, from sapl.norma.models import (LegislacaoCitada, NormaJuridica, TipoNormaJuridica) from sapl.parlamentares.models import Legislatura, Partido, Parlamentar -from sapl.protocoloadm.models import Protocolo, DocumentoAdministrativo +from sapl.protocoloadm.models import Protocolo, DocumentoAdministrativo, Anexado from sapl.settings import MAX_DOC_UPLOAD_SIZE from sapl.utils import (YES_NO_CHOICES, SEPARADOR_HASH_PROPOSICAO, ChoiceWithoutValidationField, MateriaPesquisaOrderingFilter, RangeWidgetOverride, autor_label, autor_modal, gerar_hash_arquivo, models_with_gr_for_model, qs_override_django_filter, - choice_anos_com_materias, FilterOverridesMetaMixin, FileFieldCheckMixin) + choice_anos_com_materias, FilterOverridesMetaMixin, FileFieldCheckMixin, + lista_anexados) from .models import (AcompanhamentoMateria, Anexada, Autoria, DespachoInicial, DocumentoAcessorio, Numeracao, Proposicao, Relatoria, @@ -461,7 +462,11 @@ class TramitacaoForm(ModelForm): 'unidade_tramitacao_destino', 'data_encaminhamento', 'data_fim_prazo', - 'texto'] + 'texto', + 'user', + 'ip'] + widgets = {'user': forms.HiddenInput(), + 'ip': forms.HiddenInput()} def __init__(self, *args, **kwargs): super(TramitacaoForm, self).__init__(*args, **kwargs) @@ -549,17 +554,46 @@ class TramitacaoForm(ModelForm): def save(self, commit=True): tramitacao = super(TramitacaoForm, self).save(commit) materia = tramitacao.materia - for ma in materia.anexadas.all(): + materia.em_tramitacao = False if tramitacao.status.indicador == "F" else True + materia.save() + + lista_tramitacao = [] + lista_anexadas = lista_anexados(materia) + for ma in lista_anexadas: if not ma.tramitacao_set.all() \ or ma.tramitacao_set.last().unidade_tramitacao_destino == tramitacao.unidade_tramitacao_local: - tramitacao_nova = tramitacao - tramitacao_nova.pk = None - tramitacao_nova.materia = ma - tramitacao_nova.save() + ma.em_tramitacao = False if tramitacao.status.indicador == "F" else True + ma.save() + lista_tramitacao.append(Tramitacao( + status=tramitacao.status, + materia=ma, + data_tramitacao=tramitacao.data_tramitacao, + unidade_tramitacao_local=tramitacao.unidade_tramitacao_local, + data_encaminhamento=tramitacao.data_encaminhamento, + unidade_tramitacao_destino=tramitacao.unidade_tramitacao_destino, + urgente=tramitacao.urgente, + turno=tramitacao.turno, + texto=tramitacao.texto, + data_fim_prazo=tramitacao.data_fim_prazo, + user=tramitacao.user, + ip=tramitacao.ip + )) + Tramitacao.objects.bulk_create(lista_tramitacao) return tramitacao +# Compara se os campos de duas tramitações são iguais, +# exceto os campos id, documento_id e timestamp +def compara_tramitacoes_mat(tramitacao1, tramitacao2): + if not tramitacao1 or not tramitacao2: + return False + + lst_items = ['id', 'materia_id', 'timestamp'] + values = [(k,v) for k,v in tramitacao1.__dict__.items() if ((k not in lst_items) and (k[0] != '_'))] + other_values = [(k,v) for k,v in tramitacao2.__dict__.items() if (k not in lst_items and k[0] != '_')] + return values == other_values + class TramitacaoUpdateForm(TramitacaoForm): unidade_tramitacao_local = forms.ModelChoiceField( queryset=UnidadeTramitacao.objects.all(), @@ -580,11 +614,15 @@ class TramitacaoUpdateForm(TramitacaoForm): 'data_encaminhamento', 'data_fim_prazo', 'texto', + 'user', + 'ip' ] widgets = { 'data_encaminhamento': forms.DateInput(format='%d/%m/%Y'), 'data_fim_prazo': forms.DateInput(format='%d/%m/%Y'), + 'user': forms.HiddenInput(), + 'ip': forms.HiddenInput() } def clean(self): @@ -593,33 +631,73 @@ class TramitacaoUpdateForm(TramitacaoForm): if not self.is_valid(): return self.cleaned_data + cd = self.cleaned_data + obj = self.instance + ultima_tramitacao = Tramitacao.objects.filter( - materia_id=self.instance.materia_id).order_by( + materia_id=obj.materia_id).order_by( '-data_tramitacao', '-id').first() # Se a Tramitação que está sendo editada não for a mais recente, # ela não pode ter seu destino alterado. - if ultima_tramitacao != self.instance: - if self.cleaned_data['unidade_tramitacao_destino'] != \ - self.instance.unidade_tramitacao_destino: + if ultima_tramitacao != obj: + if cd['unidade_tramitacao_destino'] != \ + obj.unidade_tramitacao_destino: self.logger.error("Você não pode mudar a Unidade de Destino desta " "tramitação para {}, pois irá conflitar com a Unidade " "Local da tramitação seguinte ({})." - .format(self.cleaned_data['unidade_tramitacao_destino'], - self.instance.unidade_tramitacao_destino)) + .format(cd['unidade_tramitacao_destino'], + obj.unidade_tramitacao_destino)) raise ValidationError( 'Você não pode mudar a Unidade de Destino desta ' 'tramitação, pois irá conflitar com a Unidade ' 'Local da tramitação seguinte') + + # Se não houve qualquer alteração em um dos dados, mantém o usuário e ip + if not (cd['data_tramitacao'] != obj.data_tramitacao or \ + cd['unidade_tramitacao_destino'] != obj.unidade_tramitacao_destino or \ + cd['status'] != obj.status or cd['texto'] != obj.texto or \ + cd['data_encaminhamento'] != obj.data_encaminhamento or \ + cd['data_fim_prazo'] != obj.data_fim_prazo or \ + cd['urgente'] != obj.urgente or \ + cd['turno'] != obj.turno): + cd['user'] = obj.user + cd['ip'] = obj.ip + + cd['data_tramitacao'] = obj.data_tramitacao + cd['unidade_tramitacao_local'] = obj.unidade_tramitacao_local - self.cleaned_data['data_tramitacao'] = \ - self.instance.data_tramitacao - self.cleaned_data['unidade_tramitacao_local'] = \ - self.instance.unidade_tramitacao_local + return cd - return self.cleaned_data + @transaction.atomic + def save(self, commit=True): + ant_tram_principal = Tramitacao.objects.get(id=self.instance.id) + nova_tram_principal = super(TramitacaoUpdateForm, self).save(commit) + materia = nova_tram_principal.materia + materia.em_tramitacao = False if nova_tram_principal.status.indicador == "F" else True + materia.save() + lista_anexadas = lista_anexados(materia) + for ma in lista_anexadas: + tram_anexada = ma.tramitacao_set.last() + if compara_tramitacoes_mat(ant_tram_principal, tram_anexada): + tram_anexada.status = nova_tram_principal.status + tram_anexada.data_tramitacao = nova_tram_principal.data_tramitacao + tram_anexada.unidade_tramitacao_local = nova_tram_principal.unidade_tramitacao_local + tram_anexada.data_encaminhamento = nova_tram_principal.data_encaminhamento + tram_anexada.unidade_tramitacao_destino = nova_tram_principal.unidade_tramitacao_destino + tram_anexada.urgente = nova_tram_principal.urgente + tram_anexada.turno = nova_tram_principal.turno + tram_anexada.texto = nova_tram_principal.texto + tram_anexada.data_fim_prazo = nova_tram_principal.data_fim_prazo + tram_anexada.user = nova_tram_principal.user + tram_anexada.ip = nova_tram_principal.ip + tram_anexada.save() + + ma.em_tramitacao = False if nova_tram_principal.status.indicador == "F" else True + ma.save() + return nova_tram_principal class LegislacaoCitadaForm(ModelForm): @@ -790,6 +868,13 @@ class AnexadaForm(ModelForm): cleaned_data = self.cleaned_data + data_anexacao = cleaned_data['data_anexacao'] + data_desanexacao = cleaned_data['data_desanexacao'] if cleaned_data['data_desanexacao'] else data_anexacao + + if data_anexacao > data_desanexacao: + self.logger.error("Data de anexação posterior à data de desanexação.") + raise ValidationError(_("Data de anexação posterior à data de desanexação.")) + try: self.logger.info("Tentando obter objeto MateriaLegislativa (numero={}, ano={}, tipo={})." .format(cleaned_data['numero'], cleaned_data['ano'], cleaned_data['tipo'])) @@ -817,6 +902,26 @@ class AnexadaForm(ModelForm): if is_anexada: self.logger.error("Matéria já se encontra anexada.") raise ValidationError(_('Matéria já se encontra anexada')) + + ciclico = False + anexadas_anexada = Anexada.objects.filter(materia_principal=materia_anexada) + + while anexadas_anexada and not ciclico: + anexadas = [] + + for anexa in anexadas_anexada: + + if materia_principal == anexa.materia_anexada: + ciclico = True + else: + for a in Anexada.objects.filter(materia_principal=anexa.materia_anexada): + anexadas.append(a) + + anexadas_anexada = anexadas + + if ciclico: + self.logger.error("A matéria não pode ser anexada por uma de suas anexadas.") + raise ValidationError(_("A matéria não pode ser anexada por uma de suas anexadas.")) cleaned_data['materia_anexada'] = materia_anexada @@ -1481,10 +1586,10 @@ class ProposicaoForm(FileFieldCheckMixin, forms.ModelForm): 'hash_code': forms.HiddenInput(), } def __init__(self, *args, **kwargs): - self.texto_articulado_proposicao = sapl.base.models.AppConfig.attr( + self.texto_articulado_proposicao = AppConfig.attr( 'texto_articulado_proposicao') - self.receber_recibo = sapl.base.models.AppConfig.attr( + self.receber_recibo = AppConfig.attr( 'receber_recibo_proposicao') if not self.texto_articulado_proposicao: @@ -1506,7 +1611,7 @@ class ProposicaoForm(FileFieldCheckMixin, forms.ModelForm): ] - if sapl.base.models.AppConfig.objects.last().escolher_numero_materia_proposicao: + if AppConfig.objects.last().escolher_numero_materia_proposicao: fields.append(to_column(('numero_materia_futuro', 12)),) else: if 'numero_materia_futuro' in self._meta.fields: @@ -1639,12 +1744,17 @@ class ProposicaoForm(FileFieldCheckMixin, forms.ModelForm): return super().save(commit) inst.ano = timezone.now().year - numero__max = Proposicao.objects.filter( - autor=inst.autor, - ano=timezone.now().year).aggregate(Max('numero_proposicao')) + sequencia_numeracao = AppConfig.attr('sequencia_numeracao_proposicao') + if sequencia_numeracao == 'A': + numero__max = Proposicao.objects.filter( + autor=inst.autor, + ano=timezone.now().year).aggregate(Max('numero_proposicao')) + elif sequencia_numeracao == 'B': + numero__max = Proposicao.objects.filter( + ano=timezone.now().year).aggregate(Max('numero_proposicao')) numero__max = numero__max['numero_proposicao__max'] inst.numero_proposicao = ( - numero__max + 1) if numero__max else 1 + numero__max + 1) if numero__max else 1 self.gerar_hash(inst, receber_recibo) @@ -1741,7 +1851,7 @@ class ConfirmarProposicaoForm(ProposicaoForm): required=False, widget=widgets.TextInput( attrs={'readonly': 'readonly'})) - regime_tramitacao = forms.ModelChoiceField( + regime_tramitacao = forms.ModelChoiceField(label="Regime de tramitação", required=False, queryset=RegimeTramitacao.objects.all()) gerar_protocolo = forms.ChoiceField( @@ -1774,8 +1884,7 @@ class ConfirmarProposicaoForm(ProposicaoForm): def __init__(self, *args, **kwargs): self.proposicao_incorporacao_obrigatoria = \ - sapl.base.models.AppConfig.attr( - 'proposicao_incorporacao_obrigatoria') + AppConfig.attr('proposicao_incorporacao_obrigatoria') if self.proposicao_incorporacao_obrigatoria != 'C': if 'gerar_protocolo' in self._meta.fields: @@ -1807,6 +1916,10 @@ class ConfirmarProposicaoForm(ProposicaoForm): # esta chamada isola o __init__ de ProposicaoForm super(ProposicaoForm, self).__init__(*args, **kwargs) + if self.instance.tipo.content_type.model_class() ==\ + TipoMateriaLegislativa: + self.fields['regime_tramitacao'].required = True + fields = [ Fieldset( _('Dados Básicos'), @@ -1823,7 +1936,7 @@ class ConfirmarProposicaoForm(ProposicaoForm): ) ] - if not sapl.base.models.AppConfig.objects.last().escolher_numero_materia_proposicao or \ + if not AppConfig.objects.last().escolher_numero_materia_proposicao or \ not self.instance.numero_materia_futuro: if 'numero_materia_futuro' in self._meta.fields: del fields[0][0][3] @@ -1903,7 +2016,7 @@ class ConfirmarProposicaoForm(ProposicaoForm): if not self.is_valid(): return self.cleaned_data - numeracao = sapl.base.models.AppConfig.attr('sequencia_numeracao') + numeracao = AppConfig.attr('sequencia_numeracao_proposicao') if not numeracao: self.logger.error("A sequência de numeração (por ano ou geral)" @@ -1980,8 +2093,8 @@ class ConfirmarProposicaoForm(ProposicaoForm): try: self.logger.debug( "Tentando obter modelo de sequência de numeração.") - numeracao = sapl.base.models.AppConfig.objects.last( - ).sequencia_numeracao + numeracao = AppConfig.objects.last( + ).sequencia_numeracao_protocolo except AttributeError as e: self.logger.error("Erro ao obter modelo. " + str(e)) pass @@ -2007,7 +2120,6 @@ class ConfirmarProposicaoForm(ProposicaoForm): elif numeracao == 'U': numero = MateriaLegislativa.objects.filter( tipo=tipo).aggregate(Max('numero')) - if numeracao is None: numero['numero__max'] = 0 @@ -2133,7 +2245,7 @@ class ConfirmarProposicaoForm(ProposicaoForm): GenericForeignKey """ - numeracao = sapl.base.models.AppConfig.attr('sequencia_numeracao') + numeracao = AppConfig.attr('sequencia_numeracao_protocolo') if numeracao == 'A': nm = Protocolo.objects.filter( ano=timezone.now().year).aggregate(Max('numero')) diff --git a/sapl/materia/migrations/0045_auto_20190415_1050.py b/sapl/materia/migrations/0045_auto_20190415_1050.py new file mode 100644 index 000000000..55f59dd49 --- /dev/null +++ b/sapl/materia/migrations/0045_auto_20190415_1050.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-04-15 13:50 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('materia', '0044_auto_20190327_1409'), + ] + + operations = [ + migrations.AlterField( + model_name='tipomaterialegislativa', + name='sequencia_numeracao', + field=models.CharField(blank=True, choices=[('A', 'Sequencial por ano para cada autor'), ('B', 'Sequencial por ano indepententemente do autor'), ('L', 'Sequencial por legislatura'), ('U', 'Sequencial único')], max_length=1, verbose_name='Sequência de numeração'), + ), + ] diff --git a/sapl/materia/migrations/0046_auto_20190417_0941.py b/sapl/materia/migrations/0046_auto_20190417_0941.py new file mode 100644 index 000000000..f98170042 --- /dev/null +++ b/sapl/materia/migrations/0046_auto_20190417_0941.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-04-17 12:41 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('materia', '0045_auto_20190415_1050'), + ] + + operations = [ + migrations.AlterField( + model_name='tipomaterialegislativa', + name='sequencia_numeracao', + field=models.CharField(blank=True, choices=[('A', 'Sequencial por ano para cada autor'), ('L', 'Sequencial por legislatura'), ('U', 'Sequencial único')], max_length=1, verbose_name='Sequência de numeração'), + ), + ] diff --git a/sapl/materia/migrations/0046_auto_20190417_1212.py b/sapl/materia/migrations/0046_auto_20190417_1212.py new file mode 100644 index 000000000..397114479 --- /dev/null +++ b/sapl/materia/migrations/0046_auto_20190417_1212.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-04-17 15:12 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('materia', '0045_auto_20190415_1050'), + ] + + operations = [ + migrations.AddField( + model_name='tramitacao', + name='ip', + field=models.CharField(blank=True, default='', max_length=30, verbose_name='IP'), + ), + migrations.AddField( + model_name='tramitacao', + name='user', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='Usuário'), + ), + ] diff --git a/sapl/materia/migrations/0047_auto_20190417_1432.py b/sapl/materia/migrations/0047_auto_20190417_1432.py new file mode 100644 index 000000000..12783fe3a --- /dev/null +++ b/sapl/materia/migrations/0047_auto_20190417_1432.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-04-17 17:32 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('materia', '0046_auto_20190417_0941'), + ] + + operations = [ + migrations.AlterField( + model_name='tipomaterialegislativa', + name='sequencia_numeracao', + field=models.CharField(blank=True, choices=[('A', 'Sequencial por ano'), ('L', 'Sequencial por legislatura'), ('U', 'Sequencial único')], max_length=1, verbose_name='Sequência de numeração'), + ), + ] diff --git a/sapl/materia/migrations/0048_merge_20190426_0828.py b/sapl/materia/migrations/0048_merge_20190426_0828.py new file mode 100644 index 000000000..94d624b1d --- /dev/null +++ b/sapl/materia/migrations/0048_merge_20190426_0828.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-04-26 11:28 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('materia', '0046_auto_20190417_1212'), + ('materia', '0047_auto_20190417_1432'), + ] + + operations = [ + ] diff --git a/sapl/materia/models.py b/sapl/materia/models.py index c7aaaed15..aad491b01 100644 --- a/sapl/materia/models.py +++ b/sapl/materia/models.py @@ -10,7 +10,7 @@ from django.utils.translation import ugettext_lazy as _ from model_utils import Choices import reversion -from sapl.base.models import SEQUENCIA_NUMERACAO, Autor +from sapl.base.models import SEQUENCIA_NUMERACAO_PROTOCOLO, Autor from sapl.comissoes.models import Comissao from sapl.compilacao.models import (PerfilEstruturalTextoArticulado, TextoArticulado) @@ -18,7 +18,7 @@ from sapl.parlamentares.models import Parlamentar #from sapl.protocoloadm.models import Protocolo from sapl.utils import (RANGE_ANOS, YES_NO_CHOICES, SaplGenericForeignKey, SaplGenericRelation, restringe_tipos_de_arquivo_txt, - texto_upload_path) + texto_upload_path, get_settings_auth_user_model) EM_TRAMITACAO = [(1, 'Sim'), @@ -128,7 +128,7 @@ class TipoMateriaLegislativa(models.Model): max_length=1, blank=True, verbose_name=_('Sequência de numeração'), - choices=SEQUENCIA_NUMERACAO) + choices=SEQUENCIA_NUMERACAO_PROTOCOLO) sequencia_regimental = models.PositiveIntegerField( default=0, @@ -1003,6 +1003,15 @@ class Tramitacao(models.Model): texto = models.TextField(verbose_name=_('Texto da Ação')) data_fim_prazo = models.DateField( blank=True, null=True, verbose_name=_('Data Fim Prazo')) + user = models.ForeignKey(get_settings_auth_user_model(), + verbose_name=_('Usuário'), + on_delete=models.PROTECT, + null=True, + blank=True) + ip = models.CharField(verbose_name=_('IP'), + max_length=30, + blank=True, + default='') class Meta: verbose_name = _('Tramitação') diff --git a/sapl/materia/tests/test_materia.py b/sapl/materia/tests/test_materia.py index 80f3f244f..1938d04f4 100644 --- a/sapl/materia/tests/test_materia.py +++ b/sapl/materia/tests/test_materia.py @@ -1,3 +1,4 @@ +from datetime import date from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType from django.core.files.uploadedfile import SimpleUploadedFile @@ -14,10 +15,67 @@ from sapl.materia.models import (Anexada, Autoria, DespachoInicial, StatusTramitacao, TipoDocumento, TipoMateriaLegislativa, TipoProposicao, Tramitacao, UnidadeTramitacao) +from sapl.materia.forms import (TramitacaoForm, compara_tramitacoes_mat, + TramitacaoUpdateForm) from sapl.norma.models import (LegislacaoCitada, NormaJuridica, TipoNormaJuridica) from sapl.parlamentares.models import Legislatura -from sapl.utils import models_with_gr_for_model +from sapl.utils import models_with_gr_for_model, lista_anexados + + +@pytest.mark.django_db(transaction=False) +def test_lista_materias_anexadas(): + tipo_materia = mommy.make( + TipoMateriaLegislativa, + descricao="Tipo_Teste" + ) + regime_tramitacao = mommy.make( + RegimeTramitacao, + descricao="Regime_Teste" + ) + materia_principal = mommy.make( + MateriaLegislativa, + numero=20, + ano=2018, + data_apresentacao="2018-01-04", + regime_tramitacao=regime_tramitacao, + tipo=tipo_materia + ) + materia_anexada = mommy.make( + MateriaLegislativa, + numero=21, + ano=2019, + data_apresentacao="2019-05-04", + regime_tramitacao=regime_tramitacao, + tipo=tipo_materia + ) + materia_anexada_anexada = mommy.make( + MateriaLegislativa, + numero=22, + ano=2020, + data_apresentacao="2020-01-05", + regime_tramitacao=regime_tramitacao, + tipo=tipo_materia + ) + + mommy.make( + Anexada, + materia_principal=materia_principal, + materia_anexada=materia_anexada, + data_anexacao="2019-05-11" + ) + mommy.make( + Anexada, + materia_principal=materia_anexada, + materia_anexada=materia_anexada_anexada, + data_anexacao="2020-11-05" + ) + + lista = lista_anexados(materia_principal) + + assert len(lista) == 2 + assert lista[0] == materia_anexada + assert lista[1] == materia_anexada_anexada @pytest.mark.django_db(transaction=False) @@ -581,3 +639,178 @@ def test_numeracao_materia_legislativa_por_ano(admin_client): response_content = eval(response.content.decode('ascii')) esperado_outro_ano = eval('{"ano": "2010", "numero": 1}') assert response_content['numero'] == esperado_outro_ano['numero'] + + +@pytest.mark.django_db(transaction=False) +def test_tramitacoes_materias_anexadas(admin_client): + tipo_materia = mommy.make( + TipoMateriaLegislativa, + descricao="Tipo_Teste" + ) + materia_principal = mommy.make( + MateriaLegislativa, + ano=2018, + data_apresentacao="2018-01-04", + tipo=tipo_materia + ) + materia_anexada = mommy.make( + MateriaLegislativa, + ano=2019, + data_apresentacao="2019-05-04", + tipo=tipo_materia + ) + materia_anexada_anexada = mommy.make( + MateriaLegislativa, + ano=2020, + data_apresentacao="2020-01-05", + tipo=tipo_materia + ) + + mommy.make( + Anexada, + materia_principal=materia_principal, + materia_anexada=materia_anexada, + data_anexacao="2019-05-11" + ) + mommy.make( + Anexada, + materia_principal=materia_anexada, + materia_anexada=materia_anexada_anexada, + data_anexacao="2020-11-05" + ) + + + unidade_tramitacao_local_1 = make_unidade_tramitacao(descricao="Teste 1") + unidade_tramitacao_destino_1 = make_unidade_tramitacao(descricao="Teste 2") + unidade_tramitacao_destino_2 = make_unidade_tramitacao(descricao="Teste 3") + + status = mommy.make( + StatusTramitacao, + indicador='R') + + # Teste criação de Tramitacao + form = TramitacaoForm(data={}) + form.data = {'data_tramitacao':date(2019, 5, 6), + 'unidade_tramitacao_local':unidade_tramitacao_local_1.pk, + 'unidade_tramitacao_destino':unidade_tramitacao_destino_1.pk, + 'status':status.pk, + 'urgente': False, + 'texto': "Texto de teste"} + form.instance.materia_id=materia_principal.pk + + assert form.is_valid() + + tramitacao_principal = form.save() + tramitacao_anexada = materia_anexada.tramitacao_set.last() + tramitacao_anexada_anexada = materia_anexada_anexada.tramitacao_set.last() + + # Verifica se foram criadas as tramitações para as matérias anexadas e anexadas às anexadas + assert materia_principal.tramitacao_set.last() == tramitacao_principal + assert tramitacao_principal.materia.em_tramitacao == (tramitacao_principal.status.indicador != "F") + assert compara_tramitacoes_mat(tramitacao_principal, tramitacao_anexada) + assert MateriaLegislativa.objects.get(id=materia_anexada.pk).em_tramitacao \ + == (tramitacao_anexada.status.indicador != "F") + assert compara_tramitacoes_mat(tramitacao_anexada_anexada, tramitacao_principal) + assert MateriaLegislativa.objects.get(id=materia_anexada_anexada.pk).em_tramitacao \ + == (tramitacao_anexada_anexada.status.indicador != "F") + + + # Teste Edição de Tramitacao + form = TramitacaoUpdateForm(data={}) + # Alterando unidade_tramitacao_destino + form.data = {'data_tramitacao':tramitacao_principal.data_tramitacao, + 'unidade_tramitacao_local':tramitacao_principal.unidade_tramitacao_local.pk, + 'unidade_tramitacao_destino':unidade_tramitacao_destino_2.pk, + 'status':tramitacao_principal.status.pk, + 'urgente': tramitacao_principal.urgente, + 'texto': tramitacao_principal.texto} + form.instance = tramitacao_principal + + assert form.is_valid() + tramitacao_principal = form.save() + tramitacao_anexada = materia_anexada.tramitacao_set.last() + tramitacao_anexada_anexada = materia_anexada_anexada.tramitacao_set.last() + + assert tramitacao_principal.unidade_tramitacao_destino == unidade_tramitacao_destino_2 + assert tramitacao_anexada.unidade_tramitacao_destino == unidade_tramitacao_destino_2 + assert tramitacao_anexada_anexada.unidade_tramitacao_destino == unidade_tramitacao_destino_2 + + + # Teste Remoção de Tramitacao + url = reverse('sapl.materia:tramitacao_delete', + kwargs={'pk': tramitacao_principal.pk}) + response = admin_client.post(url, {'confirmar':'confirmar'} ,follow=True) + assert Tramitacao.objects.filter(id=tramitacao_principal.pk).count() == 0 + assert Tramitacao.objects.filter(id=tramitacao_anexada.pk).count() == 0 + assert Tramitacao.objects.filter(id=tramitacao_anexada_anexada.pk).count() == 0 + + + # Testes para quando as tramitações das anexadas divergem + form = TramitacaoForm(data={}) + form.data = {'data_tramitacao':date(2019, 5, 6), + 'unidade_tramitacao_local':unidade_tramitacao_local_1.pk, + 'unidade_tramitacao_destino':unidade_tramitacao_destino_1.pk, + 'status':status.pk, + 'urgente': False, + 'texto': "Texto de teste"} + form.instance.materia_id=materia_principal.pk + + assert form.is_valid() + + tramitacao_principal = form.save() + tramitacao_anexada = materia_anexada.tramitacao_set.last() + tramitacao_anexada_anexada = materia_anexada_anexada.tramitacao_set.last() + + form = TramitacaoUpdateForm(data={}) + # Alterando unidade_tramitacao_destino + form.data = {'data_tramitacao':tramitacao_anexada.data_tramitacao, + 'unidade_tramitacao_local':tramitacao_anexada.unidade_tramitacao_local.pk, + 'unidade_tramitacao_destino':unidade_tramitacao_destino_2.pk, + 'status':tramitacao_anexada.status.pk, + 'urgente': tramitacao_anexada.urgente, + 'texto': tramitacao_anexada.texto} + form.instance = tramitacao_anexada + + assert form.is_valid() + + tramitacao_anexada = form.save() + tramitacao_anexada_anexada = materia_anexada_anexada.tramitacao_set.last() + + assert tramitacao_principal.unidade_tramitacao_destino == unidade_tramitacao_destino_1 + assert tramitacao_anexada.unidade_tramitacao_destino == unidade_tramitacao_destino_2 + assert tramitacao_anexada_anexada.unidade_tramitacao_destino == unidade_tramitacao_destino_2 + + # Editando a tramitação principal, as tramitações anexadas não devem ser editadas + form = TramitacaoUpdateForm(data={}) + # Alterando o texto + form.data = {'data_tramitacao':tramitacao_principal.data_tramitacao, + 'unidade_tramitacao_local':tramitacao_principal.unidade_tramitacao_local.pk, + 'unidade_tramitacao_destino':tramitacao_principal.unidade_tramitacao_destino.pk, + 'status':tramitacao_principal.status.pk, + 'urgente': tramitacao_principal.urgente, + 'texto': "Testando a alteração"} + form.instance = tramitacao_principal + + assert form.is_valid() + tramitacao_principal = form.save() + tramitacao_anexada = materia_anexada.tramitacao_set.last() + tramitacao_anexada_anexada = materia_anexada_anexada.tramitacao_set.last() + + assert tramitacao_principal.texto == "Testando a alteração" + assert not tramitacao_anexada.texto == "Testando a alteração" + assert not tramitacao_anexada_anexada.texto == "Testando a alteração" + + # Removendo a tramitação pricipal, as tramitações anexadas não devem ser removidas, pois divergiram + url = reverse('sapl.materia:tramitacao_delete', + kwargs={'pk': tramitacao_principal.pk}) + response = admin_client.post(url, {'confirmar':'confirmar'} ,follow=True) + assert Tramitacao.objects.filter(id=tramitacao_principal.pk).count() == 0 + assert Tramitacao.objects.filter(id=tramitacao_anexada.pk).count() == 1 + assert Tramitacao.objects.filter(id=tramitacao_anexada_anexada.pk).count() == 1 + + # Removendo a tramitação anexada, a tramitação anexada à anexada deve ser removida + url = reverse('sapl.materia:tramitacao_delete', + kwargs={'pk': tramitacao_anexada.pk}) + response = admin_client.post(url, {'confirmar':'confirmar'} ,follow=True) + assert Tramitacao.objects.filter(id=tramitacao_anexada.pk).count() == 0 + assert Tramitacao.objects.filter(id=tramitacao_anexada_anexada.pk).count() == 0 \ No newline at end of file diff --git a/sapl/materia/views.py b/sapl/materia/views.py index 8d7278e5f..059ec38b2 100644 --- a/sapl/materia/views.py +++ b/sapl/materia/views.py @@ -53,9 +53,9 @@ from sapl.parlamentares.models import Legislatura from sapl.protocoloadm.models import Protocolo from sapl.settings import MEDIA_ROOT from sapl.utils import (YES_NO_CHOICES, autor_label, autor_modal, SEPARADOR_HASH_PROPOSICAO, - gerar_hash_arquivo, get_base_url, + gerar_hash_arquivo, get_base_url, get_client_ip, get_mime_type_from_file_extension, montar_row_autor, - show_results_filter_set, mail_service_configured) + show_results_filter_set, mail_service_configured, lista_anexados) from .forms import (AcessorioEmLoteFilterSet, AcompanhamentoMateriaForm, AnexadaEmLoteFilterSet, @@ -69,7 +69,7 @@ from .forms import (AcessorioEmLoteFilterSet, AcompanhamentoMateriaForm, filtra_tramitacao_destino, filtra_tramitacao_destino_and_status, filtra_tramitacao_status, - ExcluirTramitacaoEmLote) + ExcluirTramitacaoEmLote, compara_tramitacoes_mat) from .models import (AcompanhamentoMateria, Anexada, AssuntoMateria, Autoria, DespachoInicial, DocumentoAcessorio, MateriaAssunto, MateriaLegislativa, Numeracao, Orgao, Origem, Proposicao, @@ -333,7 +333,7 @@ def recuperar_materia(request): logger.debug("user=" + username + ". Tentando obter numeração da matéria.") numeracao = sapl.base.models.AppConfig.objects.last( - ).sequencia_numeracao + ).sequencia_numeracao_proposicao except AttributeError as e: logger.error("user=" + username + ". " + str(e) + " Numeracao da matéria definida como None.") @@ -1189,6 +1189,8 @@ class TramitacaoCrud(MasterDetailCrud): else: initial['unidade_tramitacao_local'] = '' initial['data_tramitacao'] = timezone.now().date() + initial['ip'] = get_client_ip(self.request) + initial['user'] = self.request.user return initial def get_context_data(self, **kwargs): @@ -1201,6 +1203,7 @@ class TramitacaoCrud(MasterDetailCrud): '-timestamp', '-id').first() + #TODO: Esta checagem foi inserida na issue #2027, mas é mesmo necessária? if ultima_tramitacao: if ultima_tramitacao.unidade_tramitacao_destino: context['form'].fields[ @@ -1214,6 +1217,15 @@ class TramitacaoCrud(MasterDetailCrud): ' da última tramitação não pode ser vazia!') messages.add_message(self.request, messages.ERROR, msg) + primeira_tramitacao = not(Tramitacao.objects.filter( + materia_id=int(kwargs['root_pk'])).exists()) + + # Se não for a primeira tramitação daquela matéria, o campo + # não pode ser modificado + if not primeira_tramitacao: + context['form'].fields[ + 'unidade_tramitacao_local'].widget.attrs['disabled'] = True + return context def form_valid(self, form): @@ -1221,12 +1233,6 @@ class TramitacaoCrud(MasterDetailCrud): self.object = form.save() username = self.request.user.username - if form.instance.status.indicador == 'F': - form.instance.materia.em_tramitacao = False - else: - form.instance.materia.em_tramitacao = True - form.instance.materia.save() - try: self.logger.debug("user=" + username + ". Tentando enviar Tramitacao (sender={}, post={}, request={})." .format(Tramitacao, self.object, self.request)) @@ -1234,7 +1240,6 @@ class TramitacaoCrud(MasterDetailCrud): post=self.object, request=self.request) except Exception as e: - # TODO log error msg = _('Tramitação criada, mas e-mail de acompanhamento ' 'de matéria não enviado. Há problemas na configuração ' 'do e-mail.') @@ -1251,16 +1256,16 @@ class TramitacaoCrud(MasterDetailCrud): layout_key = 'TramitacaoUpdate' + def get_initial(self): + initial = super(UpdateView, self).get_initial() + initial['ip'] = get_client_ip(self.request) + initial['user'] = self.request.user + return initial + def form_valid(self, form): self.object = form.save() username = self.request.user.username - if form.instance.status.indicador == 'F': - form.instance.materia.em_tramitacao = False - else: - form.instance.materia.em_tramitacao = True - form.instance.materia.save() - try: self.logger.debug("user=" + username + ". Tentando enviar Tramitacao (sender={}, post={}, request={}" .format(Tramitacao, self.object, self.request)) @@ -1268,7 +1273,6 @@ class TramitacaoCrud(MasterDetailCrud): post=self.object, request=self.request) except Exception: - # TODO log error msg = _('Tramitação atualizada, mas e-mail de acompanhamento ' 'de matéria não enviado. Há problemas na configuração ' 'do e-mail.') @@ -1294,18 +1298,17 @@ class TramitacaoCrud(MasterDetailCrud): def delete(self, request, *args, **kwargs): tramitacao = Tramitacao.objects.get(id=self.kwargs['pk']) - materia = MateriaLegislativa.objects.get(id=tramitacao.materia.id) + materia = tramitacao.materia url = reverse('sapl.materia:tramitacao_list', - kwargs={'pk': tramitacao.materia.id}) - + kwargs={'pk': materia.id}) + ultima_tramitacao = materia.tramitacao_set.order_by( '-data_tramitacao', '-timestamp', '-id').first() - username = request.user.username - if tramitacao.pk != ultima_tramitacao.pk: + username = request.user.username self.logger.error("user=" + username + ". Não é possível deletar a tramitação de pk={}. " "Somente a última tramitação (pk={}) pode ser deletada!." .format(tramitacao.pk, ultima_tramitacao.pk)) @@ -1313,9 +1316,24 @@ class TramitacaoCrud(MasterDetailCrud): messages.add_message(request, messages.ERROR, msg) return HttpResponseRedirect(url) else: - tramitacao.delete() + tramitacoes_deletar = [tramitacao.id] + mat_anexadas = lista_anexados(materia) + for ma in mat_anexadas: + tram_anexada = ma.tramitacao_set.last() + if compara_tramitacoes_mat(tram_anexada, tramitacao): + tramitacoes_deletar.append(tram_anexada.id) + Tramitacao.objects.filter(id__in=tramitacoes_deletar).delete() return HttpResponseRedirect(url) + class DetailView(MasterDetailCrud.DetailView): + + template_name = "materia/tramitacao_detail.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['user'] = self.request.user + return context + def montar_helper_documento_acessorio(self): autor_row = montar_row_autor('autor') @@ -1586,6 +1604,20 @@ class MateriaLegislativaCrud(Crud): form_class = MateriaLegislativaForm + def form_valid(self, form): + self.object = form.save() + username = self.request.user.username + + if Anexada.objects.filter(materia_principal=self.kwargs['pk']).exists(): + materia = MateriaLegislativa.objects.get(pk=self.kwargs['pk']) + anexadas = lista_anexados(materia) + + for anexada in anexadas: + anexada.em_tramitacao = True if form.instance.em_tramitacao else False + anexada.save() + + return super().form_valid(form) + @property def cancel_url(self): return self.search_url @@ -2070,11 +2102,39 @@ class MateriaAnexadaEmLoteView(PermissionRequiredMixin, FilterView): qr = self.request.GET.copy() context['object_list'] = context['object_list'].order_by( - 'ano', 'numero') + 'numero', '-ano') principal = MateriaLegislativa.objects.get(pk=self.kwargs['pk']) not_list = [self.kwargs['pk']] + \ [m for m in principal.materia_principal_set.all().values_list('materia_anexada_id', flat=True)] context['object_list'] = context['object_list'].exclude(pk__in=not_list) + + context['temp_object_list'] = context['object_list'] + context['object_list'] = [] + for obj in context['temp_object_list']: + materia_anexada = obj + ciclico = False + anexadas_anexada = Anexada.objects.filter( + materia_principal = materia_anexada + ) + + while anexadas_anexada and not ciclico: + anexadas = [] + + for anexa in anexadas_anexada: + + if principal == anexa.materia_anexada: + ciclico = True + else: + for a in Anexada.objects.filter(materia_principal=anexa.materia_anexada): + anexadas.append(a) + + anexadas_anexada = anexadas + + if not ciclico: + context['object_list'].append(obj) + + context['numero_res'] = len(context['object_list']) + context['filter_url'] = ('&' + qr.urlencode()) if len(qr) > 0 else '' context['show_results'] = show_results_filter_set(qr) @@ -2084,19 +2144,31 @@ class MateriaAnexadaEmLoteView(PermissionRequiredMixin, FilterView): def post(self, request, *args, **kwargs): marcadas = request.POST.getlist('materia_id') - if len(marcadas) == 0: - msg = _('Nenhuma máteria foi selecionada.') - messages.add_message(request, messages.ERROR, msg) - return self.get(request, self.kwargs) - data_anexacao = datetime.strptime( request.POST['data_anexacao'], "%d/%m/%Y").date() if request.POST['data_desanexacao'] == '': data_desanexacao = None + v_data_desanexacao = data_anexacao else: data_desanexacao = datetime.strptime( request.POST['data_desanexacao'], "%d/%m/%Y").date() + v_data_desanexacao = data_desanexacao + + if len(marcadas) == 0: + msg = _('Nenhuma máteria foi selecionada.') + messages.add_message(request, messages.ERROR, msg) + + if data_anexacao > v_data_desanexacao: + msg = _('Data de anexação posterior à data de desanexação.') + messages.add_message(request, messages.ERROR, msg) + + return self.get(request, self.kwargs) + + if data_anexacao > v_data_desanexacao: + msg = _('Data de anexação posterior à data de desanexação.') + messages.add_message(request, messages.ERROR, msg) + return self.get(request, self.kwargs) principal = MateriaLegislativa.objects.get(pk=kwargs['pk']) for materia in MateriaLegislativa.objects.filter(id__in=marcadas): @@ -2108,9 +2180,11 @@ class MateriaAnexadaEmLoteView(PermissionRequiredMixin, FilterView): anexada.data_desanexacao = data_desanexacao anexada.save() - msg = _('Materia(s) anexada(s).') + msg = _('Matéria(s) anexada(s).') messages.add_message(request, messages.SUCCESS, msg) - return self.get(request, self.kwargs) + + sucess_url = reverse('sapl_index') + 'materia/' + kwargs['pk'] + '/anexada' + return HttpResponseRedirect(sucess_url) class PrimeiraTramitacaoEmLoteView(PermissionRequiredMixin, FilterView): @@ -2231,6 +2305,8 @@ class PrimeiraTramitacaoEmLoteView(PermissionRequiredMixin, FilterView): messages.add_message(request, messages.ERROR, msg) return self.get(request, self.kwargs) + user = request.user + ip = get_client_ip(request) t = Tramitacao( materia=materia, data_tramitacao=data_tramitacao, @@ -2243,7 +2319,9 @@ class PrimeiraTramitacaoEmLoteView(PermissionRequiredMixin, FilterView): urgente=urgente, status_id=request.POST['status'], turno=request.POST['turno'], - texto=request.POST['texto'] + texto=request.POST['texto'], + user=user, + ip=ip ) t.save() try: diff --git a/sapl/norma/migrations/0024_auto_20190425_0917.py b/sapl/norma/migrations/0024_auto_20190425_0917.py new file mode 100644 index 000000000..078bae0ee --- /dev/null +++ b/sapl/norma/migrations/0024_auto_20190425_0917.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-04-25 12:17 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('norma', '0023_auto_20190219_1535'), + ] + + operations = [ + migrations.AlterModelOptions( + name='normarelacionada', + options={'ordering': ('norma_principal__data', 'norma_relacionada__data'), 'verbose_name': 'Norma Relacionada', 'verbose_name_plural': 'Normas Relacionadas'}, + ), + ] diff --git a/sapl/norma/models.py b/sapl/norma/models.py index 5a5557e5b..6b3a18a1f 100644 --- a/sapl/norma/models.py +++ b/sapl/norma/models.py @@ -145,11 +145,11 @@ class NormaJuridica(models.Model): def get_normas_relacionadas(self): principais = NormaRelacionada.objects.filter( - norma_principal=self.id).order_by('norma_principal__ano', - 'norma_relacionada__ano') + norma_principal=self.id).order_by('norma_principal__data', + 'norma_relacionada__data') relacionadas = NormaRelacionada.objects.filter( - norma_relacionada=self.id).order_by('norma_principal__ano', - 'norma_relacionada__ano') + norma_relacionada=self.id).order_by('norma_principal__data', + 'norma_relacionada__data') return (principais, relacionadas) def get_anexos_norma_juridica(self): @@ -313,7 +313,7 @@ class NormaRelacionada(models.Model): class Meta: verbose_name = _('Norma Relacionada') verbose_name_plural = _('Normas Relacionadas') - ordering = ('norma_principal__ano', 'norma_relacionada__ano') + ordering = ('norma_principal__data', 'norma_relacionada__data') def __str__(self): return _('Principal: %(norma_principal)s' diff --git a/sapl/parlamentares/forms.py b/sapl/parlamentares/forms.py index f52cea871..2003a4679 100755 --- a/sapl/parlamentares/forms.py +++ b/sapl/parlamentares/forms.py @@ -23,7 +23,7 @@ from sapl.rules import SAPL_GROUP_VOTANTE import django_filters from .models import (ComposicaoColigacao, Filiacao, Frente, Legislatura, - Mandato, Parlamentar, Votante) + Mandato, Parlamentar, Votante, Bloco) class ImageThumbnailFileInput(ClearableFileInput): @@ -243,6 +243,21 @@ class ParlamentarCreateForm(ParlamentarForm): attrs={'id': 'texto-rico'}) } + def clean(self): + super().clean() + + if not self.is_valid(): + return self.cleaned_data + + cleaned_data = self.cleaned_data + parlamentar = Parlamentar.objects.filter(nome_parlamentar=cleaned_data['nome_parlamentar']).exists() + + if parlamentar: + self.logger.error('Parlamentar já cadastrado.') + raise ValidationError('Parlamentar já cadastrado.') + + return cleaned_data + @transaction.atomic def save(self, commit=True): parlamentar = super(ParlamentarCreateForm, self).save(commit) @@ -568,3 +583,38 @@ class VincularParlamentarForm(forms.Form): raise ValidationError(_('Data da Expedição do Diploma deve ser anterior a data de início da Legislatura.')) return cleaned_data + + +class BlocoForm(ModelForm): + + class Meta: + model = Bloco + fields = ['nome', 'partidos', 'data_criacao', + 'data_extincao', 'descricao'] + + def clean(self): + super(BlocoForm, self).clean() + + if not self.is_valid(): + return self.cleaned_data + + if self.cleaned_data['data_extincao']: + if (self.cleaned_data['data_extincao'] < + self.cleaned_data['data_criacao']): + msg = _('Data de extinção não pode ser menor que a de criação') + raise ValidationError(msg) + return self.cleaned_data + + @transaction.atomic + def save(self, commit=True): + bloco = super(BlocoForm, self).save(commit) + content_type = ContentType.objects.get_for_model(Bloco) + object_id = bloco.pk + tipo = TipoAutor.objects.get(content_type=content_type) + Autor.objects.create( + content_type=content_type, + object_id=object_id, + tipo=tipo, + nome=bloco.nome + ) + return bloco \ No newline at end of file diff --git a/sapl/parlamentares/migrations/0026_bloco.py b/sapl/parlamentares/migrations/0026_bloco.py new file mode 100644 index 000000000..2ba9f2c07 --- /dev/null +++ b/sapl/parlamentares/migrations/0026_bloco.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-04-30 11:28 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('parlamentares', '0025_auto_20180924_1724'), + ('sessao', '0039_auto_20190430_0825') + ] + + state_operations = [ + migrations.CreateModel( + name='Bloco', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('nome', models.CharField(max_length=80, verbose_name='Nome do Bloco')), + ('data_criacao', models.DateField(null=True, verbose_name='Data Criação')), + ('data_extincao', models.DateField(blank=True, null=True, verbose_name='Data Dissolução')), + ('descricao', models.TextField(blank=True, verbose_name='Descrição')), + ('partidos', models.ManyToManyField(blank=True, to='parlamentares.Partido', verbose_name='Partidos')), + ], + options={ + 'db_table': 'parlamentares_bloco', + 'verbose_name': 'Bloco Parlamentar', + 'verbose_name_plural': 'Blocos Parlamentares', + }, + bases=(models.Model,), + ), + ] + + operations = [ + migrations.SeparateDatabaseAndState(state_operations=state_operations) + ] diff --git a/sapl/parlamentares/migrations/0027_auto_20190430_0839.py b/sapl/parlamentares/migrations/0027_auto_20190430_0839.py new file mode 100644 index 000000000..454101416 --- /dev/null +++ b/sapl/parlamentares/migrations/0027_auto_20190430_0839.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-04-30 11:39 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('parlamentares', '0026_bloco'), + ] + + operations = [ + migrations.AlterModelTable( + name='bloco', + table=None, + ), + ] diff --git a/sapl/parlamentares/models.py b/sapl/parlamentares/models.py index d88f2fd87..d50600eba 100644 --- a/sapl/parlamentares/models.py +++ b/sapl/parlamentares/models.py @@ -568,3 +568,37 @@ class Votante(models.Model): def __str__(self): return self.user.username + + +@reversion.register() +class Bloco(models.Model): + ''' + * blocos podem existir por mais de uma legislatura + ''' + nome = models.CharField( + max_length=80, verbose_name=_('Nome do Bloco')) + partidos = models.ManyToManyField( + Partido, blank=True, verbose_name=_('Partidos')) + data_criacao = models.DateField( + blank=False, null=True, verbose_name=_('Data Criação')) + data_extincao = models.DateField( + blank=True, null=True, verbose_name=_('Data Dissolução')) + descricao = models.TextField(blank=True, verbose_name=_('Descrição')) + + # campo conceitual de reversão genérica para o model Autor que dá a + # o meio possível de localização de tipos de autores. + autor = SaplGenericRelation(Autor, + related_query_name='bloco_set', + fields_search=( + ('nome', '__icontains'), + ('descricao', '__icontains'), + ('partidos__sigla', '__icontains'), + ('partidos__nome', '__icontains'), + )) + + class Meta: + verbose_name = _('Bloco Parlamentar') + verbose_name_plural = _('Blocos Parlamentares') + + def __str__(self): + return self.nome diff --git a/sapl/parlamentares/urls.py b/sapl/parlamentares/urls.py index 51fb856b4..4c1434333 100644 --- a/sapl/parlamentares/urls.py +++ b/sapl/parlamentares/urls.py @@ -18,7 +18,7 @@ from sapl.parlamentares.views import (CargoMesaCrud, ColigacaoCrud, insere_parlamentar_composicao, parlamentares_frente_selected, remove_parlamentar_composicao, - parlamentares_filiados, + parlamentares_filiados, BlocoCrud, PesquisarParlamentarView, VincularParlamentarView) from .apps import AppConfig @@ -46,8 +46,9 @@ urlpatterns = [ url(r'^sistema/coligacao/', include(ColigacaoCrud.get_urls() + - ComposicaoColigacaoCrud.get_urls())), - + ComposicaoColigacaoCrud.get_urls())), + url(r'^sistema/bloco/', + include(BlocoCrud.get_urls())), url(r'^sistema/frente/', include(FrenteCrud.get_urls())), url(r'^sistema/frente/atualiza-lista-parlamentares', @@ -89,4 +90,5 @@ urlpatterns = [ url(r'^mesa-diretora/remove-parlamentar-composicao/$', remove_parlamentar_composicao, name='remove_parlamentar_composicao'), + ] diff --git a/sapl/parlamentares/views.py b/sapl/parlamentares/views.py index d5e8cc41a..874f53385 100644 --- a/sapl/parlamentares/views.py +++ b/sapl/parlamentares/views.py @@ -34,11 +34,15 @@ from sapl.parlamentares.apps import AppConfig from sapl.utils import (parlamentares_ativos, show_results_filter_set) from .forms import (FiliacaoForm, FrenteForm, LegislaturaForm, MandatoForm, - ParlamentarCreateForm, ParlamentarForm, VotanteForm, ParlamentarFilterSet, VincularParlamentarForm) + ParlamentarCreateForm, ParlamentarForm, VotanteForm, + ParlamentarFilterSet, VincularParlamentarForm, + BlocoForm) + from .models import (CargoMesa, Coligacao, ComposicaoColigacao, ComposicaoMesa, Dependente, Filiacao, Frente, Legislatura, Mandato, NivelInstrucao, Parlamentar, Partido, SessaoLegislativa, - SituacaoMilitar, TipoAfastamento, TipoDependente, Votante) + SituacaoMilitar, TipoAfastamento, TipoDependente, Votante, + Bloco) CargoMesaCrud = CrudAux.build(CargoMesa, 'cargo_mesa') @@ -249,7 +253,6 @@ class ParticipacaoParlamentarCrud(CrudBaseForListAndDetailExternalAppView): comissoes = [] for p in object_list: - if p.cargo.nome != 'Relator': comissao = [ (p.composicao.comissao.nome, reverse( 'sapl.comissoes:comissao_detail', kwargs={ @@ -1112,16 +1115,20 @@ def altera_field_mesa_public_view(request): partido_parlamentar_sessao_legislativa(sessao, parlamentar)) if parlamentar.fotografia: - thumbnail_url = get_backend().get_thumbnail_url( - parlamentar.fotografia, - { - 'size': (128, 128), - 'box': parlamentar.cropping, - 'crop': True, - 'detail': True, - } - ) - lista_fotos.append(thumbnail_url) + try: + thumbnail_url = get_backend().get_thumbnail_url( + parlamentar.fotografia, + { + 'size': (128, 128), + 'box': parlamentar.cropping, + 'crop': True, + 'detail': True, + } + ) + lista_fotos.append(thumbnail_url) + except Exception as e: + logger.error(e) + logger.error('erro processando arquivo: %s' % parlamentar.fotografia.path) else: lista_fotos.append(None) @@ -1160,3 +1167,13 @@ class VincularParlamentarView(PermissionRequiredMixin, FormView): mandato.save() return HttpResponseRedirect(self.get_success_url()) + + +class BlocoCrud(CrudAux): + model = Bloco + + class CreateView(CrudAux.CreateView): + form_class = BlocoForm + + def get_success_url(self): + return reverse('sapl.parlamentares:bloco_list') \ No newline at end of file diff --git a/sapl/protocoloadm/forms.py b/sapl/protocoloadm/forms.py index 915f62445..a8630539c 100644 --- a/sapl/protocoloadm/forms.py +++ b/sapl/protocoloadm/forms.py @@ -7,7 +7,7 @@ from crispy_forms.layout import HTML, Button, Column, Fieldset, Layout, Div from django import forms from django.core.exceptions import (MultipleObjectsReturned, ObjectDoesNotExist, ValidationError) -from django.db import models +from django.db import models, transaction from django.db.models import Max from django.forms import ModelForm from django.utils import timezone @@ -24,12 +24,12 @@ from sapl.utils import (RANGE_ANOS, YES_NO_CHOICES, AnoNumeroOrderingFilter, choice_anos_com_protocolo, choice_force_optional, choice_anos_com_documentoadministrativo, FilterOverridesMetaMixin, choice_anos_com_materias, - FileFieldCheckMixin) + FileFieldCheckMixin, lista_anexados) from .models import (AcompanhamentoDocumento, DocumentoAcessorioAdministrativo, DocumentoAdministrativo, Protocolo, TipoDocumentoAdministrativo, - TramitacaoAdministrativo) + TramitacaoAdministrativo, Anexado) TIPOS_PROTOCOLO = [('0', 'Recebido'), ('1', 'Enviado'), @@ -221,7 +221,7 @@ class DocumentoAdministrativoFilterSet(django_filters.FilterSet): ) -class AnularProcoloAdmForm(ModelForm): +class AnularProtocoloAdmForm(ModelForm): logger = logging.getLogger(__name__) @@ -240,7 +240,7 @@ class AnularProcoloAdmForm(ModelForm): widget=forms.Textarea) def clean(self): - super(AnularProcoloAdmForm, self).clean() + super(AnularProtocoloAdmForm, self).clean() cleaned_data = self.cleaned_data @@ -313,7 +313,7 @@ class AnularProcoloAdmForm(ModelForm): form_actions(label='Anular') ) ) - super(AnularProcoloAdmForm, self).__init__( + super(AnularProtocoloAdmForm, self).__init__( *args, **kwargs) @@ -650,11 +650,29 @@ class TramitacaoAdmForm(ModelForm): fields = ['data_tramitacao', 'unidade_tramitacao_local', 'status', + 'urgente', 'unidade_tramitacao_destino', 'data_encaminhamento', 'data_fim_prazo', 'texto', - ] + 'user', + 'ip'] + widgets = {'user': forms.HiddenInput(), + 'ip': forms.HiddenInput()} + + + def __init__(self, *args, **kwargs): + super(TramitacaoAdmForm, self).__init__(*args, **kwargs) + self.fields['data_tramitacao'].initial = timezone.now().date() + ust = UnidadeTramitacao.objects.select_related().all() + unidade_tramitacao_destino = [('', '---------')] + [(ut.pk, ut) + for ut in ust if ut.comissao and ut.comissao.ativa] + unidade_tramitacao_destino.extend( + [(ut.pk, ut) for ut in ust if ut.orgao]) + unidade_tramitacao_destino.extend( + [(ut.pk, ut) for ut in ust if ut.parlamentar]) + self.fields['unidade_tramitacao_destino'].choices = unidade_tramitacao_destino + self.fields['urgente'].label = "Urgente? *" def clean(self): cleaned_data = super(TramitacaoAdmForm, self).clean() @@ -727,6 +745,51 @@ class TramitacaoAdmForm(ModelForm): return self.cleaned_data + @transaction.atomic + def save(self, commit=True): + tramitacao = super(TramitacaoAdmForm, self).save(commit) + documento = tramitacao.documento + documento.tramitacao = False if tramitacao.status.indicador == "F" else True + documento.save() + + lista_tramitacao = [] + list_anexados = lista_anexados(documento, False) + for da in list_anexados: + if not da.tramitacaoadministrativo_set.all() \ + or da.tramitacaoadministrativo_set.last() \ + .unidade_tramitacao_destino == tramitacao.unidade_tramitacao_local: + da.tramitacao = False if tramitacao.status.indicador == "F" else True + da.save() + lista_tramitacao.append(TramitacaoAdministrativo( + status=tramitacao.status, + documento=da, + data_tramitacao=tramitacao.data_tramitacao, + unidade_tramitacao_local=tramitacao.unidade_tramitacao_local, + data_encaminhamento=tramitacao.data_encaminhamento, + unidade_tramitacao_destino=tramitacao.unidade_tramitacao_destino, + urgente=tramitacao.urgente, + texto=tramitacao.texto, + data_fim_prazo=tramitacao.data_fim_prazo, + user=tramitacao.user, + ip=tramitacao.ip + )) + TramitacaoAdministrativo.objects.bulk_create(lista_tramitacao) + + return tramitacao + + + +# Compara se os campos de duas tramitações são iguais, +# exceto os campos id, documento_id e timestamp +def compara_tramitacoes_doc(tramitacao1, tramitacao2): + if not tramitacao1 or not tramitacao2: + return False + + lst_items = ['id', 'documento_id', 'timestamp'] + values = [(k,v) for k,v in tramitacao1.__dict__.items() if ((k not in lst_items) and (k[0] != '_'))] + other_values = [(k,v) for k,v in tramitacao2.__dict__.items() if (k not in lst_items and k[0] != '_')] + return values == other_values + class TramitacaoAdmEditForm(TramitacaoAdmForm): @@ -742,12 +805,16 @@ class TramitacaoAdmEditForm(TramitacaoAdmForm): model = TramitacaoAdministrativo fields = ['data_tramitacao', 'unidade_tramitacao_local', - 'status', + 'status', + 'urgente', 'unidade_tramitacao_destino', 'data_encaminhamento', 'data_fim_prazo', 'texto', - ] + 'user', + 'ip'] + widgets = {'user': forms.HiddenInput(), + 'ip': forms.HiddenInput()} def clean(self): super(TramitacaoAdmEditForm, self).clean() @@ -755,30 +822,190 @@ class TramitacaoAdmEditForm(TramitacaoAdmForm): if not self.is_valid(): return self.cleaned_data + cd = self.cleaned_data + obj = self.instance + ultima_tramitacao = TramitacaoAdministrativo.objects.filter( - documento_id=self.instance.documento_id).order_by( + documento_id=obj.documento_id).order_by( '-data_tramitacao', '-id').first() # Se a Tramitação que está sendo editada não for a mais recente, # ela não pode ter seu destino alterado. - if ultima_tramitacao != self.instance: - if self.cleaned_data['unidade_tramitacao_destino'] != \ - self.instance.unidade_tramitacao_destino: + if ultima_tramitacao != obj: + if cd['unidade_tramitacao_destino'] != \ + obj.unidade_tramitacao_destino: self.logger.error('Você não pode mudar a Unidade de Destino desta ' 'tramitação (id={}), pois irá conflitar com a Unidade ' - 'Local da tramitação seguinte'.format(self.instance.documento_id)) + 'Local da tramitação seguinte'.format(obj.documento_id)) raise ValidationError( 'Você não pode mudar a Unidade de Destino desta ' 'tramitação, pois irá conflitar com a Unidade ' 'Local da tramitação seguinte') - self.cleaned_data['data_tramitacao'] = \ - self.instance.data_tramitacao - self.cleaned_data['unidade_tramitacao_local'] = \ - self.instance.unidade_tramitacao_local + # Se não houve qualquer alteração em um dos dados, mantém o usuário e ip + if not (cd['data_tramitacao'] != obj.data_tramitacao or \ + cd['unidade_tramitacao_destino'] != obj.unidade_tramitacao_destino or \ + cd['status'] != obj.status or cd['texto'] != obj.texto or \ + cd['data_encaminhamento'] != obj.data_encaminhamento or \ + cd['data_fim_prazo'] != obj.data_fim_prazo): + cd['user'] = obj.user + cd['ip'] = obj.ip - return self.cleaned_data + cd['data_tramitacao'] = obj.data_tramitacao + cd['unidade_tramitacao_local'] = obj.unidade_tramitacao_local + + return cd + + + @transaction.atomic + def save(self, commit=True): + # tram_principal = super(TramitacaoAdmEditForm, self).save(commit) + ant_tram_principal = TramitacaoAdministrativo.objects.get(id=self.instance.id) + nova_tram_principal = super(TramitacaoAdmEditForm, self).save(commit) + documento = nova_tram_principal.documento + documento.tramitacao = False if nova_tram_principal.status.indicador == "F" else True + documento.save() + + list_anexados = lista_anexados(documento, False) + for da in list_anexados: + tram_anexada = da.tramitacaoadministrativo_set.last() + if compara_tramitacoes_doc(ant_tram_principal, tram_anexada): + tram_anexada.status = nova_tram_principal.status + tram_anexada.data_tramitacao = nova_tram_principal.data_tramitacao + tram_anexada.unidade_tramitacao_local = nova_tram_principal.unidade_tramitacao_local + tram_anexada.data_encaminhamento = nova_tram_principal.data_encaminhamento + tram_anexada.unidade_tramitacao_destino = nova_tram_principal.unidade_tramitacao_destino + tram_anexada.urgente = nova_tram_principal.urgente + tram_anexada.texto = nova_tram_principal.texto + tram_anexada.data_fim_prazo = nova_tram_principal.data_fim_prazo + tram_anexada.user = nova_tram_principal.user + tram_anexada.ip = nova_tram_principal.ip + tram_anexada.save() + + da.tramitacao = False if nova_tram_principal.status.indicador == "F" else True + da.save() + return nova_tram_principal + + +class AnexadoForm(ModelForm): + + logger = logging.getLogger(__name__) + + tipo = forms.ModelChoiceField( + label='Tipo', + required=True, + queryset=TipoDocumentoAdministrativo.objects.all(), + empty_label='Selecione' + ) + + numero = forms.CharField(label='Número', required=True) + + ano = forms.CharField(label='Ano', required=True) + + def __init__(self, *args, **kwargs): + return super(AnexadoForm, self).__init__(*args, **kwargs) + + def clean(self): + super(AnexadoForm, self).clean() + + if not self.is_valid(): + return self.cleaned_data + + cleaned_data = self.cleaned_data + + data_anexacao = cleaned_data['data_anexacao'] + data_desanexacao = cleaned_data['data_desanexacao'] if cleaned_data['data_desanexacao'] else data_anexacao + + if data_anexacao > data_desanexacao: + self.logger.error("Data de anexação posterior à data de desanexação.") + raise ValidationError(_("Data de anexação posterior à data de desanexação.")) + try: + self.logger.info( + "Tentando obter objeto DocumentoAdministrativo (numero={}, ano={}, tipo={})." + .format(cleaned_data['numero'], cleaned_data['ano'], cleaned_data['tipo']) + ) + documento_anexado = DocumentoAdministrativo.objects.get( + numero=cleaned_data['numero'], + ano=cleaned_data['ano'], + tipo=cleaned_data['tipo'] + ) + except ObjectDoesNotExist: + msg = _('O {} {}/{} não existe no cadastro de documentos administrativos.' + .format(cleaned_data['tipo'], cleaned_data['numero'], cleaned_data['ano'])) + self.logger.error("O documento a ser anexado não existe no cadastro" + " de documentos administrativos") + raise ValidationError(msg) + + documento_principal = self.instance.documento_principal + if documento_principal == documento_anexado: + self.logger.error("O documento não pode ser anexado a si mesmo.") + raise ValidationError(_("O documento não pode ser anexado a si mesmo")) + + is_anexado = Anexado.objects.filter(documento_principal=documento_principal, + documento_anexado=documento_anexado + ).exclude(pk=self.instance.pk).exists() + + if is_anexado: + self.logger.error("Documento já se encontra anexado.") + raise ValidationError(_('Documento já se encontra anexado')) + + ciclico = False + anexados_anexado = Anexado.objects.filter(documento_principal=documento_anexado) + + while(anexados_anexado and not ciclico): + anexados = [] + + for anexo in anexados_anexado: + + if documento_principal == anexo.documento_anexado: + ciclico = True + else: + for a in Anexado.objects.filter(documento_principal=anexo.documento_anexado): + anexados.append(a) + + anexados_anexado = anexados + + if ciclico: + self.logger.error("O documento não pode ser anexado por um de seus anexados.") + raise ValidationError(_('O documento não pode ser anexado por um de seus anexados')) + + cleaned_data['documento_anexado'] = documento_anexado + + return cleaned_data + + def save(self, commit=False): + anexado = super(AnexadoForm, self).save(commit) + anexado.documento_anexado = self.cleaned_data['documento_anexado'] + anexado.save() + return anexado + + class Meta: + model = Anexado + fields = ['tipo', 'numero', 'ano', 'data_anexacao', 'data_desanexacao'] + + +class AnexadoEmLoteFilterSet(django_filters.FilterSet): + + class Meta(FilterOverridesMetaMixin): + model = DocumentoAdministrativo + fields = ['tipo', 'data'] + + def __init__(self, *args, **kwargs): + super(AnexadoEmLoteFilterSet, self).__init__(*args, **kwargs) + + self.filters['tipo'].label = 'Tipo de Documento*' + self.filters['data'].label = 'Data (Inicial - Final)*' + + row1 = to_row([('tipo', 12)]) + row2 = to_row([('data', 12)]) + + self.form.helper = SaplFormHelper() + self.form.helper.form_method = 'GET' + self.form.helper.layout = Layout( + Fieldset(_('Pesquisa de Documentos'), + row1, row2, form_actions(label='Pesquisar')) + ) class DocumentoAdministrativoForm(FileFieldCheckMixin, ModelForm): diff --git a/sapl/protocoloadm/migrations/0018_auto_20190314_1532.py b/sapl/protocoloadm/migrations/0018_auto_20190314_1532.py new file mode 100644 index 000000000..20822400c --- /dev/null +++ b/sapl/protocoloadm/migrations/0018_auto_20190314_1532.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-03-14 18:32 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('protocoloadm', '0017_merge_20190121_1552'), + ] + + operations = [ + migrations.CreateModel( + name='Anexado', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('data_anexacao', models.DateField(verbose_name='Data Anexação')), + ('data_desanexacao', models.DateField(blank=True, null=True, verbose_name='Data Desanexação')), + ('documento_anexado', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documento_anexado_set', to='protocoloadm.DocumentoAdministrativo', verbose_name='Documento Anexado')), + ('documento_principal', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documento_principal_set', to='protocoloadm.DocumentoAdministrativo', verbose_name='Documento Principal')), + ], + options={ + 'verbose_name': 'Anexado', + 'verbose_name_plural': 'Anexados', + }, + ), + migrations.AddField( + model_name='documentoadministrativo', + name='anexados', + field=models.ManyToManyField(blank=True, related_name='anexo_de', through='protocoloadm.Anexado', to='protocoloadm.DocumentoAdministrativo'), + ), + ] diff --git a/sapl/protocoloadm/migrations/0019_auto_20190426_0833.py b/sapl/protocoloadm/migrations/0019_auto_20190426_0833.py new file mode 100644 index 000000000..edfdd8904 --- /dev/null +++ b/sapl/protocoloadm/migrations/0019_auto_20190426_0833.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-04-26 11:33 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('protocoloadm', '0018_auto_20190314_1532'), + ] + + operations = [ + migrations.AddField( + model_name='tramitacaoadministrativo', + name='ip', + field=models.CharField(blank=True, default='', max_length=30, verbose_name='IP'), + ), + migrations.AddField( + model_name='tramitacaoadministrativo', + name='user', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='Usuário'), + ), + ] diff --git a/sapl/protocoloadm/migrations/0019_auto_20190429_0828.py b/sapl/protocoloadm/migrations/0019_auto_20190429_0828.py new file mode 100644 index 000000000..204db8598 --- /dev/null +++ b/sapl/protocoloadm/migrations/0019_auto_20190429_0828.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-04-29 11:28 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('protocoloadm', '0018_auto_20190314_1532'), + ] + + operations = [ + migrations.AddField( + model_name='tramitacaoadministrativo', + name='urgente', + field=models.BooleanField(choices=[(True, 'Sim'), (False, 'Não')], default=False, verbose_name='Urgente ?'), + ), + migrations.AlterField( + model_name='tramitacaoadministrativo', + name='texto', + field=models.TextField(verbose_name='Texto da Ação'), + ), + ] diff --git a/sapl/protocoloadm/migrations/0020_tramitacaoadministrativo_timestamp.py b/sapl/protocoloadm/migrations/0020_tramitacaoadministrativo_timestamp.py new file mode 100644 index 000000000..0e367b306 --- /dev/null +++ b/sapl/protocoloadm/migrations/0020_tramitacaoadministrativo_timestamp.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-04-29 12:15 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('protocoloadm', '0019_auto_20190429_0828'), + ] + + operations = [ + migrations.AddField( + model_name='tramitacaoadministrativo', + name='timestamp', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + ] diff --git a/sapl/protocoloadm/migrations/0021_merge_20190429_1531.py b/sapl/protocoloadm/migrations/0021_merge_20190429_1531.py new file mode 100644 index 000000000..4613b46ce --- /dev/null +++ b/sapl/protocoloadm/migrations/0021_merge_20190429_1531.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-04-29 18:31 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('protocoloadm', '0020_tramitacaoadministrativo_timestamp'), + ('protocoloadm', '0019_auto_20190426_0833'), + ] + + operations = [ + ] diff --git a/sapl/protocoloadm/models.py b/sapl/protocoloadm/models.py index e335a5db1..8396349a8 100644 --- a/sapl/protocoloadm/models.py +++ b/sapl/protocoloadm/models.py @@ -6,7 +6,8 @@ import reversion from sapl.base.models import Autor from sapl.materia.models import TipoMateriaLegislativa, UnidadeTramitacao -from sapl.utils import RANGE_ANOS, YES_NO_CHOICES, texto_upload_path +from sapl.utils import (RANGE_ANOS, YES_NO_CHOICES, texto_upload_path, + get_settings_auth_user_model) @reversion.register() @@ -170,6 +171,18 @@ class DocumentoAdministrativo(models.Model): verbose_name=_('Acesso Restrito'), blank=True) + anexados = models.ManyToManyField( + 'self', + blank=True, + through='Anexado', + symmetrical=False, + related_name='anexo_de', + through_fields=( + 'documento_principal', + 'documento_anexado' + ) + ) + class Meta: verbose_name = _('Documento Administrativo') verbose_name_plural = _('Documentos Administrativos') @@ -288,6 +301,7 @@ class TramitacaoAdministrativo(models.Model): verbose_name=_('Status')) documento = models.ForeignKey(DocumentoAdministrativo, on_delete=models.PROTECT) + timestamp = models.DateTimeField(default=timezone.now) data_tramitacao = models.DateField( verbose_name=_('Data Tramitação')) unidade_tramitacao_local = models.ForeignKey( @@ -302,10 +316,21 @@ class TramitacaoAdministrativo(models.Model): related_name='adm_tramitacoes_destino', on_delete=models.PROTECT, verbose_name=_('Unidade Destino')) - texto = models.TextField( - blank=True, verbose_name=_('Texto da Ação')) + urgente = models.BooleanField(verbose_name=_('Urgente ?'), + choices=YES_NO_CHOICES, + default=False) + texto = models.TextField(verbose_name=_('Texto da Ação')) data_fim_prazo = models.DateField( blank=True, null=True, verbose_name=_('Data Fim do Prazo')) + user = models.ForeignKey(get_settings_auth_user_model(), + verbose_name=_('Usuário'), + on_delete=models.PROTECT, + null=True, + blank=True) + ip = models.CharField(verbose_name=_('IP'), + max_length=30, + blank=True, + default='') class Meta: verbose_name = _('Tramitação de Documento Administrativo') @@ -317,6 +342,36 @@ class TramitacaoAdministrativo(models.Model): } +@reversion.register() +class Anexado(models.Model): + documento_principal = models.ForeignKey( + DocumentoAdministrativo, related_name='documento_principal_set', + on_delete = models.CASCADE, + verbose_name=_('Documento Principal') + ) + documento_anexado = models.ForeignKey( + DocumentoAdministrativo, related_name='documento_anexado_set', + on_delete = models.CASCADE, + verbose_name=_('Documento Anexado') + ) + data_anexacao = models.DateField(verbose_name=_('Data Anexação')) + data_desanexacao = models.DateField( + blank=True, null=True, verbose_name=_('Data Desanexação') + ) + + class Meta: + verbose_name = _('Anexado') + verbose_name_plural = _('Anexados') + + def __str__(self): + return _('Anexado: %(documento_anexado_tipo)s %(documento_anexado_numero)s' + '/%(documento_anexado_ano)s\n') % { + 'documento_anexado_tipo': self.documento_anexado.tipo, + 'documento_anexado_numero': self.documento_anexado.numero, + 'documento_anexado_ano': self.documento_anexado.ano + } + + @reversion.register() class AcompanhamentoDocumento(models.Model): usuario = models.CharField(max_length=50) diff --git a/sapl/protocoloadm/tests/test_protocoloadm.py b/sapl/protocoloadm/tests/test_protocoloadm.py index 0e43aebbe..ce322a7ba 100644 --- a/sapl/protocoloadm/tests/test_protocoloadm.py +++ b/sapl/protocoloadm/tests/test_protocoloadm.py @@ -8,16 +8,20 @@ from model_mommy import mommy import pytest from sapl.base.models import AppConfig +from sapl.comissoes.models import Comissao, TipoComissao from sapl.materia.models import UnidadeTramitacao -from sapl.protocoloadm.forms import (AnularProcoloAdmForm, +from sapl.protocoloadm.forms import (AnularProtocoloAdmForm, DocumentoAdministrativoForm, MateriaLegislativa, ProtocoloDocumentForm, - ProtocoloMateriaForm) + ProtocoloMateriaForm, TramitacaoAdmForm, + TramitacaoAdmEditForm, + compara_tramitacoes_doc) from sapl.protocoloadm.models import (DocumentoAdministrativo, Protocolo, StatusTramitacaoAdministrativo, TipoDocumentoAdministrativo, - TipoMateriaLegislativa, + TipoMateriaLegislativa, Anexado, TramitacaoAdministrativo) +from sapl.utils import lista_anexados @pytest.mark.django_db(transaction=False) @@ -51,7 +55,7 @@ def test_anular_protocolo_submit(admin_client): @pytest.mark.django_db(transaction=False) def test_form_anular_protocolo_inexistente(): - form = AnularProcoloAdmForm({'numero': '1', + form = AnularProtocoloAdmForm({'numero': '1', 'ano': '2016', 'justificativa_anulacao': 'TESTE'}) @@ -64,7 +68,7 @@ def test_form_anular_protocolo_inexistente(): @pytest.mark.django_db(transaction=False) def test_form_anular_protocolo_valido(): mommy.make(Protocolo, numero='1', ano='2016', anulado=False) - form = AnularProcoloAdmForm({'numero': '1', + form = AnularProtocoloAdmForm({'numero': '1', 'ano': '2016', 'justificativa_anulacao': 'TESTE'}) if not form.is_valid(): @@ -74,7 +78,7 @@ def test_form_anular_protocolo_valido(): @pytest.mark.django_db(transaction=False) def test_form_anular_protocolo_anulado(): mommy.make(Protocolo, numero='1', ano='2016', anulado=True) - form = AnularProcoloAdmForm({'numero': '1', + form = AnularProtocoloAdmForm({'numero': '1', 'ano': '2016', 'justificativa_anulacao': 'TESTE'}) assert form.errors['__all__'] == \ @@ -88,7 +92,7 @@ def test_form_anular_protocolo_campos_obrigatorios(): # TODO: generalizar para diminuir o tamanho deste método # numero ausente - form = AnularProcoloAdmForm({'numero': '', + form = AnularProtocoloAdmForm({'numero': '', 'ano': '2016', 'justificativa_anulacao': 'TESTE'}) if form.is_valid(): @@ -98,7 +102,7 @@ def test_form_anular_protocolo_campos_obrigatorios(): assert form.errors['numero'] == [_('Este campo é obrigatório.')] # ano ausente - form = AnularProcoloAdmForm({'numero': '1', + form = AnularProtocoloAdmForm({'numero': '1', 'ano': '', 'justificativa_anulacao': 'TESTE'}) if form.is_valid(): @@ -108,7 +112,7 @@ def test_form_anular_protocolo_campos_obrigatorios(): assert form.errors['ano'] == [_('Este campo é obrigatório.')] # justificativa_anulacao ausente - form = AnularProcoloAdmForm({'numero': '1', + form = AnularProtocoloAdmForm({'numero': '1', 'ano': '2016', 'justificativa_anulacao': ''}) if form.is_valid(): @@ -157,12 +161,14 @@ def test_create_tramitacao(admin_client): 'unidade_tramitacao_destino': unidade_tramitacao_local_1.pk, 'documento': documento_adm.pk, 'status': status.pk, + 'urgente': False, + 'texto': 'teste', 'data_tramitacao': date(2016, 8, 21)}, follow=True) msg = force_text(_('A origem da nova tramitação deve ser igual ao ' 'destino da última adicionada!')) - + # Verifica se a origem da nova tramitacao é igual ao destino da última assert msg in response.context_data[ 'form'].errors['__all__'] @@ -175,6 +181,8 @@ def test_create_tramitacao(admin_client): 'unidade_tramitacao_destino': unidade_tramitacao_destino_2.pk, 'documento': documento_adm.pk, 'status': status.pk, + 'urgente': False, + 'texto': 'teste', 'data_tramitacao': date(2016, 8, 20)}, follow=True) @@ -193,6 +201,8 @@ def test_create_tramitacao(admin_client): 'unidade_tramitacao_destino': unidade_tramitacao_destino_2.pk, 'documento': documento_adm.pk, 'status': status.pk, + 'urgente': False, + 'texto': 'teste', 'data_tramitacao': timezone.now().date() + timedelta( days=1)}, follow=True) @@ -212,6 +222,8 @@ def test_create_tramitacao(admin_client): 'unidade_tramitacao_destino': unidade_tramitacao_destino_2.pk, 'documento': documento_adm.pk, 'status': status.pk, + 'urgente': False, + 'texto': 'teste', 'data_tramitacao': date(2016, 8, 21), 'data_encaminhamento': date(2016, 8, 20)}, follow=True) @@ -231,6 +243,8 @@ def test_create_tramitacao(admin_client): 'unidade_tramitacao_destino': unidade_tramitacao_destino_2.pk, 'documento': documento_adm.pk, 'status': status.pk, + 'urgente': False, + 'texto': 'teste', 'data_tramitacao': date(2016, 8, 21), 'data_fim_prazo': date(2016, 8, 20)}, follow=True) @@ -250,6 +264,8 @@ def test_create_tramitacao(admin_client): 'unidade_tramitacao_destino': unidade_tramitacao_destino_2.pk, 'documento': documento_adm.pk, 'status': status.pk, + 'urgente': False, + 'texto': 'teste', 'data_tramitacao': date(2016, 8, 21)}, follow=True) @@ -261,7 +277,7 @@ def test_create_tramitacao(admin_client): @pytest.mark.django_db(transaction=False) def test_anular_protocolo_dados_invalidos(): - form = AnularProcoloAdmForm(data={}) + form = AnularProtocoloAdmForm(data={}) assert not form.is_valid() @@ -276,10 +292,10 @@ def test_anular_protocolo_dados_invalidos(): @pytest.mark.django_db(transaction=False) def test_anular_protocolo_form_anula_protocolo_inexistente(): - form = AnularProcoloAdmForm(data={'numero': '1', + form = AnularProtocoloAdmForm(data={'numero': '1', 'ano': '2017', 'justificativa_anulacao': 'teste' - }) + }) assert not form.is_valid() @@ -291,10 +307,10 @@ def test_anular_protocolo_form_anula_protocolo_inexistente(): def test_anular_protocolo_form_anula_protocolo_anulado(): mommy.make(Protocolo, numero=1, ano=2017, anulado=True) - form = AnularProcoloAdmForm(data={'numero': '1', + form = AnularProtocoloAdmForm(data={'numero': '1', 'ano': '2017', 'justificativa_anulacao': 'teste' - }) + }) assert not form.is_valid() @@ -316,10 +332,10 @@ def test_anular_protocolo_form_anula_protocolo_com_doc_vinculado(): ano=2017, numero_protocolo=1) - form = AnularProcoloAdmForm(data={'numero': '1', + form = AnularProtocoloAdmForm(data={'numero': '1', 'ano': '2017', 'justificativa_anulacao': 'teste' - }) + }) assert not form.is_valid() @@ -338,10 +354,10 @@ def test_anular_protocolo_form_anula_protocolo_com_doc_vinculado(): mommy.make(DocumentoAdministrativo, protocolo=protocolo_documento) - form = AnularProcoloAdmForm(data={'numero': '2', + form = AnularProtocoloAdmForm(data={'numero': '2', 'ano': '2017', 'justificativa_anulacao': 'teste' - }) + }) assert not form.is_valid() @@ -443,3 +459,247 @@ def test_protocolo_materia_invalido(): assert errors['vincular_materia'] == [_('Este campo é obrigatório.')] assert len(errors) == 7 + + +@pytest.mark.django_db(transaction=False) +def test_lista_documentos_anexados(): + tipo_documento = mommy.make( + TipoDocumentoAdministrativo, + descricao="Tipo_Teste" + ) + documento_principal = mommy.make( + DocumentoAdministrativo, + numero=20, + ano=2018, + data="2018-01-04", + tipo=tipo_documento + ) + documento_anexado = mommy.make( + DocumentoAdministrativo, + numero=21, + ano=2019, + data="2019-05-04", + tipo=tipo_documento + ) + documento_anexado_anexado = mommy.make( + DocumentoAdministrativo, + numero=22, + ano=2020, + data="2020-01-05", + tipo=tipo_documento + ) + + mommy.make( + Anexado, + documento_principal=documento_principal, + documento_anexado=documento_anexado, + data_anexacao="2019-05-11" + ) + mommy.make( + Anexado, + documento_principal=documento_anexado, + documento_anexado=documento_anexado_anexado, + data_anexacao="2020-11-05" + ) + + lista = lista_anexados(documento_principal, False) + + assert len(lista) == 2 + assert lista[0] == documento_anexado + assert lista[1] == documento_anexado_anexado + + +@pytest.mark.django_db(transaction=False) +def make_unidade_tramitacao(descricao): + # Cria uma comissão para ser a unidade de tramitação + tipo_comissao = mommy.make(TipoComissao) + comissao = mommy.make(Comissao, + tipo=tipo_comissao, + nome=descricao, + sigla='T', + data_criacao='2016-03-21') + + # Testa a comissão + assert comissao.tipo == tipo_comissao + assert comissao.nome == descricao + + # Cria a unidade + unidade = mommy.make(UnidadeTramitacao, comissao=comissao) + assert unidade.comissao == comissao + + return unidade + + +@pytest.mark.django_db(transaction=False) +def test_tramitacoes_documentos_anexados(admin_client): + tipo_documento = mommy.make( + TipoDocumentoAdministrativo, + descricao="Tipo_Teste" + ) + documento_principal = mommy.make( + DocumentoAdministrativo, + numero=20, + ano=2018, + data="2018-01-04", + tipo=tipo_documento + ) + documento_anexado = mommy.make( + DocumentoAdministrativo, + numero=21, + ano=2019, + data="2019-05-04", + tipo=tipo_documento + ) + documento_anexado_anexado = mommy.make( + DocumentoAdministrativo, + numero=22, + ano=2020, + data="2020-01-05", + tipo=tipo_documento + ) + + mommy.make( + Anexado, + documento_principal=documento_principal, + documento_anexado=documento_anexado, + data_anexacao="2019-05-11" + ) + mommy.make( + Anexado, + documento_principal=documento_anexado, + documento_anexado=documento_anexado_anexado, + data_anexacao="2020-11-05" + ) + + + unidade_tramitacao_local_1 = make_unidade_tramitacao(descricao="Teste 1") + unidade_tramitacao_destino_1 = make_unidade_tramitacao(descricao="Teste 2") + unidade_tramitacao_destino_2 = make_unidade_tramitacao(descricao="Teste 3") + + status = mommy.make( + StatusTramitacaoAdministrativo, + indicador='R') + + # Teste criação de Tramitacao + form = TramitacaoAdmForm(data={}) + form.data = {'data_tramitacao':date(2019, 5, 6), + 'unidade_tramitacao_local':unidade_tramitacao_local_1.pk, + 'unidade_tramitacao_destino':unidade_tramitacao_destino_1.pk, + 'status':status.pk, + 'urgente': False, + 'texto': "Texto de teste"} + form.instance.documento_id=documento_principal.pk + + assert form.is_valid() + tramitacao_principal = form.save() + tramitacao_anexada = documento_anexado.tramitacaoadministrativo_set.last() + tramitacao_anexada_anexada = documento_anexado_anexado.tramitacaoadministrativo_set.last() + + # Verifica se foram criadas as tramitações para os documentos anexados e anexados aos anexados + assert documento_principal.tramitacaoadministrativo_set.last() == tramitacao_principal + assert tramitacao_principal.documento.tramitacao == (tramitacao_principal.status.indicador != "F") + assert compara_tramitacoes_doc(tramitacao_principal, tramitacao_anexada) + assert DocumentoAdministrativo.objects.get(id=documento_anexado.pk).tramitacao \ + == (tramitacao_anexada.status.indicador != "F") + assert compara_tramitacoes_doc(tramitacao_anexada_anexada, tramitacao_principal) + assert DocumentoAdministrativo.objects.get(id=documento_anexado_anexado.pk).tramitacao \ + == (tramitacao_anexada_anexada.status.indicador != "F") + + + # Teste Edição de Tramitacao + form = TramitacaoAdmEditForm(data={}) + # Alterando unidade_tramitacao_destino + form.data = {'data_tramitacao':tramitacao_principal.data_tramitacao, + 'unidade_tramitacao_local':tramitacao_principal.unidade_tramitacao_local.pk, + 'unidade_tramitacao_destino':unidade_tramitacao_destino_2.pk, + 'status':tramitacao_principal.status.pk, + 'urgente': tramitacao_principal.urgente, + 'texto': tramitacao_principal.texto} + form.instance = tramitacao_principal + + assert form.is_valid() + tramitacao_principal = form.save() + tramitacao_anexada = documento_anexado.tramitacaoadministrativo_set.last() + tramitacao_anexada_anexada = documento_anexado_anexado.tramitacaoadministrativo_set.last() + + assert tramitacao_principal.unidade_tramitacao_destino == unidade_tramitacao_destino_2 + assert tramitacao_anexada.unidade_tramitacao_destino == unidade_tramitacao_destino_2 + assert tramitacao_anexada_anexada.unidade_tramitacao_destino == unidade_tramitacao_destino_2 + + + # Teste Remoção de Tramitacao + url = reverse('sapl.protocoloadm:tramitacaoadministrativo_delete', + kwargs={'pk': tramitacao_principal.pk}) + response = admin_client.post(url, {'confirmar':'confirmar'} ,follow=True) + assert TramitacaoAdministrativo.objects.filter(id=tramitacao_principal.pk).count() == 0 + assert TramitacaoAdministrativo.objects.filter(id=tramitacao_anexada.pk).count() == 0 + assert TramitacaoAdministrativo.objects.filter(id=tramitacao_anexada_anexada.pk).count() == 0 + + + # Testes para quando as tramitações das anexadas divergem + form = TramitacaoAdmForm(data={}) + form.data = {'data_tramitacao':date(2019, 5, 6), + 'unidade_tramitacao_local':unidade_tramitacao_local_1.pk, + 'unidade_tramitacao_destino':unidade_tramitacao_destino_1.pk, + 'status':status.pk, + 'urgente': False, + 'texto': "Texto de teste"} + form.instance.documento_id=documento_principal.pk + + assert form.is_valid() + tramitacao_principal = form.save() + tramitacao_anexada = documento_anexado.tramitacaoadministrativo_set.last() + tramitacao_anexada_anexada = documento_anexado_anexado.tramitacaoadministrativo_set.last() + + form = TramitacaoAdmEditForm(data={}) + # Alterando unidade_tramitacao_destino + form.data = {'data_tramitacao':tramitacao_anexada.data_tramitacao, + 'unidade_tramitacao_local':tramitacao_anexada.unidade_tramitacao_local.pk, + 'unidade_tramitacao_destino':unidade_tramitacao_destino_2.pk, + 'status':tramitacao_anexada.status.pk, + 'urgente': tramitacao_anexada.urgente, + 'texto': tramitacao_anexada.texto} + form.instance = tramitacao_anexada + + assert form.is_valid() + tramitacao_anexada = form.save() + tramitacao_anexada_anexada = documento_anexado_anexado.tramitacaoadministrativo_set.last() + + assert tramitacao_principal.unidade_tramitacao_destino == unidade_tramitacao_destino_1 + assert tramitacao_anexada.unidade_tramitacao_destino == unidade_tramitacao_destino_2 + assert tramitacao_anexada_anexada.unidade_tramitacao_destino == unidade_tramitacao_destino_2 + + # Editando a tramitação principal, as tramitações anexadas não devem ser editadas + form = TramitacaoAdmEditForm(data={}) + # Alterando o texto + form.data = {'data_tramitacao':tramitacao_principal.data_tramitacao, + 'unidade_tramitacao_local':tramitacao_principal.unidade_tramitacao_local.pk, + 'unidade_tramitacao_destino':tramitacao_principal.unidade_tramitacao_destino.pk, + 'status':tramitacao_principal.status.pk, + 'urgente': tramitacao_principal.urgente, + 'texto': "Testando a alteração"} + form.instance = tramitacao_principal + + assert form.is_valid() + tramitacao_principal = form.save() + tramitacao_anexada = documento_anexado.tramitacaoadministrativo_set.last() + tramitacao_anexada_anexada = documento_anexado_anexado.tramitacaoadministrativo_set.last() + + assert tramitacao_principal.texto == "Testando a alteração" + assert not tramitacao_anexada.texto == "Testando a alteração" + assert not tramitacao_anexada_anexada.texto == "Testando a alteração" + + # Removendo a tramitação pricipal, as tramitações anexadas não devem ser removidas, pois divergiram + url = reverse('sapl.protocoloadm:tramitacaoadministrativo_delete', + kwargs={'pk': tramitacao_principal.pk}) + response = admin_client.post(url, {'confirmar':'confirmar'} ,follow=True) + assert TramitacaoAdministrativo.objects.filter(id=tramitacao_principal.pk).count() == 0 + assert TramitacaoAdministrativo.objects.filter(id=tramitacao_anexada.pk).count() == 1 + assert TramitacaoAdministrativo.objects.filter(id=tramitacao_anexada_anexada.pk).count() == 1 + + # Removendo a tramitação anexada, a tramitação anexada à anexada deve ser removida + url = reverse('sapl.protocoloadm:tramitacaoadministrativo_delete', + kwargs={'pk': tramitacao_anexada.pk}) + response = admin_client.post(url, {'confirmar':'confirmar'} ,follow=True) + assert TramitacaoAdministrativo.objects.filter(id=tramitacao_anexada.pk).count() == 0 + assert TramitacaoAdministrativo.objects.filter(id=tramitacao_anexada_anexada.pk).count() == 0 diff --git a/sapl/protocoloadm/urls.py b/sapl/protocoloadm/urls.py index 67d5edb65..e5925204d 100644 --- a/sapl/protocoloadm/urls.py +++ b/sapl/protocoloadm/urls.py @@ -21,7 +21,8 @@ from sapl.protocoloadm.views import (AcompanhamentoDocumentoView, atualizar_numero_documento, doc_texto_integral, DesvincularDocumentoView, - DesvincularMateriaView) + DesvincularMateriaView, + AnexadoCrud, DocumentoAnexadoEmLoteView) from .apps import AppConfig @@ -30,6 +31,7 @@ app_name = AppConfig.name urlpatterns_documento_administrativo = [ url(r'^docadm/', include(DocumentoAdministrativoCrud.get_urls() + + AnexadoCrud.get_urls() + TramitacaoAdmCrud.get_urls() + DocumentoAcessorioAdministrativoCrud.get_urls())), @@ -38,6 +40,9 @@ urlpatterns_documento_administrativo = [ url(r'^docadm/texto_integral/(?P\d+)$', doc_texto_integral, name='doc_texto_integral'), + + url(r'^docadm/(?P\d+)/anexado_em_lote', DocumentoAnexadoEmLoteView.as_view(), + name='anexado_em_lote'), ] urlpatterns_protocolo = [ diff --git a/sapl/protocoloadm/views.py b/sapl/protocoloadm/views.py index bb5d12ed1..6296758ee 100755 --- a/sapl/protocoloadm/views.py +++ b/sapl/protocoloadm/views.py @@ -17,7 +17,7 @@ from django.http.response import HttpResponseRedirect from django.shortcuts import redirect from django.utils import timezone from django.utils.translation import ugettext_lazy as _ -from django.views.generic import ListView, CreateView +from django.views.generic import ListView, CreateView, UpdateView from django.views.generic.base import RedirectView, TemplateView from django.views.generic.edit import FormView from django_filters.views import FilterView @@ -27,16 +27,17 @@ from sapl.base.email_utils import do_envia_email_confirmacao from sapl.base.models import Autor, CasaLegislativa from sapl.base.signals import tramitacao_signal from sapl.comissoes.models import Comissao -from sapl.crud.base import Crud, CrudAux, MasterDetailCrud, make_pagination +from sapl.crud.base import (Crud, CrudAux, MasterDetailCrud, make_pagination, + RP_LIST, RP_DETAIL) from sapl.materia.models import MateriaLegislativa, TipoMateriaLegislativa from sapl.materia.views import gerar_pdf_impressos from sapl.parlamentares.models import Legislatura, Parlamentar from sapl.protocoloadm.models import Protocolo from sapl.utils import (create_barcode, get_base_url, get_client_ip, - get_mime_type_from_file_extension, + get_mime_type_from_file_extension, lista_anexados, show_results_filter_set, mail_service_configured) -from .forms import (AcompanhamentoDocumentoForm, AnularProcoloAdmForm, +from .forms import (AcompanhamentoDocumentoForm, AnularProtocoloAdmForm, DocumentoAcessorioAdministrativoForm, DocumentoAdministrativoFilterSet, DocumentoAdministrativoForm, FichaPesquisaAdmForm, FichaSelecionaAdmForm, ProtocoloDocumentForm, @@ -44,10 +45,12 @@ from .forms import (AcompanhamentoDocumentoForm, AnularProcoloAdmForm, TramitacaoAdmEditForm, TramitacaoAdmForm, DesvincularDocumentoForm, DesvincularMateriaForm, filtra_tramitacao_adm_destino_and_status, - filtra_tramitacao_adm_destino, filtra_tramitacao_adm_status) + filtra_tramitacao_adm_destino, filtra_tramitacao_adm_status, + AnexadoForm, AnexadoEmLoteFilterSet, + compara_tramitacoes_doc) from .models import (AcompanhamentoDocumento, DocumentoAcessorioAdministrativo, DocumentoAdministrativo, StatusTramitacaoAdministrativo, - TipoDocumentoAdministrativo, TramitacaoAdministrativo) + TipoDocumentoAdministrativo, TramitacaoAdministrativo, Anexado) TipoDocumentoAdministrativoCrud = CrudAux.build( @@ -482,7 +485,7 @@ class ProtocoloListView(PermissionRequiredMixin, ListView): class AnularProtocoloAdmView(PermissionRequiredMixin, CreateView): template_name = 'protocoloadm/anular_protocoloadm.html' - form_class = AnularProcoloAdmForm + form_class = AnularProtocoloAdmForm form_valid_message = _('Protocolo anulado com sucesso!') permission_required = ('protocoloadm.action_anular_protocolo', ) @@ -535,12 +538,12 @@ class ProtocoloDocumentoView(PermissionRequiredMixin, def form_valid(self, form): protocolo = form.save(commit=False) username = self.request.user.username - try: - self.logger.debug("user=" + username + - ". Tentando obter sequência de numeração.") - numeracao = sapl.base.models.AppConfig.objects.last( - ).sequencia_numeracao - except AttributeError as e: + + self.logger.debug("user=" + username + + ". Tentando obter sequência de numeração.") + numeracao = sapl.base.models.AppConfig.objects.last( + ).sequencia_numeracao_protocolo + if not numeracao: self.logger.error("user=" + username + ". É preciso definir a sequencia de " "numeração na tabelas auxiliares! " + str(e)) msg = _('É preciso definir a sequencia de ' + @@ -722,12 +725,11 @@ class ProtocoloMateriaView(PermissionRequiredMixin, CreateView): def form_valid(self, form): protocolo = form.save(commit=False) username = self.request.user.username - try: - self.logger.debug("user=" + username + - ". Tentando obter sequência de numeração.") - numeracao = sapl.base.models.AppConfig.objects.last( - ).sequencia_numeracao - except AttributeError: + self.logger.debug("user=" + username + + ". Tentando obter sequência de numeração.") + numeracao = sapl.base.models.AppConfig.objects.last( + ).sequencia_numeracao_protocolo + if not numeracao: self.logger.error("user=" + username + ". É preciso definir a sequencia de " "numeração na tabelas auxiliares!") msg = _('É preciso definir a sequencia de ' + @@ -941,6 +943,154 @@ class PesquisarDocumentoAdministrativoView(DocumentoAdministrativoMixin, return self.render_to_response(context) +class AnexadoCrud(MasterDetailCrud): + model = Anexado + parent_field = 'documento_principal' + help_topic = 'documento_anexado' + public = [RP_LIST, RP_DETAIL] + + class BaseMixin(MasterDetailCrud.BaseMixin): + list_field_names = ['documento_anexado', 'data_anexacao'] + + class CreateView(MasterDetailCrud.CreateView): + form_class = AnexadoForm + + class UpdateView(MasterDetailCrud.UpdateView): + form_class = AnexadoForm + + def get_initial(self): + initial = super(UpdateView, self).get_initial() + initial['tipo'] = self.object.documento_anexado.tipo.id + initial['numero'] = self.object.documento_anexado.numero + initial['ano'] = self.object.documento_anexado.ano + return initial + + class DetailView(MasterDetailCrud.DetailView): + + @property + def layout_key(self): + return 'AnexadoDetail' + + +class DocumentoAnexadoEmLoteView(PermissionRequiredMixin, FilterView): + filterset_class = AnexadoEmLoteFilterSet + template_name = 'protocoloadm/em_lote/anexado.html' + permission_required = ('protocoloadm.add_anexado', ) + + def get_context_data(self, **kwargs): + context = super( + DocumentoAnexadoEmLoteView,self + ).get_context_data(**kwargs) + + context['root_pk'] = self.kwargs['pk'] + + context['subnav_template_name'] = 'protocoloadm/subnav.yaml' + + context['title'] = _('Documentos Anexados em Lote') + + # Verifica se os campos foram preenchidos + if not self.request.GET.get('tipo', " "): + msg =_('Por favor, selecione um tipo de documento.') + messages.add_message(self.request, messages.ERROR, msg) + + if not self.request.GET.get('data_0', " ") or not self.request.GET.get('data_1', " "): + msg =_('Por favor, preencha as datas.') + messages.add_message(self.request, messages.ERROR, msg) + + return context + + if not self.request.GET.get('data_0', " ") or not self.request.GET.get('data_1', " "): + msg =_('Por favor, preencha as datas.') + messages.add_message(self.request, messages.ERROR, msg) + return context + + qr = self.request.GET.copy() + context['temp_object_list'] = context['object_list'].order_by( + 'numero', '-ano' + ) + + context['object_list'] = [] + for obj in context['temp_object_list']: + if not obj.pk == int(context['root_pk']): + documento_principal = DocumentoAdministrativo.objects.get(id=context['root_pk']) + documento_anexado = obj + is_anexado = Anexado.objects.filter(documento_principal=documento_principal, + documento_anexado=documento_anexado).exists() + if not is_anexado: + ciclico = False + anexados_anexado = Anexado.objects.filter(documento_principal=documento_anexado) + + while anexados_anexado and not ciclico: + anexados = [] + + for anexo in anexados_anexado: + + if documento_principal == anexo.documento_anexado: + ciclico = True + else: + for a in Anexado.objects.filter(documento_principal=anexo.documento_anexado): + anexados.append(a) + + anexados_anexado = anexados + + if not ciclico: + context['object_list'].append(obj) + + context['numero_res'] = len(context['object_list']) + + context['filter_url'] = ('&' + qr.urlencode()) if len(qr) > 0 else '' + + context['show_results'] = show_results_filter_set(qr) + + return context + + def post(self, request, *args, **kwargs): + marcados = request.POST.getlist('documento_id') + + data_anexacao = datetime.strptime( + request.POST['data_anexacao'], "%d/%m/%Y" + ).date() + + if request.POST['data_desanexacao'] == '': + data_desanexacao = None + v_data_desanexacao = data_anexacao + else: + data_desanexacao = datetime.strptime( + request.POST['data_desanexacao'], "%d/%m/%Y" + ).date() + v_data_desanexacao = data_desanexacao + + if len(marcados) == 0: + msg =_('Nenhum documento foi selecionado') + messages.add_message(request, messages.ERROR, msg) + + if data_anexacao > v_data_desanexacao: + msg=_('Data de anexação posterior à data de desanexação.') + messages.add_message(request, messages.ERROR, msg) + + return self.get(request, self.kwargs) + + if data_anexacao > v_data_desanexacao: + msg =_('Data de anexação posterior à data de desanexação.') + messages.add_message(request, messages.ERROR, msg) + return self.get(request, messages.ERROR, msg) + + principal = DocumentoAdministrativo.objects.get(pk = kwargs['pk']) + for documento in DocumentoAdministrativo.objects.filter(id__in = marcados): + anexado = Anexado() + anexado.documento_principal = principal + anexado.documento_anexado = documento + anexado.data_anexacao = data_anexacao + anexado.data_desanexacao = data_desanexacao + anexado.save() + + msg = _('Documento(s) anexado(s).') + messages.add_message(request, messages.SUCCESS, msg) + + success_url = reverse('sapl_index') + 'docadm/' + kwargs['pk'] + '/anexado' + return HttpResponseRedirect(success_url) + + class TramitacaoAdmCrud(MasterDetailCrud): model = TramitacaoAdministrativo parent_field = 'documento' @@ -954,6 +1104,10 @@ class TramitacaoAdmCrud(MasterDetailCrud): form_class = TramitacaoAdmForm logger = logging.getLogger(__name__) + def get_success_url(self): + return reverse('sapl.protocoloadm:tramitacaoadministrativo_list', kwargs={ + 'pk': self.kwargs['pk']}) + def get_initial(self): initial = super(CreateView, self).get_initial() local = DocumentoAdministrativo.objects.get( @@ -967,10 +1121,33 @@ class TramitacaoAdmCrud(MasterDetailCrud): else: initial['unidade_tramitacao_local'] = '' initial['data_tramitacao'] = timezone.now().date() + initial['ip'] = get_client_ip(self.request) + initial['user'] = self.request.user return initial def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) + username = self.request.user.username + + ultima_tramitacao = TramitacaoAdministrativo.objects.filter( + documento_id=self.kwargs['pk']).order_by( + '-data_tramitacao', + '-timestamp', + '-id').first() + + #TODO: Esta checagem foi inserida na issue #2027, mas é mesmo necessária? + if ultima_tramitacao: + if ultima_tramitacao.unidade_tramitacao_destino: + context['form'].fields[ + 'unidade_tramitacao_local'].choices = [ + (ultima_tramitacao.unidade_tramitacao_destino.pk, + ultima_tramitacao.unidade_tramitacao_destino)] + else: + self.logger.error('user=' + username + '. Unidade de tramitação destino ' + 'da última tramitação não pode ser vazia!') + msg = _('Unidade de tramitação destino ' + ' da última tramitação não pode ser vazia!') + messages.add_message(self.request, messages.ERROR, msg) primeira_tramitacao = not(TramitacaoAdministrativo.objects.filter( documento_id=int(kwargs['root_pk'])).exists()) @@ -990,7 +1167,6 @@ class TramitacaoAdmCrud(MasterDetailCrud): post=self.object, request=self.request) except Exception as e: - # TODO log error self.logger.error('user=' + username + '. Tramitação criada, mas e-mail de acompanhamento de documento ' 'não enviado. A não configuração do servidor de e-mail ' 'impede o envio de aviso de tramitação. ' + str(e)) @@ -1005,6 +1181,12 @@ class TramitacaoAdmCrud(MasterDetailCrud): form_class = TramitacaoAdmEditForm logger = logging.getLogger(__name__) + def get_initial(self): + initial = super(UpdateView, self).get_initial() + initial['ip'] = get_client_ip(self.request) + initial['user'] = self.request.user + return initial + def form_valid(self, form): self.object = form.save() username = self.request.user.username @@ -1013,7 +1195,6 @@ class TramitacaoAdmCrud(MasterDetailCrud): post=self.object, request=self.request) except Exception as e: - # TODO log error self.logger.error('user=' + username + '. Tramitação criada, mas e-mail de acompanhamento de documento ' 'não enviado. A não configuração do servidor de e-mail ' 'impede o envio de aviso de tramitação. ' + str(e)) @@ -1034,18 +1215,26 @@ class TramitacaoAdmCrud(MasterDetailCrud): class DetailView(DocumentoAdministrativoMixin, MasterDetailCrud.DetailView): - pass + + template_name = 'protocoloadm/tramitacaoadministrativo_detail.html' + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['user'] = self.request.user + return context + class DeleteView(MasterDetailCrud.DeleteView): + logger = logging.getLogger(__name__) + def delete(self, request, *args, **kwargs): tramitacao = TramitacaoAdministrativo.objects.get( id=self.kwargs['pk']) - documento = DocumentoAdministrativo.objects.get( - id=tramitacao.documento.id) + documento = tramitacao.documento url = reverse( 'sapl.protocoloadm:tramitacaoadministrativo_list', - kwargs={'pk': tramitacao.documento.id}) + kwargs={'pk': documento.id}) ultima_tramitacao = \ documento.tramitacaoadministrativo_set.order_by( @@ -1053,11 +1242,21 @@ class TramitacaoAdmCrud(MasterDetailCrud): '-id').first() if tramitacao.pk != ultima_tramitacao.pk: + username = request.user.username + self.logger.error("user=" + username + ". Não é possível deletar a tramitação de pk={}. " + "Somente a última tramitação (pk={}) pode ser deletada!." + .format(tramitacao.pk, ultima_tramitacao.pk)) msg = _('Somente a última tramitação pode ser deletada!') messages.add_message(request, messages.ERROR, msg) return HttpResponseRedirect(url) else: - tramitacao.delete() + tramitacoes_deletar = [tramitacao.id] + docs_anexados = lista_anexados(documento, False) + for da in docs_anexados: + tram_anexada = da.tramitacaoadministrativo_set.last() + if compara_tramitacoes_doc(tram_anexada, tramitacao): + tramitacoes_deletar.append(tram_anexada.id) + TramitacaoAdministrativo.objects.filter(id__in=tramitacoes_deletar).delete() return HttpResponseRedirect(url) @@ -1232,8 +1431,8 @@ class FichaSelecionaAdmView(PermissionRequiredMixin, FormView): self.messages.add_message(self.request, messages.INFO, mensagem) return self.render_to_response(context) - if len(documento.assunto) > 301: - documento.assunto = documento.assunto[0:300] + '[...]' + if len(documento.assunto) > 201: + documento.assunto = documento.assunto[0:200] + '[...]' context['documento'] = documento return gerar_pdf_impressos(self.request, context, diff --git a/sapl/relatorios/templates/pdf_sessao_plenaria_gerar.py b/sapl/relatorios/templates/pdf_sessao_plenaria_gerar.py index 4c388e59a..4bbe31bed 100644 --- a/sapl/relatorios/templates/pdf_sessao_plenaria_gerar.py +++ b/sapl/relatorios/templates/pdf_sessao_plenaria_gerar.py @@ -128,6 +128,24 @@ def inf_basicas(inf_basicas_dic): return tmp +def multimidia(cont_mult_dic): + """ + """ + tmp = "" + + mul_audio = cont_mult_dic['multimidia_audio'] + mul_video = cont_mult_dic['multimidia_video'] + + tmp += '\t\tConteúdo Multimídia\n' + tmp += '\t\t\n' + tmp += '\t\t\t
\n' + tmp += '\t\t
\n' + tmp += '\t\tAudio: ' + mul_audio + '\n' + tmp += '\t\tVideo: ' + mul_video + '\n' + + return tmp + + def mesa(lst_mesa): """ @@ -392,7 +410,7 @@ def ocorrencias(lst_ocorrencias): return tmp -def principal(rodape_dic, imagem, inf_basicas_dic, lst_mesa, lst_presenca_sessao, lst_ausencia_sessao, lst_expedientes, lst_expediente_materia, lst_expediente_materia_vot_nom, lst_oradores_expediente, lst_presenca_ordem_dia, lst_votacao, lst_votacao_vot_nom, lst_oradores_ordemdia, lst_oradores, lst_ocorrencias): +def principal(rodape_dic, imagem, inf_basicas_dic, cont_mult_dic, lst_mesa, lst_presenca_sessao, lst_ausencia_sessao, lst_expedientes, lst_expediente_materia, lst_expediente_materia_vot_nom, lst_oradores_expediente, lst_presenca_ordem_dia, lst_votacao, lst_votacao_vot_nom, lst_oradores_ordemdia, lst_oradores, lst_ocorrencias): """ """ arquivoPdf = str(int(time.time() * 100)) + ".pdf" @@ -416,7 +434,7 @@ def principal(rodape_dic, imagem, inf_basicas_dic, lst_mesa, lst_presenca_sessao ordenacao = ResumoOrdenacao.objects.first() dict_ord_template = { - 'cont_mult': '', + 'cont_mult': multimidia(cont_mult_dic), 'exp': expedientes(lst_expedientes), 'id_basica': inf_basicas(inf_basicas_dic), 'lista_p': presenca(lst_presenca_sessao, lst_ausencia_sessao), @@ -452,6 +470,7 @@ def principal(rodape_dic, imagem, inf_basicas_dic, lst_mesa, lst_presenca_sessao logger.error("KeyError: " + str(e) + ". Erro ao tentar utilizar " "configuração de ordenação. Utilizando ordenação padrão.") tmp += inf_basicas(inf_basicas_dic) + tmp += multimidia(cont_mult_dic) tmp += mesa(lst_mesa) tmp += presenca(lst_presenca_sessao, lst_ausencia_sessao) tmp += expedientes(lst_expedientes) @@ -467,6 +486,7 @@ def principal(rodape_dic, imagem, inf_basicas_dic, lst_mesa, lst_presenca_sessao else: tmp += inf_basicas(inf_basicas_dic) + tmp += multimidia(cont_mult_dic) tmp += mesa(lst_mesa) tmp += presenca(lst_presenca_sessao, lst_ausencia_sessao) tmp += expedientes(lst_expedientes) diff --git a/sapl/relatorios/views.py b/sapl/relatorios/views.py index 117b7a172..72595b239 100755 --- a/sapl/relatorios/views.py +++ b/sapl/relatorios/views.py @@ -519,6 +519,18 @@ def get_sessao_plenaria(sessao, casa): inf_basicas_dic["dat_fim_sessao"] = '' inf_basicas_dic["hr_fim_sessao"] = sessao.hora_fim inf_basicas_dic["nom_camara"] = casa.nome + + # Conteudo multimidia + cont_mult_dic = {} + if sessao.url_audio: + cont_mult_dic['multimidia_audio'] = str(sessao.url_audio) + else: + cont_mult_dic['multimidia_audio'] = 'Indisponível' + + if sessao.url_video: + cont_mult_dic['multimidia_video'] = str(sessao.url_video) + else: + cont_mult_dic['multimidia_video'] = 'Indisponível' # Lista da composicao da mesa diretora lst_mesa = [] @@ -580,7 +592,9 @@ def get_sessao_plenaria(sessao, casa): # https://github.com/interlegis/sapl/issues/1046 conteudo = re.sub('style=".*?"', '', conteudo) conteudo = re.sub('class=".*?"', '', conteudo) + conteudo = re.sub('align=".*?"', '', conteudo) # OSTicket Ticket #796450 conteudo = re.sub('', '

', conteudo) + conteudo = re.sub('', '
', conteudo) # OSTicket Ticket #796450 conteudo = html.unescape(conteudo) # escape special character '&' @@ -850,6 +864,7 @@ def get_sessao_plenaria(sessao, casa): lst_ocorrencias.append(o) return (inf_basicas_dic, + cont_mult_dic, lst_mesa, lst_presenca_sessao, lst_ausencia_sessao, @@ -908,6 +923,7 @@ def relatorio_sessao_plenaria(request, pk): raise Http404('Essa página não existe') (inf_basicas_dic, + cont_mult_dic, lst_mesa, lst_presenca_sessao, lst_ausencia_sessao, @@ -932,6 +948,7 @@ def relatorio_sessao_plenaria(request, pk): rodape, imagem, inf_basicas_dic, + cont_mult_dic, lst_mesa, lst_presenca_sessao, lst_ausencia_sessao, diff --git a/sapl/rules/map_rules.py b/sapl/rules/map_rules.py index d09e65961..75fc76f33 100644 --- a/sapl/rules/map_rules.py +++ b/sapl/rules/map_rules.py @@ -60,6 +60,7 @@ rules_group_administrativo = { 'can_access_impressos'], __perms_publicas__), # TODO: tratar em sapl.api a questão de ostencivo e restritivo (protocoloadm.DocumentoAdministrativo, __base__, set()), + (protocoloadm.Anexado, __base__, set()), (protocoloadm.DocumentoAcessorioAdministrativo, __base__, set()), (protocoloadm.TramitacaoAdministrativo, __base__, set()), ] @@ -118,6 +119,8 @@ rules_group_materia = { (materia.Autoria, __base__, __perms_publicas__), (materia.DespachoInicial, __base__, __perms_publicas__), (materia.DocumentoAcessorio, __base__, __perms_publicas__), + (materia.MateriaAssunto, __base__, __perms_publicas__), + (materia.AssuntoMateria, __base__, __perms_publicas__), (materia.MateriaLegislativa, __base__ + ['can_access_impressos'], __perms_publicas__), @@ -275,6 +278,8 @@ rules_group_geral = { (parlamentares.ComposicaoMesa, __base__, __perms_publicas__), (parlamentares.Frente, __base__, __perms_publicas__), (parlamentares.Votante, __base__, __perms_publicas__), + (parlamentares.Bloco, __base__, __perms_publicas__), + (sessao.CargoBancada, __base__, __perms_publicas__), (sessao.Bancada, __base__, __perms_publicas__), @@ -283,7 +288,6 @@ rules_group_geral = { (sessao.TipoExpediente, __base__, __perms_publicas__), (sessao.TipoJustificativa, __base__, __perms_publicas__), (sessao.JustificativaAusencia, __base__, __perms_publicas__), - (sessao.Bloco, __base__, __perms_publicas__), (sessao.ResumoOrdenacao, __base__, __perms_publicas__), (sessao.TipoRetiradaPauta, __base__, __perms_publicas__), diff --git a/sapl/sessao/forms.py b/sapl/sessao/forms.py index 373b1e951..1e14b0f45 100644 --- a/sapl/sessao/forms.py +++ b/sapl/sessao/forms.py @@ -23,32 +23,17 @@ from sapl.utils import (RANGE_DIAS_MES, RANGE_MESES, autor_modal, timezone, choice_anos_com_sessaoplenaria, FileFieldCheckMixin) -from .models import (Bancada, Bloco, ExpedienteMateria, JustificativaAusencia, +from .models import (Bancada, ExpedienteMateria, JustificativaAusencia, Orador, OradorExpediente, OrdemDia, PresencaOrdemDia, SessaoPlenaria, SessaoPlenariaPresenca, TipoResultadoVotacao, - OcorrenciaSessao, RetiradaPauta, TipoRetiradaPauta, OradorOrdemDia) + OcorrenciaSessao, RetiradaPauta, TipoRetiradaPauta, OradorOrdemDia, ORDENACAO_RESUMO, + ResumoOrdenacao) MES_CHOICES = RANGE_MESES DIA_CHOICES = RANGE_DIAS_MES -ORDENACAO_RESUMO = [('cont_mult', 'Conteúdo Multimídia'), - ('exp', 'Expedientes'), - ('id_basica', 'Identificação Básica'), - ('lista_p', 'Lista de Presença'), - ('lista_p_o_d', 'Lista de Presença Ordem do Dia'), - ('mat_exp', 'Matérias do Expediente'), - ('mat_o_d', 'Matérias da Ordem do Dia'), - ('mesa_d', 'Mesa Diretora'), - ('oradores_exped', 'Oradores do Expediente'), - ('oradores_expli', 'Oradores das Explicações Pessoais'), - ('ocorr_sessao', 'Ocorrências da Sessão'), - ('v_n_mat_exp', 'Votações Nominais - Matérias do Expediente'), - ('v_n_mat_o_d', 'Votações Nominais - Matérias da Ordem do Dia'), - ('oradores_o_d', 'Oradores da Ordem do Dia')] - - class SessaoPlenariaForm(FileFieldCheckMixin, ModelForm): class Meta: @@ -331,41 +316,6 @@ class BancadaForm(ModelForm): return bancada -class BlocoForm(ModelForm): - - class Meta: - model = Bloco - fields = ['nome', 'partidos', 'data_criacao', - 'data_extincao', 'descricao'] - - def clean(self): - super(BlocoForm, self).clean() - - if not self.is_valid(): - return self.cleaned_data - - if self.cleaned_data['data_extincao']: - if (self.cleaned_data['data_extincao'] < - self.cleaned_data['data_criacao']): - msg = _('Data de extinção não pode ser menor que a de criação') - raise ValidationError(msg) - return self.cleaned_data - - @transaction.atomic - def save(self, commit=True): - bloco = super(BlocoForm, self).save(commit) - content_type = ContentType.objects.get_for_model(Bloco) - object_id = bloco.pk - tipo = TipoAutor.objects.get(content_type=content_type) - Autor.objects.create( - content_type=content_type, - object_id=object_id, - tipo=tipo, - nome=bloco.nome - ) - return bloco - - class ExpedienteMateriaForm(ModelForm): _model = ExpedienteMateria @@ -807,38 +757,64 @@ class PautaSessaoFilterSet(SessaoPlenariaFilterSet): class ResumoOrdenacaoForm(forms.Form): - primeiro = forms.ChoiceField(label=_('1°'), - choices=ORDENACAO_RESUMO) - segundo = forms.ChoiceField(label=_('2°'), - choices=ORDENACAO_RESUMO) - terceiro = forms.ChoiceField(label='3°', - choices=ORDENACAO_RESUMO) - quarto = forms.ChoiceField(label=_('4°'), - choices=ORDENACAO_RESUMO) - quinto = forms.ChoiceField(label=_('5°'), - choices=ORDENACAO_RESUMO) - sexto = forms.ChoiceField(label=_('6°'), - choices=ORDENACAO_RESUMO) - setimo = forms.ChoiceField(label=_('7°'), - choices=ORDENACAO_RESUMO) - oitavo = forms.ChoiceField(label=_('8°'), - choices=ORDENACAO_RESUMO) - nono = forms.ChoiceField(label=_('9°'), - choices=ORDENACAO_RESUMO) - decimo = forms.ChoiceField(label='10°', - choices=ORDENACAO_RESUMO) - decimo_primeiro = forms.ChoiceField(label='11°', - choices=ORDENACAO_RESUMO) - decimo_segundo = forms.ChoiceField(label='12°', - choices=ORDENACAO_RESUMO) - decimo_terceiro = forms.ChoiceField(label='13°', - choices=ORDENACAO_RESUMO) - decimo_quarto = forms.ChoiceField(label='14°', - choices=ORDENACAO_RESUMO) + primeiro = forms.ChoiceField( + label='1°', + choices=ORDENACAO_RESUMO + ) + segundo = forms.ChoiceField( + label='2°', + choices=ORDENACAO_RESUMO + ) + terceiro = forms.ChoiceField( + label='3°', + choices=ORDENACAO_RESUMO + ) + quarto = forms.ChoiceField( + label='4°', + choices=ORDENACAO_RESUMO + ) + quinto = forms.ChoiceField( + label='5°', + choices=ORDENACAO_RESUMO + ) + sexto = forms.ChoiceField( + label='6°', + choices=ORDENACAO_RESUMO + ) + setimo = forms.ChoiceField( + label='7°', + choices=ORDENACAO_RESUMO + ) + oitavo = forms.ChoiceField( + label='8°', + choices=ORDENACAO_RESUMO + ) + nono = forms.ChoiceField( + label='9°', + choices=ORDENACAO_RESUMO + ) + decimo = forms.ChoiceField( + label='10°', + choices=ORDENACAO_RESUMO + ) + decimo_primeiro = forms.ChoiceField( + label='11°', + choices=ORDENACAO_RESUMO + ) + decimo_segundo = forms.ChoiceField( + label='12°', + choices=ORDENACAO_RESUMO + ) + decimo_terceiro = forms.ChoiceField( + label='13°', + choices=ORDENACAO_RESUMO + ) + decimo_quarto = forms.ChoiceField( + label='14°', + choices=ORDENACAO_RESUMO + ) def __init__(self, *args, **kwargs): - super(ResumoOrdenacaoForm, self).__init__(*args, **kwargs) - row1 = to_row( [('primeiro', 12)]) row2 = to_row( @@ -878,6 +854,8 @@ class ResumoOrdenacaoForm(forms.Form): form_actions(label='Atualizar')) ) + super().__init__(*args, **kwargs) + def clean(self): super(ResumoOrdenacaoForm, self).clean() @@ -896,6 +874,27 @@ class ResumoOrdenacaoForm(forms.Form): 'Não é possível ter campos repetidos')) return self.cleaned_data + def save(self): + ordenacao = ResumoOrdenacao.objects.get() + cleaned_data = self.cleaned_data + + ordenacao.primeiro = cleaned_data['primeiro'] + ordenacao.segundo = cleaned_data['segundo'] + ordenacao.terceiro = cleaned_data['terceiro'] + ordenacao.quarto = cleaned_data['quarto'] + ordenacao.quinto = cleaned_data['quinto'] + ordenacao.sexto = cleaned_data['sexto'] + ordenacao.setimo = cleaned_data['setimo'] + ordenacao.oitavo = cleaned_data['oitavo'] + ordenacao.nono = cleaned_data['nono'] + ordenacao.decimo = cleaned_data['decimo'] + ordenacao.decimo_primeiro = cleaned_data['decimo_primeiro'] + ordenacao.decimo_segundo = cleaned_data['decimo_segundo'] + ordenacao.decimo_terceiro = cleaned_data['decimo_terceiro'] + ordenacao.decimo_quarto = cleaned_data['decimo_quarto'] + + ordenacao.save() + class JustificativaAusenciaForm(ModelForm): diff --git a/sapl/sessao/migrations/0036_auto_20190412_1106.py b/sapl/sessao/migrations/0036_auto_20190412_1106.py new file mode 100644 index 000000000..fe8f563cc --- /dev/null +++ b/sapl/sessao/migrations/0036_auto_20190412_1106.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-04-12 14:06 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('sessao', '0035_resumoordenacao_decimo_quarto'), + ] + + operations = [ + migrations.AlterField( + model_name='expedientesessao', + name='sessao_plenaria', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='expedientesessao_set', to='sessao.SessaoPlenaria'), + ), + ] diff --git a/sapl/sessao/migrations/0037_auto_20190415_1324.py b/sapl/sessao/migrations/0037_auto_20190415_1324.py new file mode 100644 index 000000000..ed1137e4b --- /dev/null +++ b/sapl/sessao/migrations/0037_auto_20190415_1324.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-04-15 16:24 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('sessao', '0036_auto_20190412_1106'), + ] + + operations = [ + migrations.AddField( + model_name='registrovotacao', + name='data_hora', + field=models.DateTimeField(auto_now_add=True, null=True, verbose_name='Data/Hora'), + ), + migrations.AddField( + model_name='registrovotacao', + name='ip', + field=models.CharField(blank=True, default='', max_length=30, verbose_name='IP'), + ), + migrations.AddField( + model_name='registrovotacao', + name='user', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/sapl/sessao/migrations/0037_auto_20190415_1635.py b/sapl/sessao/migrations/0037_auto_20190415_1635.py new file mode 100644 index 000000000..701d9eee9 --- /dev/null +++ b/sapl/sessao/migrations/0037_auto_20190415_1635.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-04-15 19:35 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('sessao', '0036_auto_20190412_1106'), + ] + + operations = [ + migrations.AlterField( + model_name='resumoordenacao', + name='decimo', + field=models.CharField(default='mat_o_d', max_length=50), + ), + migrations.AlterField( + model_name='resumoordenacao', + name='decimo_primeiro', + field=models.CharField(default='v_n_mat_o_d', max_length=50), + ), + migrations.AlterField( + model_name='resumoordenacao', + name='decimo_quarto', + field=models.CharField(default='ocorr_sessao', max_length=50), + ), + migrations.AlterField( + model_name='resumoordenacao', + name='decimo_segundo', + field=models.CharField(default='oradores_o_d', max_length=50), + ), + migrations.AlterField( + model_name='resumoordenacao', + name='decimo_terceiro', + field=models.CharField(default='oradores_expli', max_length=50), + ), + migrations.AlterField( + model_name='resumoordenacao', + name='nono', + field=models.CharField(default='lista_p_o_d', max_length=50), + ), + migrations.AlterField( + model_name='resumoordenacao', + name='oitavo', + field=models.CharField(default='oradores_exped', max_length=50), + ), + migrations.AlterField( + model_name='resumoordenacao', + name='primeiro', + field=models.CharField(default='id_basica', max_length=50), + ), + migrations.AlterField( + model_name='resumoordenacao', + name='quarto', + field=models.CharField(default='lista_p', max_length=50), + ), + migrations.AlterField( + model_name='resumoordenacao', + name='quinto', + field=models.CharField(default='exp', max_length=50), + ), + migrations.AlterField( + model_name='resumoordenacao', + name='segundo', + field=models.CharField(default='cont_mult', max_length=50), + ), + migrations.AlterField( + model_name='resumoordenacao', + name='setimo', + field=models.CharField(default='v_n_mat_exp', max_length=50), + ), + migrations.AlterField( + model_name='resumoordenacao', + name='sexto', + field=models.CharField(default='mat_exp', max_length=50), + ), + migrations.AlterField( + model_name='resumoordenacao', + name='terceiro', + field=models.CharField(default='mesa_d', max_length=50), + ), + ] diff --git a/sapl/sessao/migrations/0038_merge_20190415_1800.py b/sapl/sessao/migrations/0038_merge_20190415_1800.py new file mode 100644 index 000000000..0bf3ab4ea --- /dev/null +++ b/sapl/sessao/migrations/0038_merge_20190415_1800.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-04-15 21:00 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('sessao', '0037_auto_20190415_1635'), + ('sessao', '0037_auto_20190415_1324'), + ] + + operations = [ + ] diff --git a/sapl/sessao/migrations/0039_auto_20190430_0825.py b/sapl/sessao/migrations/0039_auto_20190430_0825.py new file mode 100644 index 000000000..5a1c717f5 --- /dev/null +++ b/sapl/sessao/migrations/0039_auto_20190430_0825.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-04-30 11:25 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('sessao', '0038_merge_20190415_1800'), + ] + + database_operations = [migrations.AlterModelTable('Bloco', 'parlamentares_bloco')] + + state_operations = [migrations.DeleteModel('Bloco')] + + operations = [ + migrations.SeparateDatabaseAndState( + database_operations=database_operations, + state_operations=state_operations) + ] \ No newline at end of file diff --git a/sapl/sessao/models.py b/sapl/sessao/models.py index 7267e78fc..53721499b 100644 --- a/sapl/sessao/models.py +++ b/sapl/sessao/models.py @@ -290,8 +290,11 @@ class TipoExpediente(models.Model): @reversion.register() class ExpedienteSessao(models.Model): # ExpedienteSessaoPlenaria - sessao_plenaria = models.ForeignKey(SessaoPlenaria, - on_delete=models.CASCADE) + sessao_plenaria = models.ForeignKey( + SessaoPlenaria, + on_delete=models.CASCADE, + related_name='expedientesessao_set' + ) tipo = models.ForeignKey(TipoExpediente, on_delete=models.PROTECT) conteudo = models.TextField( blank=True, verbose_name=_('Conteúdo do expediente')) @@ -379,7 +382,7 @@ class OradorExpediente(AbstractOrador): # OradoresExpediente @reversion.register() -class OradorOrdemDia(AbstractOrador): # OradoresOrdemDia +class OradorOrdemDia(AbstractOrador): # OradoresOrdemDia class Meta: verbose_name = _('Orador da Ordem do Dia') @@ -453,6 +456,19 @@ class RegistroVotacao(models.Model): verbose_name=_('Abstenções')) observacao = models.TextField( blank=True, verbose_name=_('Observações')) + user = models.ForeignKey(get_settings_auth_user_model(), + on_delete=models.PROTECT, + null=True, + blank=True) + ip = models.CharField(verbose_name=_('IP'), + max_length=30, + blank=True, + default='') + data_hora = models.DateTimeField( + verbose_name=_('Data/Hora'), + auto_now_add=True, + blank=True, + null=True) class Meta: verbose_name = _('Votação') @@ -534,38 +550,22 @@ class SessaoPlenariaPresenca(models.Model): ordering = ['parlamentar__nome_parlamentar'] -@reversion.register() -class Bloco(models.Model): - ''' - * blocos podem existir por mais de uma legislatura - ''' - nome = models.CharField( - max_length=80, verbose_name=_('Nome do Bloco')) - partidos = models.ManyToManyField( - Partido, blank=True, verbose_name=_('Partidos')) - data_criacao = models.DateField( - blank=False, null=True, verbose_name=_('Data Criação')) - data_extincao = models.DateField( - blank=True, null=True, verbose_name=_('Data Dissolução')) - descricao = models.TextField(blank=True, verbose_name=_('Descrição')) - - # campo conceitual de reversão genérica para o model Autor que dá a - # o meio possível de localização de tipos de autores. - autor = SaplGenericRelation(Autor, - related_query_name='bloco_set', - fields_search=( - ('nome', '__icontains'), - ('descricao', '__icontains'), - ('partidos__sigla', '__icontains'), - ('partidos__nome', '__icontains'), - )) - - class Meta: - verbose_name = _('Bloco Parlamentar') - verbose_name_plural = _('Blocos Parlamentares') - - def __str__(self): - return self.nome +ORDENACAO_RESUMO = [ + ('id_basica', 'Identificação Básica'), + ('cont_mult', 'Conteúdo Multimídia'), + ('mesa_d', 'Mesa Diretora'), + ('lista_p', 'Lista de Presença'), + ('exp', 'Expedientes'), + ('mat_exp', 'Matérias do Expediente'), + ('v_n_mat_exp', 'Votações Nominais - Matérias do Expediente'), + ('oradores_exped', 'Oradores do Expediente'), + ('lista_p_o_d', 'Lista de Presença Ordem do Dia'), + ('mat_o_d', 'Matérias da Ordem do Dia'), + ('v_n_mat_o_d', 'Votações Nominais - Matérias da Ordem do Dia'), + ('oradores_o_d', 'Oradores da Ordem do Dia'), + ('oradores_expli', 'Oradores das Explicações Pessoais'), + ('ocorr_sessao', 'Ocorrências da Sessão') +] @reversion.register() @@ -574,20 +574,62 @@ class ResumoOrdenacao(models.Model): Tabela para registrar em qual ordem serão renderizados os componentes da tela de resumo de uma sessão ''' - primeiro = models.CharField(max_length=30) - segundo = models.CharField(max_length=30) - terceiro = models.CharField(max_length=30) - quarto = models.CharField(max_length=30) - quinto = models.CharField(max_length=30) - sexto = models.CharField(max_length=30) - setimo = models.CharField(max_length=30) - oitavo = models.CharField(max_length=30) - nono = models.CharField(max_length=30) - decimo = models.CharField(max_length=30) - decimo_primeiro = models.CharField(max_length=30,default="Ocorrências da Sessão") - decimo_segundo = models.CharField(max_length=30, default="Votos Nominais Mat Expediente") - decimo_terceiro = models.CharField(max_length=30, default="Votos Nominais Mat Ordem Dia") - decimo_quarto = models.CharField(max_length=30, default="Oradores da Ordem do Dia") + primeiro = models.CharField( + max_length=50, + default=ORDENACAO_RESUMO[0][0] + ) + segundo = models.CharField( + max_length=50, + default=ORDENACAO_RESUMO[1][0] + ) + terceiro = models.CharField( + max_length=50, + default=ORDENACAO_RESUMO[2][0] + ) + quarto = models.CharField( + max_length=50, + default=ORDENACAO_RESUMO[3][0] + ) + quinto = models.CharField( + max_length=50, + default=ORDENACAO_RESUMO[4][0] + ) + sexto = models.CharField( + max_length=50, + default=ORDENACAO_RESUMO[5][0] + ) + setimo = models.CharField( + max_length=50, + default=ORDENACAO_RESUMO[6][0] + ) + oitavo = models.CharField( + max_length=50, + default=ORDENACAO_RESUMO[7][0] + ) + nono = models.CharField( + max_length=50, + default=ORDENACAO_RESUMO[8][0] + ) + decimo = models.CharField( + max_length=50, + default=ORDENACAO_RESUMO[9][0] + ) + decimo_primeiro = models.CharField( + max_length=50, + default=ORDENACAO_RESUMO[10][0] + ) + decimo_segundo = models.CharField( + max_length=50, + default=ORDENACAO_RESUMO[11][0] + ) + decimo_terceiro = models.CharField( + max_length=50, + default=ORDENACAO_RESUMO[12][0] + ) + decimo_quarto = models.CharField( + max_length=50, + default=ORDENACAO_RESUMO[13][0] + ) class Meta: verbose_name = _('Ordenação do Resumo de uma Sessão') @@ -596,6 +638,7 @@ class ResumoOrdenacao(models.Model): def __str__(self): return 'Ordenação do Resumo de uma Sessão' + @reversion.register() class TipoRetiradaPauta(models.Model): descricao = models.CharField(max_length=150, verbose_name=_('Descrição')) @@ -687,6 +730,7 @@ class JustificativaAusencia(models.Model): using=using, update_fields=update_fields) + class RetiradaPauta(models.Model): materia = models.ForeignKey(MateriaLegislativa, on_delete=models.CASCADE, diff --git a/sapl/sessao/urls.py b/sapl/sessao/urls.py index 00add810a..d5d709967 100644 --- a/sapl/sessao/urls.py +++ b/sapl/sessao/urls.py @@ -2,8 +2,8 @@ from django.conf.urls import include, url from sapl.sessao.views import (AdicionarVariasMateriasExpediente, AdicionarVariasMateriasOrdemDia, BancadaCrud, - BlocoCrud, CargoBancadaCrud, - ExpedienteMateriaCrud, ExpedienteView, JustificativaAusenciaCrud, + CargoBancadaCrud, ExpedienteMateriaCrud, + ExpedienteView, JustificativaAusenciaCrud, OcorrenciaSessaoView, MateriaOrdemDiaCrud, OradorOrdemDiaCrud, MesaView, OradorCrud, OradorExpedienteCrud, PainelView, @@ -96,8 +96,6 @@ urlpatterns = [ include(TipoRetiradaPautaCrud.get_urls())), url(r'^sistema/bancada/', include(BancadaCrud.get_urls())), - url(r'^sistema/bloco/', - include(BlocoCrud.get_urls())), url(r'^sistema/cargo-bancada/', include(CargoBancadaCrud.get_urls())), url(r'^sistema/resumo-ordenacao/', diff --git a/sapl/sessao/views.py b/sapl/sessao/views.py index 08adf1fa0..191de0b8f 100755 --- a/sapl/sessao/views.py +++ b/sapl/sessao/views.py @@ -35,21 +35,21 @@ from sapl.parlamentares.models import (Filiacao, Legislatura, Mandato, Parlamentar, SessaoLegislativa) from sapl.sessao.apps import AppConfig from sapl.sessao.forms import ExpedienteMateriaForm, OrdemDiaForm -from sapl.utils import show_results_filter_set, remover_acentos +from sapl.utils import show_results_filter_set, remover_acentos, get_client_ip -from .forms import (AdicionarVariasMateriasFilterSet, BancadaForm, BlocoForm, +from .forms import (AdicionarVariasMateriasFilterSet, BancadaForm, ExpedienteForm, JustificativaAusenciaForm, OcorrenciaSessaoForm, ListMateriaForm, MesaForm, OradorExpedienteForm, OradorForm, PautaSessaoFilterSet, PresencaForm, ResumoOrdenacaoForm, SessaoPlenariaFilterSet, SessaoPlenariaForm, VotacaoEditForm, VotacaoForm, VotacaoNominalForm, RetiradaPautaForm, OradorOrdemDiaForm) -from .models import (Bancada, Bloco, CargoBancada, CargoMesa, +from .models import (Bancada, CargoBancada, CargoMesa, ExpedienteMateria, ExpedienteSessao, OcorrenciaSessao, IntegranteMesa, MateriaLegislativa, Orador, OradorExpediente, OrdemDia, PresencaOrdemDia, RegistroVotacao, ResumoOrdenacao, SessaoPlenaria, SessaoPlenariaPresenca, TipoExpediente, TipoResultadoVotacao, TipoSessaoPlenaria, VotoParlamentar, TipoRetiradaPauta, - RetiradaPauta, TipoJustificativa, JustificativaAusencia, OradorOrdemDia) + RetiradaPauta, TipoJustificativa, JustificativaAusencia, OradorOrdemDia, ORDENACAO_RESUMO) TipoSessaoCrud = CrudAux.build(TipoSessaoPlenaria, 'tipo_sessao_plenaria') @@ -679,16 +679,6 @@ class BancadaCrud(CrudAux): return reverse('sapl.sessao:bancada_list') -class BlocoCrud(CrudAux): - model = Bloco - - class CreateView(CrudAux.CreateView): - form_class = BlocoForm - - def get_success_url(self): - return reverse('sapl.sessao:bloco_list') - - def recuperar_numero_sessao(request): try: sessao = SessaoPlenaria.objects.filter( @@ -1296,49 +1286,38 @@ class ResumoOrdenacaoView(PermissionRequiredMixin, FormView): form_class = ResumoOrdenacaoForm permission_required = {'sessao.change_resumoordenacao'} - def get_success_url(self): - return reverse('sapl.base:sistema') + def get_tupla(self, tupla_key): + for tupla in ORDENACAO_RESUMO: + if tupla[0] == tupla_key: + return tupla def get_initial(self): - initial = super(ResumoOrdenacaoView, self).get_initial() - ordenacao = ResumoOrdenacao.objects.first() - if ordenacao: - initial.update({'primeiro': ordenacao.primeiro, - 'segundo': ordenacao.segundo, - 'terceiro': ordenacao.terceiro, - 'quarto': ordenacao.quarto, - 'quinto': ordenacao.quinto, - 'sexto': ordenacao.sexto, - 'setimo': ordenacao.setimo, - 'oitavo': ordenacao.oitavo, - 'nono': ordenacao.nono, - 'decimo': ordenacao.decimo, - 'decimo_primeiro': ordenacao.decimo_primeiro, - 'decimo_segundo': ordenacao.decimo_segundo, - 'decimo_terceiro': ordenacao.decimo_terceiro, - 'decimo_quarto': ordenacao.decimo_quarto}) - return initial - - def form_valid(self, form): ordenacao = ResumoOrdenacao.objects.get_or_create()[0] - ordenacao.primeiro = form.cleaned_data['primeiro'] - ordenacao.segundo = form.cleaned_data['segundo'] - ordenacao.terceiro = form.cleaned_data['terceiro'] - ordenacao.quarto = form.cleaned_data['quarto'] - ordenacao.quinto = form.cleaned_data['quinto'] - ordenacao.sexto = form.cleaned_data['sexto'] - ordenacao.setimo = form.cleaned_data['setimo'] - ordenacao.oitavo = form.cleaned_data['oitavo'] - ordenacao.nono = form.cleaned_data['nono'] - ordenacao.decimo = form.cleaned_data['decimo'] - ordenacao.decimo_primeiro = form.cleaned_data['decimo_primeiro'] - ordenacao.decimo_segundo = form.cleaned_data['decimo_segundo'] - ordenacao.decimo_terceiro = form.cleaned_data['decimo_terceiro'] - ordenacao.decimo_quarto = form.cleaned_data['decimo_quarto'] - - ordenacao.save() + initial = { + 'primeiro': self.get_tupla(ordenacao.primeiro), + 'segundo': self.get_tupla(ordenacao.segundo), + 'terceiro': self.get_tupla(ordenacao.terceiro), + 'quarto': self.get_tupla(ordenacao.quarto), + 'quinto': self.get_tupla(ordenacao.quinto), + 'sexto': self.get_tupla(ordenacao.sexto), + 'setimo': self.get_tupla(ordenacao.setimo), + 'oitavo': self.get_tupla(ordenacao.oitavo), + 'nono': self.get_tupla(ordenacao.nono), + 'decimo': self.get_tupla(ordenacao.decimo), + 'decimo_primeiro': self.get_tupla(ordenacao.decimo_primeiro), + 'decimo_segundo': self.get_tupla(ordenacao.decimo_segundo), + 'decimo_terceiro': self.get_tupla(ordenacao.decimo_terceiro), + 'decimo_quarto': self.get_tupla(ordenacao.decimo_quarto) + } + + return initial + + def get_success_url(self): + return reverse('sapl.base:sistema') + def form_valid(self, form): + form.save() return HttpResponseRedirect(self.get_success_url()) @@ -1761,7 +1740,6 @@ class ResumoView(DetailView): context.update(get_ocorrencias_da_sessão(self.object)) # ===================================================================== # Indica a ordem com a qual o template será renderizado - ordenacao = ResumoOrdenacao.objects.first() dict_ord_template = { 'cont_mult': 'conteudo_multimidia.html', 'exp': 'expedientes.html', @@ -1779,59 +1757,23 @@ class ResumoView(DetailView): 'ocorr_sessao': 'ocorrencias_da_sessao.html' } - if ordenacao: - try: - context.update( - {'primeiro_ordenacao': dict_ord_template[ordenacao.primeiro], - 'segundo_ordenacao': dict_ord_template[ordenacao.segundo], - 'terceiro_ordenacao': dict_ord_template[ordenacao.terceiro], - 'quarto_ordenacao': dict_ord_template[ordenacao.quarto], - 'quinto_ordenacao': dict_ord_template[ordenacao.quinto], - 'sexto_ordenacao': dict_ord_template[ordenacao.sexto], - 'setimo_ordenacao': dict_ord_template[ordenacao.setimo], - 'oitavo_ordenacao': dict_ord_template[ordenacao.oitavo], - 'nono_ordenacao': dict_ord_template[ordenacao.nono], - 'decimo_ordenacao': dict_ord_template[ordenacao.decimo], - 'decimo_primeiro_ordenacao': dict_ord_template[ordenacao.decimo_primeiro], - 'decimo_segundo_ordenacao': dict_ord_template[ordenacao.decimo_segundo], - 'decimo_terceiro_ordenacao': dict_ord_template[ordenacao.decimo_terceiro], - 'decimo_quarto_ordenacao': dict_ord_template[ordenacao.decimo_quarto]}) - except KeyError as e: - self.logger.error('user=' + self.request.user.username + '. ' + "KeyError: " + str(e) + ". Erro " - "ao tentar utilizar configuração de ordenação. Utilizando ordenação padrão.") - context.update( - {'primeiro_ordenacao': dict_ord_template['id_basica'], - 'segundo_ordenacao': dict_ord_template['cont_mult'], - 'terceiro_ordenacao': dict_ord_template['mesa_d'], - 'quarto_ordenacao': dict_ord_template['lista_p'], - 'quinto_ordenacao': dict_ord_template['exp'], - 'sexto_ordenacao': dict_ord_template['mat_exp'], - 'setimo_ordenacao': dict_ord_template['v_n_mat_exp'], - 'oitavo_ordenacao': dict_ord_template['oradores_exped'], - 'nono_ordenacao': dict_ord_template['lista_p_o_d'], - 'decimo_ordenacao': dict_ord_template['mat_o_d'], - 'decimo_primeiro_ordenacao': dict_ord_template['v_n_mat_o_d'], - 'decimo_segundo_ordenacao': dict_ord_template['oradores_o_d'], - 'decimo_terceiro_ordenacao': dict_ord_template['oradores_expli'], - 'decimo_quarto_ordenacao': dict_ord_template['ocorr_sessao'] - }) - else: - context.update( - {'primeiro_ordenacao': dict_ord_template['id_basica'], - 'segundo_ordenacao': dict_ord_template['cont_mult'], - 'terceiro_ordenacao': dict_ord_template['mesa_d'], - 'quarto_ordenacao': dict_ord_template['lista_p'], - 'quinto_ordenacao': dict_ord_template['exp'], - 'sexto_ordenacao': dict_ord_template['mat_exp'], - 'setimo_ordenacao': dict_ord_template['v_n_mat_exp'], - 'oitavo_ordenacao': dict_ord_template['oradores_exped'], - 'nono_ordenacao': dict_ord_template['lista_p_o_d'], - 'decimo_ordenacao': dict_ord_template['mat_o_d'], - 'decimo_primeiro_ordenacao': dict_ord_template['v_n_mat_o_d'], - 'decimo_segundo_ordenacao': dict_ord_template['oradores_o_d'], - 'decimo_terceiro_ordenacao': dict_ord_template['oradores_expli'], - 'decimo_quarto_ordenacao': dict_ord_template['ocorr_sessao'] - }) + ordenacao = ResumoOrdenacao.objects.get_or_create()[0] + context.update({ + 'primeiro_ordenacao': dict_ord_template[ordenacao.primeiro], + 'segundo_ordenacao': dict_ord_template[ordenacao.segundo], + 'terceiro_ordenacao': dict_ord_template[ordenacao.terceiro], + 'quarto_ordenacao': dict_ord_template[ordenacao.quarto], + 'quinto_ordenacao': dict_ord_template[ordenacao.quinto], + 'sexto_ordenacao': dict_ord_template[ordenacao.sexto], + 'setimo_ordenacao': dict_ord_template[ordenacao.setimo], + 'oitavo_ordenacao': dict_ord_template[ordenacao.oitavo], + 'nono_ordenacao': dict_ord_template[ordenacao.nono], + 'decimo_ordenacao': dict_ord_template[ordenacao.decimo], + 'decimo_primeiro_ordenacao': dict_ord_template[ordenacao.decimo_primeiro], + 'decimo_segundo_ordenacao': dict_ord_template[ordenacao.decimo_segundo], + 'decimo_terceiro_ordenacao': dict_ord_template[ordenacao.decimo_terceiro], + 'decimo_quarto_ordenacao': dict_ord_template[ordenacao.decimo_quarto] + }) return context @@ -2163,6 +2105,8 @@ class VotacaoView(SessaoPermissionMixin): votacao.ordem_id = ordem_id votacao.tipo_resultado_votacao_id = int( request.POST['resultado_votacao']) + votacao.user = request.user + votacao.ip = get_client_ip(request) votacao.save() except Exception as e: username = request.user.username @@ -2383,6 +2327,8 @@ class VotacaoNominalAbstract(SessaoPermissionMixin): votacao.numero_votos_nao = votos_nao votacao.numero_abstencoes = abstencoes votacao.observacao = request.POST.get('observacao', None) + votacao.user = request.user + votacao.ip = get_client_ip(request) votacao.materia_id = materia_votacao.materia.id if self.ordem: @@ -2410,6 +2356,8 @@ class VotacaoNominalAbstract(SessaoPermissionMixin): voto_parlamentar.voto = voto voto_parlamentar.parlamentar_id = parlamentar_id voto_parlamentar.votacao_id = votacao.id + voto_parlamentar.user = request.user + voto_parlamentar.ip = get_client_ip(request) voto_parlamentar.save() resultado = form.cleaned_data['resultado_votacao'] @@ -2847,10 +2795,10 @@ class VotacaoExpedienteView(SessaoPermissionMixin): if (int(request.POST['voto_presidente']) == 0): qtde_presentes -= 1 - if (qtde_votos > qtde_presentes or qtde_votos < qtde_presentes): + if qtde_votos != qtde_presentes: form._errors["total_votos"] = ErrorList([u""]) return self.render_to_response(context) - elif (qtde_presentes == qtde_votos): + else: try: votacao = RegistroVotacao() votacao.numero_votos_sim = int(request.POST['votos_sim']) @@ -2861,6 +2809,8 @@ class VotacaoExpedienteView(SessaoPermissionMixin): votacao.expediente_id = expediente_id votacao.tipo_resultado_votacao_id = int( request.POST['resultado_votacao']) + votacao.user = request.user + votacao.ip = get_client_ip(request) votacao.save() except Exception as e: username = request.user.username @@ -3544,9 +3494,13 @@ class VotacaoEmBlocoExpediente(PermissionRequiredForAppCrudMixin, ListView): def get_context_data(self, **kwargs): context = super(VotacaoEmBlocoExpediente, self).get_context_data(**kwargs) - context['turno_choices'] = Tramitacao.TURNO_CHOICES context['pk'] = self.kwargs['pk'] context['root_pk'] = self.kwargs['pk'] + if not verifica_sessao_iniciada(self.request, self.kwargs['pk']): + context['sessao_iniciada'] = False + return context + context['sessao_iniciada'] = True + context['turno_choices'] = Tramitacao.TURNO_CHOICES context['title'] = SessaoPlenaria.objects.get(id=self.kwargs['pk']) return context @@ -3566,9 +3520,13 @@ class VotacaoEmBlocoOrdemDia(PermissionRequiredForAppCrudMixin, ListView): def get_context_data(self, **kwargs): context = super(VotacaoEmBlocoOrdemDia, self).get_context_data(**kwargs) - context['turno_choices'] = Tramitacao.TURNO_CHOICES context['pk'] = self.kwargs['pk'] context['root_pk'] = self.kwargs['pk'] + if not verifica_sessao_iniciada(self.request, self.kwargs['pk']): + context['sessao_iniciada'] = False + return context + context['sessao_iniciada'] = True + context['turno_choices'] = Tramitacao.TURNO_CHOICES context['title'] = SessaoPlenaria.objects.get(id=self.kwargs['pk']) return context @@ -3640,6 +3598,8 @@ class VotacaoEmBlocoSimbolicaView(PermissionRequiredForAppCrudMixin, TemplateVie resultado = TipoResultadoVotacao.objects.get( id=request.POST['resultado_votacao']) votacao.tipo_resultado_votacao = resultado + votacao.user = request.user + votacao.ip = get_client_ip(request) votacao.save() except Exception as e: username = request.user.username @@ -3671,6 +3631,8 @@ class VotacaoEmBlocoSimbolicaView(PermissionRequiredForAppCrudMixin, TemplateVie resultado = TipoResultadoVotacao.objects.get( id=request.POST['resultado_votacao']) votacao.tipo_resultado_votacao = resultado + votacao.user = request.user + votacao.ip = get_client_ip(request) votacao.save() except Exception as e: username = request.user.username @@ -3848,6 +3810,8 @@ class VotacaoEmBlocoNominalView(PermissionRequiredForAppCrudMixin, TemplateView) votacao.materia = ordem.materia votacao.ordem = ordem votacao.tipo_resultado_votacao = form.cleaned_data['resultado_votacao'] + votacao.user = request.user + votacao.ip = get_client_ip(request) votacao.save() for votos in request.POST.getlist('voto_parlamentar'): @@ -3862,6 +3826,8 @@ class VotacaoEmBlocoNominalView(PermissionRequiredForAppCrudMixin, TemplateView) voto_parlamentar.voto = voto voto_parlamentar.parlamentar_id = parlamentar_id voto_parlamentar.votacao_id = votacao.id + voto_parlamentar.user = request.user + voto_parlamentar.ip = get_client_ip(request) voto_parlamentar.save() ordem.resultado = form.cleaned_data['resultado_votacao'].nome @@ -3889,6 +3855,8 @@ class VotacaoEmBlocoNominalView(PermissionRequiredForAppCrudMixin, TemplateView) votacao.materia = expediente.materia votacao.expediente = expediente votacao.tipo_resultado_votacao = form.cleaned_data['resultado_votacao'] + votacao.user = request.user + votacao.ip = get_client_ip(request) votacao.save() # Salva os votos de cada parlamentar @@ -3904,6 +3872,8 @@ class VotacaoEmBlocoNominalView(PermissionRequiredForAppCrudMixin, TemplateView) voto_parlamentar.voto = voto voto_parlamentar.parlamentar_id = parlamentar_id voto_parlamentar.votacao_id = votacao.id + voto_parlamentar.user = request.user + voto_parlamentar.ip = get_client_ip(request) voto_parlamentar.save() expediente.resultado = form.cleaned_data['resultado_votacao'].nome diff --git a/sapl/settings.py b/sapl/settings.py index af1b8d697..ae900faf7 100644 --- a/sapl/settings.py +++ b/sapl/settings.py @@ -41,7 +41,7 @@ ALLOWED_HOSTS = ['*'] LOGIN_REDIRECT_URL = '/' LOGIN_URL = '/login/?next=' -SAPL_VERSION = '3.1.153' +SAPL_VERSION = '3.1.155' if DEBUG: EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' diff --git a/sapl/templates/base.html b/sapl/templates/base.html index b19ec673e..a2b37cd02 100644 --- a/sapl/templates/base.html +++ b/sapl/templates/base.html @@ -179,7 +179,7 @@ Desenvolvido pelo Interlegis em software livre e aberto. - Release: 3.1.153 + Release: 3.1.155

diff --git a/sapl/templates/base/autores_duplicados.html b/sapl/templates/base/autores_duplicados.html index a7861aecd..361ff7add 100644 --- a/sapl/templates/base/autores_duplicados.html +++ b/sapl/templates/base/autores_duplicados.html @@ -11,16 +11,14 @@ Autor - Tipo de Autor Quantidade - {% for autor, tipo, quantidade in autores_duplicados %} + {% for autor in autores_duplicados %} - {{ autor }} - {{ tipo }} - {{ quantidade }} + {{ autor.nome }} + {{ autor.count }} {% endfor %} diff --git a/sapl/templates/base/layouts.yaml b/sapl/templates/base/layouts.yaml index c98810b10..4768577b8 100644 --- a/sapl/templates/base/layouts.yaml +++ b/sapl/templates/base/layouts.yaml @@ -18,8 +18,9 @@ AppConfig: - esfera_federacao {% trans 'Proposições e Protocolo' %}: - - sequencia_numeracao proposicao_incorporacao_obrigatoria receber_recibo_proposicao - - escolher_numero_materia_proposicao protocolo_manual + - sequencia_numeracao_proposicao sequencia_numeracao_protocolo + - protocolo_manual receber_recibo_proposicao + - proposicao_incorporacao_obrigatoria escolher_numero_materia_proposicao {% trans 'Textos Articulados' %}: - texto_articulado_proposicao texto_articulado_materia texto_articulado_norma @@ -31,7 +32,8 @@ AppConfig: - assinatura_ata {% trans 'Cronômetros do Painel' %}: - - cronometro_discurso cronometro_aparte cronometro_ordem cronometro_consideracoes + - cronometro_discurso cronometro_aparte + - cronometro_ordem cronometro_consideracoes {% trans 'Configurações do Painel' %}: - mostrar_brasao_painel diff --git a/sapl/templates/compilacao/text_list_bloco.html b/sapl/templates/compilacao/text_list_bloco.html index 0c2900734..2ef5e359a 100644 --- a/sapl/templates/compilacao/text_list_bloco.html +++ b/sapl/templates/compilacao/text_list_bloco.html @@ -20,7 +20,6 @@
- {% if not dpt.tipo_dispositivo.dispositivo_de_articulacao or dpt.tipo_dispositivo.dispositivo_de_articulacao and dpt.dispositivo_subsequente %} {% if dpt.auto_inserido %} {{ dpt.dispositivo_pai.tipo_dispositivo.rotulo_prefixo_html|safe }} @@ -31,6 +30,7 @@ {{ dpt.rotulo }} {{ dpt.tipo_dispositivo.rotulo_sufixo_html|safe }} {% endif %} + {% endif %} {{ dpt.tipo_dispositivo.texto_prefixo_html|safe }}{%if dpt.texto %}{{ dpt.texto|safe }}{%else%}{%if not dpt.tipo_dispositivo.dispositivo_de_articulacao %} {% endif %}{% endif %} @@ -43,20 +43,21 @@   {% endif %} - {% endif %} - {% if user.is_authenticated and not dpt.tipo_dispositivo.dispositivo_de_articulacao%} - {% if perms.compilacao.add_nota or perms.compilacao.add_vide or perms.compilacao.change_dispositivo%} -
-
    - {% if perms.compilacao.change_dispositivo %}
  • Ed
  • {% endif %} - {% if perms.compilacao.add_nota %}
  • N
  • {% endif %} - {% if perms.compilacao.add_vide %}
  • V
  • {% endif %} -
-
-
- {% endif %} + + {% if user.is_authenticated and not dpt.tipo_dispositivo.dispositivo_de_articulacao%} + {% if perms.compilacao.add_nota or perms.compilacao.add_vide or perms.compilacao.change_dispositivo%} +
+
    + {% if perms.compilacao.change_dispositivo %}
  • Ed
  • {% endif %} + {% if perms.compilacao.add_nota %}
  • N
  • {% endif %} + {% if perms.compilacao.add_vide %}
  • V
  • {% endif %} +
+
+
{% endif %} + {% endif %} +
{% if not dpt.tipo_dispositivo.dispositivo_de_articulacao %} diff --git a/sapl/templates/materia/em_lote/anexada.html b/sapl/templates/materia/em_lote/anexada.html index 76a154ec6..2b1c22c56 100644 --- a/sapl/templates/materia/em_lote/anexada.html +++ b/sapl/templates/materia/em_lote/anexada.html @@ -8,11 +8,11 @@ {% endif %} {% if show_results %} - {% if object_list.count > 0 %} - {% if object_list.count == 1 %} + {% if numero_res > 0 %} + {% if numero_res == 1 %}

{% trans 'Pesquisa concluída com sucesso! Foi encontrada 1 matéria.'%}

{% else %} -

{% blocktrans with object_list.count as total_materias %}Foram encontradas {{total_materias}} matérias.{% endblocktrans %}

+

Foram encontradas {{ numero_res }} matérias.

{% endif %}
{% csrf_token %} diff --git a/sapl/templates/materia/tramitacao_detail.html b/sapl/templates/materia/tramitacao_detail.html new file mode 100644 index 000000000..4aaca09b6 --- /dev/null +++ b/sapl/templates/materia/tramitacao_detail.html @@ -0,0 +1,38 @@ +{% extends "crud/detail.html" %} +{% load i18n %} + +{% block detail_content %} + {{ block.super }} + {% if user.is_superuser %} +
+ {% if tramitacao.user %} +
+
+

Usuário

+
+ +
+
+
+ {% endif %} + {% if tramitacao.ip %} +
+
+

IP

+
+
+
+ {{tramitacao.ip}} +
+
+
+
+
+ {% endif %} +
+ {% endif %} +{% endblock detail_content %} \ No newline at end of file diff --git a/sapl/templates/menu_tabelas_auxiliares.yaml b/sapl/templates/menu_tabelas_auxiliares.yaml index 4f690ca00..9a9c40d8d 100644 --- a/sapl/templates/menu_tabelas_auxiliares.yaml +++ b/sapl/templates/menu_tabelas_auxiliares.yaml @@ -78,7 +78,7 @@ url: sapl.parlamentares:frente_list css_class: btn btn-link - title: {% trans 'Bloco Parlamentar' %} - url: sapl.sessao:bloco_list + url: sapl.parlamentares:bloco_list css_class: btn btn-link - title: {% trans 'Módulo Proposições' %} css_class: head_title diff --git a/sapl/templates/navbar.yaml b/sapl/templates/navbar.yaml index 673d1a215..de556d882 100644 --- a/sapl/templates/navbar.yaml +++ b/sapl/templates/navbar.yaml @@ -75,7 +75,7 @@ - title: {% trans 'Administração de Usuários' %} url: {% url 'sapl.base:usuario' %} check_permission: user.is_superuser - - title: {% trans 'Inconsistências da Aplicação' %} + - title: {% trans 'Inconsistências de Dados' %} url: {% url 'sapl.base:lista_inconsistencias' %} check_permission: user.is_superuser diff --git a/sapl/templates/parlamentares/layouts.yaml b/sapl/templates/parlamentares/layouts.yaml index 030e32758..7c3e59908 100644 --- a/sapl/templates/parlamentares/layouts.yaml +++ b/sapl/templates/parlamentares/layouts.yaml @@ -127,3 +127,10 @@ Votante: {% trans 'Votante' %}: - parlamentar user - data + +Bloco: + {% trans 'Bloco' %}: + - nome + - data_criacao data_extincao + - partidos + - descricao \ No newline at end of file diff --git a/sapl/templates/protocoloadm/anexado_list.html b/sapl/templates/protocoloadm/anexado_list.html new file mode 100644 index 000000000..1dfe166c6 --- /dev/null +++ b/sapl/templates/protocoloadm/anexado_list.html @@ -0,0 +1,13 @@ +{% extends "crud/list.html" %} +{% load i18n %} +{% load common_tags %} + +{% block more_buttons %} + + {% if perms|get_add_perm:view %} + + {% trans "Adicionar Anexado em Lote" %} + + {% endif %} + +{% endblock more_buttons %} \ No newline at end of file diff --git a/sapl/templates/protocoloadm/em_lote/anexado.html b/sapl/templates/protocoloadm/em_lote/anexado.html index a8ddf6712..dfb0714ca 100644 --- a/sapl/templates/protocoloadm/em_lote/anexado.html +++ b/sapl/templates/protocoloadm/em_lote/anexado.html @@ -54,7 +54,7 @@ - {{documento.tipo.sigla}} {{documento.numero}}/{{documento.ano}} - {{documento.tipo.descricao}} + {{documento.tipo.sigla}} {{documento.numero}}/{{documento.ano}} - {{documento.tipo.descricao}} {% endfor %} @@ -63,6 +63,7 @@
+
{% else %} @@ -75,7 +76,7 @@ {% block extra_js %}