diff --git a/.gitignore b/.gitignore index 429873229..65adbb56b 100644 --- a/.gitignore +++ b/.gitignore @@ -87,6 +87,7 @@ target/ *.sublime-workspace .ipynb_checkpoints/ *.ipynb +.vscode/ # specific to this project diff --git a/docker-compose.yml b/docker-compose.yml index 1bfcf65f5..3982527fb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,5 @@ sapldb: - image: postgres:9.6.8-alpine + image: postgres:10.5-alpine restart: always environment: POSTGRES_PASSWORD: sapl @@ -11,7 +11,7 @@ sapldb: ports: - "5432:5432" sapl: - image: interlegis/sapl:3.1.122 + image: interlegis/sapl:3.1.128 restart: always environment: ADMIN_PASSWORD: interlegis diff --git a/release.sh b/release.sh index 958e91b21..d8301aafe 100755 --- a/release.sh +++ b/release.sh @@ -21,7 +21,7 @@ function bump_version { function commit_and_push { echo "committing..." - git add docker-compose.yml setup.py + git add docker-compose.yml setup.py sapl/templates/base.html git commit -m "Release: $NEXT_VERSION" git tag $NEXT_VERSION diff --git a/sapl/audiencia/views.py b/sapl/audiencia/views.py index c999cf4ff..0a8da8af1 100644 --- a/sapl/audiencia/views.py +++ b/sapl/audiencia/views.py @@ -1,4 +1,5 @@ from django.http import HttpResponse +from django.core.urlresolvers import reverse from django.views.decorators.clickjacking import xframe_options_exempt from django.views.generic import UpdateView from sapl.crud.base import RP_DETAIL, RP_LIST, Crud @@ -23,6 +24,23 @@ class AudienciaCrud(Crud): class ListView(Crud.ListView): paginate_by = 10 + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + audiencia_materia = {} + for o in context['object_list']: + # indexado pelo numero da audiencia + audiencia_materia[str(o.numero)] = o.materia + + for row in context['rows']: + coluna_materia = row[3] # se mudar a ordem de listagem mudar aqui + if coluna_materia[0]: + materia = audiencia_materia[row[0][0]] + url_materia = reverse('sapl.materia:materialegislativa_detail', + kwargs={'pk': materia.id}) + row[3] = (coluna_materia[0], url_materia) + return context + class CreateView(Crud.CreateView): form_class = AudienciaForm diff --git a/sapl/materia/email_utils.py b/sapl/base/email_utils.py similarity index 56% rename from sapl/materia/email_utils.py rename to sapl/base/email_utils.py index 3dc6b220d..737449e57 100644 --- a/sapl/materia/email_utils.py +++ b/sapl/base/email_utils.py @@ -8,7 +8,8 @@ from django.utils import timezone from sapl.base.models import CasaLegislativa from sapl.settings import EMAIL_SEND_USER -from .models import AcompanhamentoMateria +from sapl.materia.models import AcompanhamentoMateria +from sapl.protocoloadm.models import AcompanhamentoDocumento def load_email_templates(templates, context={}): @@ -61,56 +62,73 @@ def enviar_emails(sender, recipients, messages): fail_silently=False) -def criar_email_confirmacao(base_url, casa_legislativa, materia, hash_txt=''): +def criar_email_confirmacao(base_url, casa_legislativa, doc_mat, tipo, hash_txt=''): if not casa_legislativa: raise ValueError("Casa Legislativa é obrigatória") - if not materia: - raise ValueError("Matéria é obrigatória") + if not doc_mat: + if tipo == "materia": + msg = "Matéria é obrigatória" + else: + msg = "Documento é obrigatório" + raise ValueError(msg) # FIXME i18n - casa_nome = (casa_legislativa.nome + ' de ' + - casa_legislativa.municipio + '-' + - casa_legislativa.uf) + casa_nome = ("{} de {} - {}".format(casa_legislativa.nome, + casa_legislativa.municipio, + casa_legislativa.uf)) + + if tipo == "materia": + doc_mat_url = reverse('sapl.materia:materialegislativa_detail', + kwargs={'pk': doc_mat.id}) + confirmacao_url = reverse('sapl.materia:acompanhar_confirmar', + kwargs={'pk': doc_mat.id}) + ementa = doc_mat.ementa + autores = [autoria.autor.nome for autoria in doc_mat.autoria_set.all()] + else: + doc_mat_url = reverse('sapl.protocoloadm:documentoadministrativo_detail', + kwargs={'pk': doc_mat.id}) + confirmacao_url = reverse('sapl.protocoloadm:acompanhar_confirmar', + kwargs={'pk': doc_mat.id}) + ementa = doc_mat.assunto + autores = "" - materia_url = reverse('sapl.materia:materialegislativa_detail', - kwargs={'pk': materia.id}) - confirmacao_url = reverse('sapl.materia:acompanhar_confirmar', - kwargs={'pk': materia.id}) - autores = [] - for autoria in materia.autoria_set.all(): - autores.append(autoria.autor.nome) templates = load_email_templates(['email/acompanhar.txt', 'email/acompanhar.html'], {"casa_legislativa": casa_nome, "logotipo": casa_legislativa.logotipo, - "descricao_materia": materia.ementa, + "descricao_materia": ementa, "autoria": autores, "hash_txt": hash_txt, "base_url": base_url, - "materia": str(materia), - "materia_url": materia_url, + "materia": str(doc_mat), + "materia_url": doc_mat_url, "confirmacao_url": confirmacao_url, }) return templates -def do_envia_email_confirmacao(base_url, casa, materia, destinatario): +def do_envia_email_confirmacao(base_url, casa, tipo, doc_mat, destinatario): # # Envia email de confirmacao para atualizações de tramitação # sender = EMAIL_SEND_USER # FIXME i18n - subject = "[SAPL] " + str(materia) + " - Ative o Acompanhamento da Materia" + if tipo == "materia": + msg = " - Ative o Acompanhamento da Matéria" + else: + msg = " - Ative o Acompanhamento de Documento" + subject = "[SAPL] {} {}".format(str(doc_mat), msg) messages = [] recipients = [] email_texts = criar_email_confirmacao(base_url, casa, - materia, + doc_mat, + tipo, destinatario.hash,) recipients.append(destinatario.email) messages.append({ @@ -123,30 +141,41 @@ def do_envia_email_confirmacao(base_url, casa, materia, destinatario): enviar_emails(sender, recipients, messages) -def criar_email_tramitacao(base_url, casa_legislativa, materia, status, +def criar_email_tramitacao(base_url, casa_legislativa, tipo, doc_mat, status, unidade_destino, hash_txt=''): if not casa_legislativa: raise ValueError("Casa Legislativa é obrigatória") - if not materia: - raise ValueError("Matéria é obrigatória") + if not doc_mat: + if tipo == "materia": + msg = "Matéria é obrigatória" + else: + msg = "Documento é obrigatório" + raise ValueError(msg) # FIXME i18n - casa_nome = (casa_legislativa.nome + ' de ' + - casa_legislativa.municipio + '-' + - casa_legislativa.uf) - - url_materia = reverse('sapl.materia:tramitacao_list', - kwargs={'pk': materia.id}) - url_excluir = reverse('sapl.materia:acompanhar_excluir', - kwargs={'pk': materia.id}) + casa_nome = ("{} de {} - {}".format(casa_legislativa.nome, + casa_legislativa.municipio, + casa_legislativa.uf)) + if tipo == "materia": + doc_mat_url = reverse('sapl.materia:tramitacao_list', + kwargs={'pk': doc_mat.id}) + url_excluir = reverse('sapl.materia:acompanhar_excluir', + kwargs={'pk': doc_mat.id}) + + ementa = doc_mat.ementa + autores = [autoria.autor.nome for autoria in doc_mat.autoria_set.all()] + tramitacao = doc_mat.tramitacao_set.last() - autores = [] - for autoria in materia.autoria_set.all(): - autores.append(autoria.autor.nome) - - tramitacao = materia.tramitacao_set.last() + else: + doc_mat_url = reverse('sapl.protocoloadm:tramitacaoadministrativo_list', + kwargs={'pk': doc_mat.id}) + url_excluir = reverse('sapl.protocoloadm:acompanhar_excluir', + kwargs={'pk': doc_mat.id}) + autores = "" + ementa = doc_mat.assunto + tramitacao = doc_mat.tramitacaoadministrativo_set.last() templates = load_email_templates(['email/tramitacao.txt', 'email/tramitacao.html'], @@ -154,34 +183,42 @@ def criar_email_tramitacao(base_url, casa_legislativa, materia, status, "data_registro": dt.strftime( timezone.now(), "%d/%m/%Y"), - "cod_materia": materia.id, + "cod_materia": doc_mat.id, "logotipo": casa_legislativa.logotipo, - "descricao_materia": materia.ementa, + "descricao_materia": ementa, "autoria": autores, "data": tramitacao.data_tramitacao, "status": status, "localizacao": unidade_destino, "texto_acao": tramitacao.texto, "hash_txt": hash_txt, - "materia": str(materia), + "materia": str(doc_mat), "base_url": base_url, - "materia_url": url_materia, + "materia_url": doc_mat_url, "excluir_url": url_excluir}) return templates -def do_envia_email_tramitacao(base_url, materia, status, unidade_destino): +def do_envia_email_tramitacao(base_url, tipo, doc_mat, status, unidade_destino): # # Envia email de tramitacao para usuarios cadastrados # - destinatarios = AcompanhamentoMateria.objects.filter(materia=materia, - confirmado=True) + if tipo == "materia": + destinatarios = AcompanhamentoMateria.objects.filter(materia=doc_mat, + confirmado=True) + else: + destinatarios = AcompanhamentoDocumento.objects.filter(documento=doc_mat, + confirmado=True) + casa = CasaLegislativa.objects.first() sender = EMAIL_SEND_USER - # FIXME i18n - subject = "[SAPL] " + str(materia) + \ - " - Acompanhamento de Materia Legislativa" + # FIXME i18nn + if tipo == "materia": + msg = " - Acompanhamento de Matéria Legislativa" + else: + msg = " - Acompanhamento de Documento" + subject = "[SAPL] {} {}".format(str(doc_mat), msg) connection = get_connection() connection.open() @@ -190,10 +227,11 @@ def do_envia_email_tramitacao(base_url, materia, status, unidade_destino): try: email_texts = criar_email_tramitacao(base_url, casa, - materia, + tipo, + doc_mat, status, unidade_destino, - destinatario.hash,) + destinatario.hash) email = EmailMultiAlternatives( subject, diff --git a/sapl/base/forms.py b/sapl/base/forms.py index 028f2c464..7d6705687 100644 --- a/sapl/base/forms.py +++ b/sapl/base/forms.py @@ -262,6 +262,32 @@ class TipoAutorForm(ModelForm): super(TipoAutorForm, self).__init__(*args, **kwargs) + def clean(self): + super(TipoAutorForm, self).clean() + + if not self.is_valid(): + return self.cleaned_data + + cd = self.cleaned_data + lista = ['comissão', + 'comis', + 'parlamentar', + 'bancada', + 'bloco', + 'comissao', + 'vereador', + 'órgão', + 'orgao', + 'deputado', + 'senador', + 'vereadora', + 'frente'] + + for l in lista: + if l in cd['descricao'].lower(): + raise ValidationError(_('A descrição colocada não pode ser usada ' + 'por ser equivalente a um tipo já existente')) + class AutorForm(ModelForm): senha = forms.CharField( diff --git a/sapl/base/receivers.py b/sapl/base/receivers.py new file mode 100644 index 000000000..b2176be14 --- /dev/null +++ b/sapl/base/receivers.py @@ -0,0 +1,42 @@ +from django.db.models.signals import post_delete, post_save +from django.dispatch import receiver + +from sapl.materia.models import Tramitacao +from sapl.protocoloadm.models import TramitacaoAdministrativo +from sapl.base.signals import tramitacao_signal +from sapl.utils import get_base_url + +from sapl.base.email_utils import do_envia_email_tramitacao + + +@receiver(tramitacao_signal) +def handle_tramitacao_signal(sender, **kwargs): + tramitacao = kwargs.get("post") + request = kwargs.get("request") + if 'protocoloadm' in str(sender): + doc_mat = tramitacao.documento + tipo = "documento" + elif 'materia' in str(sender): + tipo = "materia" + doc_mat = tramitacao.materia + + do_envia_email_tramitacao( + get_base_url(request), + tipo, + doc_mat, + tramitacao.status, + tramitacao.unidade_tramitacao_destino) + + +@receiver(post_delete) +def status_tramitacao_materia(sender, instance, **kwargs): + if isinstance(sender, TramitacaoAdministrativo): + if instance.status.indicador == 'F': + materia = instance.materia + materia.em_tramitacao = True + materia.save() + elif isinstance(sender, TramitacaoAdministrativo): + if instance.status.indicador == 'F': + documento = instance.documento + documento.tramitacao = True + documento.save() diff --git a/sapl/materia/signals.py b/sapl/base/signals.py similarity index 100% rename from sapl/materia/signals.py rename to sapl/base/signals.py diff --git a/sapl/base/templatetags/common_tags.py b/sapl/base/templatetags/common_tags.py index 71f63e130..440f747a4 100644 --- a/sapl/base/templatetags/common_tags.py +++ b/sapl/base/templatetags/common_tags.py @@ -198,6 +198,19 @@ def url(value): return True return False +@register.filter +def audio_url(value): + return True if url(value) and value.endswith("mp3") else False + + +@register.filter +def video_url(value): + return True if url(value) and value.endswith("mp4") else False + +@register.filter +def file_extension(value): + import pathlib + return pathlib.Path(value).suffix.replace('.', '') @register.filter def cronometro_to_seconds(value): diff --git a/sapl/compilacao/views.py b/sapl/compilacao/views.py index 82b5cbd10..6cf1911d6 100644 --- a/sapl/compilacao/views.py +++ b/sapl/compilacao/views.py @@ -2892,7 +2892,21 @@ class DispositivoSearchFragmentFormView(ListView): itens.append(item) return JsonResponse(itens, safe=False) - return ListView.get(self, request, *args, **kwargs) + response = ListView.get(self, request, *args, **kwargs) + + if not self.object_list.exists(): + messages.info( + request, _('Não foram encontrados resultados ' + 'com seus critérios de busca!')) + + try: + r = response.render() + return response + except Exception as e: + messages.error(request, "Erro - %s" % e) + context = {} + self.template_name = 'compilacao/messages.html' + return self.render_to_response(context) def get_queryset(self): try: diff --git a/sapl/legacy/management/commands/ressucitar_deps.py b/sapl/legacy/management/commands/ressuscitar_deps.py similarity index 54% rename from sapl/legacy/management/commands/ressucitar_deps.py rename to sapl/legacy/management/commands/ressuscitar_deps.py index 70900e887..ea219330c 100644 --- a/sapl/legacy/management/commands/ressucitar_deps.py +++ b/sapl/legacy/management/commands/ressuscitar_deps.py @@ -1,12 +1,12 @@ from django.core.management.base import BaseCommand -from sapl.legacy.scripts.ressucita_dependencias import adiciona_ressucitar +from sapl.legacy.scripts.ressuscita_dependencias import adiciona_ressuscitar class Command(BaseCommand): - help = 'Ressucita dependências apagadas ' \ + help = 'Ressuscita dependências apagadas ' \ 'que são necessárias para migrar outros registros' def handle(self, *args, **options): - adiciona_ressucitar() + adiciona_ressuscitar() diff --git a/sapl/legacy/migracao.py b/sapl/legacy/migracao.py index c4c183b84..94fd25930 100644 --- a/sapl/legacy/migracao.py +++ b/sapl/legacy/migracao.py @@ -70,12 +70,13 @@ def scrap_sde(url, usuario, senha=None): {'__ac_name': usuario, '__ac_password': senha}) assert res.status_code == 200 - url_proposicao = '{}/sapl_documentos/proposicao/{}/renderXML?xsl=__default__' # noqa + url_proposicao_tmpl = '{}/sapl_documentos/proposicao/{}/renderXML?xsl=__default__' # noqa total = Proposicao.objects.count() for num, proposicao in enumerate(Proposicao.objects.all()): pk = proposicao.pk - res = session.get(url_proposicao.format(url, pk)) - print("pk: {} status: {} (progresso: {:.2%})".format( - pk, res.status_code, num / total)) + url_proposicao = url_proposicao_tmpl.format(url, pk) + res = session.get(url_proposicao) + print("pk: {} status: {} {} (progresso: {:.2%})".format( + pk, res.status_code, url_proposicao, num / total)) if res.status_code == 200: salva_conteudo_do_sde(proposicao, res.content) diff --git a/sapl/legacy/migracao_dados.py b/sapl/legacy/migracao_dados.py index 1be1f6e96..1def7f9c3 100644 --- a/sapl/legacy/migracao_dados.py +++ b/sapl/legacy/migracao_dados.py @@ -623,6 +623,36 @@ def corrige_unidades_tramitacao_destino_vazia_como_anterior(): '''.format(tabela_tramitacao)) +def apaga_ref_a_mats_e_docs_inexistentes_em_proposicoes(): + # as referencias a matérias e documentos apagados não aparecem no 3.1 + # além do que, se ressuscitássemos essas matérias e docs, + # não seria possível apagá-los, + # pois é impossível para um usuário não autor acessar as proposicões + # para apagar a referências antes + exec_legado(''' + update proposicao set cod_materia = NULL where cod_materia not in ( + select cod_materia from materia_legislativa + where ind_excluido <> 1); + ''') + props_sem_mats = list(primeira_coluna(exec_legado(''' + select cod_proposicao from proposicao p inner join tipo_proposicao t + on p.tip_proposicao = t.tip_proposicao + where t.ind_mat_ou_doc = 'M' and cod_mat_ou_doc not in ( + select cod_materia from materia_legislativa + where ind_excluido <> 1) + '''))) + props_sem_docs = list(primeira_coluna(exec_legado(''' + select cod_proposicao from proposicao p inner join tipo_proposicao t + on p.tip_proposicao = t.tip_proposicao + where t.ind_mat_ou_doc = 'D' and cod_mat_ou_doc not in ( + select cod_documento from documento_acessorio + where ind_excluido <> 1); + '''))) + exec_legado_em_subconjunto(''' + update proposicao set cod_mat_ou_doc = NULL + where cod_proposicao in {}''', props_sem_mats + props_sem_docs) + + def uniformiza_banco(): propaga_exclusoes(PROPAGACOES_DE_EXCLUSAO) checa_registros_votacao_ambiguos_e_remove_nao_usados() @@ -722,6 +752,8 @@ sessao_plenaria_presenca | dat_sessao = NULL | dat_sessao = 0 select cod_materia from materia_legislativa where ind_excluido <> 1);''') + apaga_ref_a_mats_e_docs_inexistentes_em_proposicoes() + class Record: pass diff --git a/sapl/legacy/migracao_documentos.py b/sapl/legacy/migracao_documentos.py index 66ada19fc..2c9e00842 100644 --- a/sapl/legacy/migracao_documentos.py +++ b/sapl/legacy/migracao_documentos.py @@ -1,4 +1,5 @@ import os +import shutil import re from glob import glob from os.path import join @@ -52,6 +53,12 @@ def mover_documento(repo, origem, destino, ignora_origem_ausente=False): if ignora_origem_ausente and not os.path.exists(origem): print('Origem ignorada ao mover documento: {}'.format(origem)) return + # apaga destino, se houver, e renomeia origem para destino + if os.path.exists(destino): + if os.path.isdir(destino): + shutil.rmtree(destino) + else: + os.remove(destino) os.makedirs(os.path.dirname(destino), exist_ok=True) os.rename(origem, destino) diff --git a/sapl/legacy/scripts/ressucita_dependencias.py b/sapl/legacy/scripts/ressuscita_dependencias.py similarity index 96% rename from sapl/legacy/scripts/ressucita_dependencias.py rename to sapl/legacy/scripts/ressuscita_dependencias.py index 9810924a9..5e12efc01 100644 --- a/sapl/legacy/scripts/ressucita_dependencias.py +++ b/sapl/legacy/scripts/ressuscita_dependencias.py @@ -70,6 +70,7 @@ tipo_dependente /sistema/parlamentar/tipo-dependente origem /sistema/materia/origem documento_acessorio /materia/documentoacessorio tipo_fim_relatoria /sistema/materia/tipo-fim-relatoria +tipo_situacao_militar /sistema/parlamentar/tipo-militar ''' urls = dict(stripsplit(urls)) @@ -194,7 +195,7 @@ Para facilitar sua conferência, seguem os links para as proposições envolvida '''.format(table.draw(), links, sqls) -def get_dependencias_a_ressucitar(slug): +def get_dependencias_a_ressuscitar(slug): ocorrencias = yaml.load( Path(DIR_REPO.child('ocorrencias.yaml').read_file())) fks_faltando = ocorrencias.get('fk') @@ -265,7 +266,7 @@ SQLS_CRIACAO = [ ('unidade_tramitacao', ''' insert into unidade_tramitacao ( cod_unid_tramitacao, cod_comissao, cod_orgao, cod_parlamentar, ind_excluido) - values ({}, NULL, NULL, NULL, 0); + values ({}, NULL, NULL, 0, 0); '''), ('autor', SQL_INSERT_TIPO_AUTOR.format(0) + ''' insert into autor ( @@ -307,6 +308,9 @@ SQLS_CRIACAO = [ insert into parlamentar (cod_parlamentar, nom_completo, nom_parlamentar, sex_parlamentar, cod_casa, ind_ativo, ind_unid_deliberativa, ind_excluido) values ({}, "DESCONHECIDO", "DESCONHECIDO", "M", 0, 0, 0, 0); '''), + ('tipo_sessao_plenaria', ''' + insert into tipo_sessao_plenaria (tip_sessao, nom_sessao, ind_excluido, num_minimo) values ({}, "DESCONHECIDO", 0, 0); + '''), ] SQLS_CRIACAO = {k: (dedent(sql.strip()), extras) for k, sql, *extras in SQLS_CRIACAO} @@ -349,8 +353,8 @@ def get_sql_criar(tabela_alvo, campo, valor, slug): return sql, links -TEMPLATE_RESSUCITADOS = '''{} -/* RESSUCITADOS +TEMPLATE_RESSUSCITADOS = '''{} +/* RESSUSCITADOS SOBRE REGISTROS QUE ESTAVAM APAGADOS E FORAM RESTAURADOS @@ -397,11 +401,11 @@ def get_sqls_desexcluir_criar(preambulo, desexcluir, criar, slug): links = sem_repeticoes_mantendo_ordem(links) sqls, links = ['\n'.join(sorted(s)) for s in [sqls, links]] - return TEMPLATE_RESSUCITADOS.format(preambulo, links, sqls) + return TEMPLATE_RESSUSCITADOS.format(preambulo, links, sqls) -def get_ressucitar(slug): - preambulo, desexcluir, criar = get_dependencias_a_ressucitar(slug) +def get_ressuscitar(slug): + preambulo, desexcluir, criar = get_dependencias_a_ressuscitar(slug) return get_sqls_desexcluir_criar(preambulo, desexcluir, criar, slug) @@ -413,8 +417,8 @@ def get_slug(): return siglas_para_slugs[sigla] -def adiciona_ressucitar(): - sqls = get_ressucitar(get_slug()) +def adiciona_ressuscitar(): + sqls = get_ressuscitar(get_slug()) if sqls.strip(): arq_ajustes_pre_migracao = get_arquivo_ajustes_pre_migracao() conteudo = arq_ajustes_pre_migracao.read_file() diff --git a/sapl/materia/apps.py b/sapl/materia/apps.py index bb4f72f73..2cc3559ae 100644 --- a/sapl/materia/apps.py +++ b/sapl/materia/apps.py @@ -8,4 +8,4 @@ class AppConfig(apps.AppConfig): verbose_name = _('Matéria') def ready(self): - from . import receivers + from sapl.base import receivers diff --git a/sapl/materia/forms.py b/sapl/materia/forms.py index 6f6a544cd..b244af69a 100644 --- a/sapl/materia/forms.py +++ b/sapl/materia/forms.py @@ -1855,14 +1855,13 @@ class ConfirmarProposicaoForm(ProposicaoForm): else: # numeracao == 'U' ou não informada nm = Protocolo.objects.all().aggregate(Max('numero')) - protocolo = Protocolo() protocolo.numero = (nm['numero__max'] + 1) if nm['numero__max'] else 1 protocolo.ano = timezone.now().year protocolo.tipo_protocolo = '1' - protocolo.interessado = str(proposicao.autor) + protocolo.interessado = str(proposicao.autor)[:200] # tamanho máximo 200 protocolo.autor = proposicao.autor protocolo.assunto_ementa = proposicao.descricao protocolo.numero_paginas = cd['numero_de_paginas'] diff --git a/sapl/materia/migrations/0032_auto_20181022_1743.py b/sapl/materia/migrations/0032_auto_20181022_1743.py new file mode 100644 index 000000000..8721e831b --- /dev/null +++ b/sapl/materia/migrations/0032_auto_20181022_1743.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.13 on 2018-10-22 20:43 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('materia', '0031_auto_20180924_1724'), + ] + + operations = [ + migrations.AlterField( + model_name='autoria', + name='autor', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='base.Autor', verbose_name='Autor'), + ), + ] diff --git a/sapl/materia/models.py b/sapl/materia/models.py index dd9f8ddab..3c1b4862e 100644 --- a/sapl/materia/models.py +++ b/sapl/materia/models.py @@ -307,7 +307,7 @@ class MateriaLegislativa(models.Model): class Autoria(models.Model): autor = models.ForeignKey(Autor, verbose_name=_('Autor'), - on_delete=models.CASCADE) + on_delete=models.PROTECT) materia = models.ForeignKey( MateriaLegislativa, on_delete=models.CASCADE, verbose_name=_('Matéria Legislativa')) diff --git a/sapl/materia/receivers.py b/sapl/materia/receivers.py deleted file mode 100644 index 945c6636e..000000000 --- a/sapl/materia/receivers.py +++ /dev/null @@ -1,29 +0,0 @@ -from django.db.models.signals import post_delete, post_save -from django.dispatch import receiver - -from sapl.materia.models import Tramitacao -from sapl.materia.signals import tramitacao_signal -from sapl.utils import get_base_url - -from .email_utils import do_envia_email_tramitacao - - -@receiver(tramitacao_signal) -def handle_tramitacao_signal(sender, **kwargs): - tramitacao = kwargs.get("post") - request = kwargs.get("request") - materia = tramitacao.materia - - do_envia_email_tramitacao( - get_base_url(request), - materia, - tramitacao.status, - tramitacao.unidade_tramitacao_destino) - - -@receiver(post_delete, sender=Tramitacao) -def status_tramitacao_materia(sender, instance, **kwargs): - if instance.status.indicador == 'F': - materia = instance.materia - materia.em_tramitacao = True - materia.save() diff --git a/sapl/materia/tests/test_email_templates.py b/sapl/materia/tests/test_email_templates.py index aac13cbb7..32b2f7ec8 100644 --- a/sapl/materia/tests/test_email_templates.py +++ b/sapl/materia/tests/test_email_templates.py @@ -1,6 +1,6 @@ from django.core import mail -from sapl.materia.email_utils import enviar_emails, load_email_templates +from sapl.base.email_utils import enviar_emails, load_email_templates def test_email_template_loading(): diff --git a/sapl/materia/views.py b/sapl/materia/views.py index 9e738dbe9..e3d62c2bf 100644 --- a/sapl/materia/views.py +++ b/sapl/materia/views.py @@ -46,7 +46,7 @@ from sapl.utils import (YES_NO_CHOICES, autor_label, autor_modal, get_mime_type_from_file_extension, montar_row_autor, show_results_filter_set) -from .email_utils import do_envia_email_confirmacao +from sapl.base.email_utils import do_envia_email_confirmacao from .forms import (AcessorioEmLoteFilterSet, AcompanhamentoMateriaForm, AdicionarVariasAutoriasFilterSet, DespachoInicialForm, DocumentoAcessorioForm, EtiquetaPesquisaForm, @@ -65,7 +65,7 @@ from .models import (AcompanhamentoMateria, Anexada, AssuntoMateria, Autoria, RegimeTramitacao, Relatoria, StatusTramitacao, TipoDocumento, TipoFimRelatoria, TipoMateriaLegislativa, TipoProposicao, Tramitacao, UnidadeTramitacao) -from .signals import tramitacao_signal +from sapl.base.signals import tramitacao_signal AssuntoMateriaCrud = CrudAux.build(AssuntoMateria, 'assunto_materia') @@ -481,10 +481,13 @@ class ReceberProposicao(PermissionRequiredForAppCrudMixin, FormView): form = ReceberProposicaoForm(request.POST) if form.is_valid(): - proposicoes = Proposicao.objects.filter( - data_envio__isnull=False, data_recebimento__isnull=True) + try: + # A ultima parte do código deve ser a pk da Proposicao + id = form.cleaned_data["cod_hash"].split("/")[1] + proposicao = Proposicao.objects.get(id=id, + data_envio__isnull=False, + data_recebimento__isnull=True) - for proposicao in proposicoes: if proposicao.texto_articulado.exists(): ta = proposicao.texto_articulado.first() # FIXME hash para textos articulados @@ -492,7 +495,7 @@ class ReceberProposicao(PermissionRequiredForAppCrudMixin, FormView): else: hasher = gerar_hash_arquivo( proposicao.texto_original.path, - str(proposicao.pk)) \ + str(proposicao.id)) \ if proposicao.texto_original else None if hasher == form.cleaned_data['cod_hash']: return HttpResponseRedirect( @@ -500,8 +503,12 @@ class ReceberProposicao(PermissionRequiredForAppCrudMixin, FormView): kwargs={ 'hash': hasher.split('/')[0][1:], 'pk': proposicao.pk})) - - messages.error(request, _('Proposição não encontrada!')) + except ObjectDoesNotExist: + messages.error(request, _('Proposição não encontrada!')) + except IndexError: + messages.error(request, _('Código de recibo mal formado!')) + except IOError: + messages.error(request, _('Erro abrindo texto original de proposição')) return self.form_invalid(form) def get_success_url(self): @@ -1114,7 +1121,7 @@ class TramitacaoCrud(MasterDetailCrud): msg = _('Tramitação criada, mas e-mail de acompanhamento ' 'de matéria não enviado. Há problemas na configuração ' 'do e-mail.') - messages.add_message(self.request, messages.ERROR, msg) + messages.add_message(self.request, messages.WARNING, msg) return HttpResponseRedirect(self.get_success_url()) return super().form_valid(form) @@ -1141,7 +1148,7 @@ class TramitacaoCrud(MasterDetailCrud): msg = _('Tramitação atualizada, mas e-mail de acompanhamento ' 'de matéria não enviado. Há problemas na configuração ' 'do e-mail.') - messages.add_message(self.request, messages.ERROR, msg) + messages.add_message(self.request, messages.WARNING, msg) return HttpResponseRedirect(self.get_success_url()) return super().form_valid(form) @@ -1683,6 +1690,7 @@ class AcompanhamentoMateriaView(CreateView): do_envia_email_confirmacao(base_url, casa, + "materia", materia, destinatario) @@ -1876,9 +1884,9 @@ class PrimeiraTramitacaoEmLoteView(PermissionRequiredMixin, FilterView): flag_error = True if flag_error: msg = _('Tramitação criada, mas e-mail de acompanhamento ' - 'de matéria não enviado. Há problemas na configuração ' - 'do e-mail.') - messages.add_message(self.request, messages.ERROR, msg) + 'de matéria não enviado. A não configuração do servidor de e-mail ' + 'impede o envio de aviso de tramitação') + messages.add_message(self.request, messages.WARNING, msg) status = StatusTramitacao.objects.get(id=request.POST['status']) diff --git a/sapl/norma/forms.py b/sapl/norma/forms.py index 5fa26f0fd..818d6ba38 100644 --- a/sapl/norma/forms.py +++ b/sapl/norma/forms.py @@ -5,17 +5,18 @@ from crispy_forms.layout import Fieldset, Layout from django import forms from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.db import models -from django.forms import ModelForm, widgets +from django.forms import ModelForm, widgets, ModelChoiceField from django.utils import timezone from django.utils.translation import ugettext_lazy as _ +from sapl.base.models import Autor, TipoAutor from sapl.crispy_layout_mixin import form_actions, to_row from sapl.materia.models import MateriaLegislativa, TipoMateriaLegislativa from sapl.settings import MAX_DOC_UPLOAD_SIZE from sapl.utils import NormaPesquisaOrderingFilter, RANGE_ANOS, RangeWidgetOverride from .models import (AnexoNormaJuridica, AssuntoNorma, NormaJuridica, NormaRelacionada, - TipoNormaJuridica) + TipoNormaJuridica, AutoriaNorma) def ANO_CHOICES(): @@ -191,6 +192,51 @@ class NormaJuridicaForm(ModelForm): return norma +class AutoriaNormaForm(ModelForm): + + tipo_autor = ModelChoiceField(label=_('Tipo Autor'), + required=False, + queryset=TipoAutor.objects.all(), + empty_label=_('Selecione'),) + + data_relativa = forms.DateField( + widget=forms.HiddenInput(), required=False) + + def __init__(self, *args, **kwargs): + super(AutoriaNormaForm, self).__init__(*args, **kwargs) + + row1 = to_row([('tipo_autor', 4), + ('autor', 4), + ('primeiro_autor', 4)]) + + self.helper = FormHelper() + self.helper.layout = Layout( + Fieldset(_('Autoria'), + row1, 'data_relativa', form_actions(label='Salvar'))) + + if not kwargs['instance']: + self.fields['autor'].choices = [] + + class Meta: + model = AutoriaNorma + fields = ['tipo_autor', 'autor', 'primeiro_autor', 'data_relativa'] + + def clean(self): + cd = super(AutoriaNormaForm, self).clean() + + if not self.is_valid(): + return self.cleaned_data + + autorias = AutoriaNorma.objects.filter( + norma=self.instance.norma, autor=cd['autor']) + pk = self.instance.pk + + if ((not pk and autorias.exists()) or + (pk and autorias.exclude(pk=pk).exists())): + raise ValidationError(_('Esse Autor já foi cadastrado.')) + + return cd + class AnexoNormaJuridicaForm(ModelForm): class Meta: model = AnexoNormaJuridica diff --git a/sapl/norma/migrations/0014_auto_20181008_1655.py b/sapl/norma/migrations/0014_auto_20181008_1655.py new file mode 100644 index 000000000..da9bf4add --- /dev/null +++ b/sapl/norma/migrations/0014_auto_20181008_1655.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.13 on 2018-10-08 19:55 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0021_appconfig_esfera_federacao'), + ('norma', '0013_anexonormajuridica_assunto_anexo'), + ] + + operations = [ + migrations.CreateModel( + name='AutoriaNorma', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('primeiro_autor', models.BooleanField(choices=[(True, 'Sim'), (False, 'Não')], default=False, verbose_name='Primeiro Autor')), + ('autor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='base.Autor', verbose_name='Autor')), + ('norma', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='norma.NormaJuridica', verbose_name='Matéria Legislativa')), + ], + options={ + 'ordering': ('-primeiro_autor', 'autor__nome'), + 'verbose_name': 'Autoria', + 'verbose_name_plural': 'Autorias', + }, + ), + migrations.AddField( + model_name='normajuridica', + name='autores', + field=models.ManyToManyField(through='norma.AutoriaNorma', to='base.Autor'), + ), + migrations.AlterUniqueTogether( + name='autorianorma', + unique_together=set([('autor', 'norma')]), + ), + ] diff --git a/sapl/norma/models.py b/sapl/norma/models.py index e4e3677b3..46f34a0ef 100644 --- a/sapl/norma/models.py +++ b/sapl/norma/models.py @@ -5,6 +5,7 @@ from django.utils.translation import ugettext_lazy as _ from model_utils import Choices import reversion +from sapl.base.models import Autor from sapl.compilacao.models import TextoArticulado from sapl.materia.models import MateriaLegislativa from sapl.utils import (RANGE_ANOS, YES_NO_CHOICES, @@ -129,6 +130,12 @@ class NormaJuridica(models.Model): auto_now=True, verbose_name=_('Data')) + autores = models.ManyToManyField( + Autor, + through='AutoriaNorma', + through_fields=('norma', 'autor'), + symmetrical=False) + class Meta: verbose_name = _('Norma Jurídica') verbose_name_plural = _('Normas Jurídicas') @@ -184,6 +191,28 @@ class NormaJuridica(models.Model): update_fields=update_fields) +@reversion.register() +class AutoriaNorma(models.Model): + autor = models.ForeignKey(Autor, + verbose_name=_('Autor'), + on_delete=models.CASCADE) + norma = models.ForeignKey( + NormaJuridica, on_delete=models.CASCADE, + verbose_name=_('Matéria Legislativa')) + primeiro_autor = models.BooleanField(verbose_name=_('Primeiro Autor'), + choices=YES_NO_CHOICES, + default=False) + + class Meta: + verbose_name = _('Autoria') + verbose_name_plural = _('Autorias') + unique_together = (('autor', 'norma'), ) + ordering = ('-primeiro_autor', 'autor__nome') + + def __str__(self): + return _('Autoria: %(autor)s - %(norma)s') % { + 'autor': self.autor, 'norma': self.norma} + @reversion.register() class LegislacaoCitada(models.Model): materia = models.ForeignKey(MateriaLegislativa, on_delete=models.CASCADE) diff --git a/sapl/norma/urls.py b/sapl/norma/urls.py index d943f71e8..800707915 100644 --- a/sapl/norma/urls.py +++ b/sapl/norma/urls.py @@ -3,7 +3,7 @@ from django.conf.urls import include, url from sapl.norma.views import (AnexoNormaJuridicaCrud,AssuntoNormaCrud, NormaCrud, NormaPesquisaView, NormaRelacionadaCrud, NormaTaView, TipoNormaCrud, TipoVinculoNormaJuridicaCrud, recuperar_norma, - recuperar_numero_norma) + recuperar_numero_norma, AutoriaNormaCrud) from .apps import AppConfig @@ -13,7 +13,8 @@ app_name = AppConfig.name urlpatterns = [ url(r'^norma/', include(NormaCrud.get_urls() + NormaRelacionadaCrud.get_urls() + - AnexoNormaJuridicaCrud.get_urls())), + AnexoNormaJuridicaCrud.get_urls() + + AutoriaNormaCrud.get_urls())), # Integração com Compilação url(r'^norma/(?P[0-9]+)/ta$', NormaTaView.as_view(), name='norma_ta'), diff --git a/sapl/norma/views.py b/sapl/norma/views.py index beae10d4d..b91aec2a5 100644 --- a/sapl/norma/views.py +++ b/sapl/norma/views.py @@ -21,9 +21,9 @@ from sapl.crud.base import (RP_DETAIL, RP_LIST, Crud, CrudAux, from sapl.utils import show_results_filter_set from .forms import (AnexoNormaJuridicaForm, NormaFilterSet, NormaJuridicaForm, - NormaPesquisaSimplesForm, NormaRelacionadaForm) + NormaPesquisaSimplesForm, NormaRelacionadaForm, AutoriaNormaForm) from .models import (AnexoNormaJuridica, AssuntoNorma, NormaJuridica, NormaRelacionada, - TipoNormaJuridica, TipoVinculoNormaJuridica) + TipoNormaJuridica, TipoVinculoNormaJuridica, AutoriaNorma) # LegislacaoCitadaCrud = Crud.build(LegislacaoCitada, '') @@ -274,6 +274,39 @@ def recuperar_numero_norma(request): return response +class AutoriaNormaCrud(MasterDetailCrud): + model = AutoriaNorma + parent_field = 'norma' + help_topic = 'despacho_autoria' + public = [RP_LIST, RP_DETAIL] + list_field_names = ['autor', 'autor__tipo__descricao', 'primeiro_autor'] + + class LocalBaseMixin(): + form_class = AutoriaNormaForm + + @property + def layout_key(self): + return None + + class CreateView(LocalBaseMixin, MasterDetailCrud.CreateView): + + def get_initial(self): + initial = super().get_initial() + norma = NormaJuridica.objects.get(id=self.kwargs['pk']) + initial['data_relativa'] = norma.data + initial['autor'] = [] + return initial + + class UpdateView(LocalBaseMixin, MasterDetailCrud.UpdateView): + + def get_initial(self): + initial = super().get_initial() + initial.update({ + 'data_relativa': self.object.norma.data_apresentacao, + 'tipo_autor': self.object.autor.tipo.id, + }) + return initial + class ImpressosView(PermissionRequiredMixin, TemplateView): template_name = 'materia/impressos/impressos.html' permission_required = ('materia.can_access_impressos', ) diff --git a/sapl/protocoloadm/apps.py b/sapl/protocoloadm/apps.py index 8697e58d9..98e28ea36 100644 --- a/sapl/protocoloadm/apps.py +++ b/sapl/protocoloadm/apps.py @@ -6,3 +6,6 @@ class AppConfig(apps.AppConfig): name = 'sapl.protocoloadm' label = 'protocoloadm' verbose_name = _('Protocolo Administrativo') + + def ready(self): + from sapl.base import receivers diff --git a/sapl/protocoloadm/forms.py b/sapl/protocoloadm/forms.py index 9985f522f..8c441ad93 100644 --- a/sapl/protocoloadm/forms.py +++ b/sapl/protocoloadm/forms.py @@ -2,7 +2,7 @@ import django_filters from crispy_forms.bootstrap import InlineRadios from crispy_forms.helper import FormHelper -from crispy_forms.layout import HTML, Button, Fieldset, Layout +from crispy_forms.layout import HTML, Button, Column, Fieldset, Layout from django import forms from django.core.exceptions import (MultipleObjectsReturned, ObjectDoesNotExist, ValidationError) @@ -19,7 +19,8 @@ from sapl.materia.models import (MateriaLegislativa, TipoMateriaLegislativa, from sapl.utils import (RANGE_ANOS, YES_NO_CHOICES, AnoNumeroOrderingFilter, RangeWidgetOverride, autor_label, autor_modal) -from .models import (DocumentoAcessorioAdministrativo, DocumentoAdministrativo, +from .models import (AcompanhamentoDocumento, DocumentoAcessorioAdministrativo, + DocumentoAdministrativo, Protocolo, TipoDocumentoAdministrativo, TramitacaoAdministrativo) @@ -39,6 +40,28 @@ EM_TRAMITACAO = [('', '---------'), (0, 'Sim'), (1, 'Não')] +class AcompanhamentoDocumentoForm(ModelForm): + + class Meta: + model = AcompanhamentoDocumento + fields = ['email'] + + def __init__(self, *args, **kwargs): + + row1 = to_row([('email', 10)]) + + row1.append( + Column(form_actions(label='Cadastrar'), css_class='col-md-2') + ) + + self.helper = FormHelper() + self.helper.layout = Layout( + Fieldset( + _('Acompanhamento de Documento por e-mail'), row1 + ) + ) + super(AcompanhamentoDocumentoForm, self).__init__(*args, **kwargs) + class ProtocoloFilterSet(django_filters.FilterSet): diff --git a/sapl/protocoloadm/migrations/0008_acompanhamentodocumento.py b/sapl/protocoloadm/migrations/0008_acompanhamentodocumento.py new file mode 100644 index 000000000..dcc25ed45 --- /dev/null +++ b/sapl/protocoloadm/migrations/0008_acompanhamentodocumento.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.13 on 2018-09-27 15:24 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('protocoloadm', '0007_auto_20180924_1724'), + ] + + operations = [ + migrations.CreateModel( + name='AcompanhamentoDocumento', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('usuario', models.CharField(max_length=50)), + ('email', models.EmailField(max_length=100, verbose_name='E-mail')), + ('data_cadastro', models.DateField(auto_now_add=True)), + ('hash', models.CharField(max_length=8)), + ('confirmado', models.BooleanField(default=False)), + ('documento', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='protocoloadm.DocumentoAdministrativo')), + ], + options={ + 'verbose_name_plural': 'Acompanhamentos de Documento', + 'verbose_name': 'Acompanhamento de Documento', + }, + ), + ] diff --git a/sapl/protocoloadm/migrations/0008_auto_20181009_1741.py b/sapl/protocoloadm/migrations/0008_auto_20181009_1741.py new file mode 100644 index 000000000..0ea354a29 --- /dev/null +++ b/sapl/protocoloadm/migrations/0008_auto_20181009_1741.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.13 on 2018-10-09 20:41 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('protocoloadm', '0007_auto_20180924_1724'), + ] + + operations = [ + migrations.AlterField( + model_name='protocolo', + name='interessado', + field=models.CharField(blank=True, max_length=200, verbose_name='Interessado'), + ), + ] diff --git a/sapl/protocoloadm/migrations/0009_merge.py b/sapl/protocoloadm/migrations/0009_merge.py new file mode 100644 index 000000000..b59fca714 --- /dev/null +++ b/sapl/protocoloadm/migrations/0009_merge.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.13 on 2018-10-10 11:10 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('protocoloadm', '0008_acompanhamentodocumento'), + ('protocoloadm', '0008_auto_20181009_1741'), + ] + + operations = [ + ] diff --git a/sapl/protocoloadm/models.py b/sapl/protocoloadm/models.py index 52bd9ad22..aa4e7c0c2 100644 --- a/sapl/protocoloadm/models.py +++ b/sapl/protocoloadm/models.py @@ -67,7 +67,7 @@ class Protocolo(models.Model): blank=True, null=True, verbose_name=_('Tipo de Protocolo')) tipo_processo = models.PositiveIntegerField() interessado = models.CharField( - max_length=60, blank=True, verbose_name=_('Interessado')) + max_length=200, blank=True, verbose_name=_('Interessado')) autor = models.ForeignKey(Autor, blank=True, null=True, @@ -298,3 +298,30 @@ class TramitacaoAdministrativo(models.Model): return _('%(documento)s - %(status)s') % { 'documento': self.documento, 'status': self.status } + +@reversion.register() +class AcompanhamentoDocumento(models.Model): + usuario = models.CharField(max_length=50) + documento = models.ForeignKey(DocumentoAdministrativo, on_delete=models.CASCADE) + email = models.EmailField( + max_length=100, verbose_name=_('E-mail')) + data_cadastro = models.DateField(auto_now_add=True) + hash = models.CharField(max_length=8) + confirmado = models.BooleanField(default=False) + + class Meta: + verbose_name = _('Acompanhamento de Documento') + verbose_name_plural = _('Acompanhamentos de Documento') + + def __str__(self): + if self.data_cadastro is None: + return _('%(documento)s - %(email)s') % { + 'documento': self.documento, + 'email': self.email + } + else: + return _('%(documento)s - %(email)s - Registrado em: %(data)s') % { + 'documento': self.documento, + 'email': self.email, + 'data': str(self.data_cadastro.strftime('%d/%m/%Y')) + } diff --git a/sapl/protocoloadm/tests/test_docadm_email_templates.py b/sapl/protocoloadm/tests/test_docadm_email_templates.py new file mode 100644 index 000000000..ec79098fa --- /dev/null +++ b/sapl/protocoloadm/tests/test_docadm_email_templates.py @@ -0,0 +1,65 @@ +from django.core import mail + +from sapl.base.email_utils import enviar_emails, load_email_templates + + +def test_email_template_loading(): + expected = "Hello Django" + emails = load_email_templates(['email/test_tramitacao.html'], + context={"name": "Django"}) + + # strip \n and \r to compare with expected + actual = emails[0].replace('\n', '').replace('\r', '') + + assert actual == expected + + +def test_html_email_body_with_materia(): + templates = load_email_templates(['email/tramitacao.txt', + 'email/tramitacao.html'], + {"image": 'img/logo.png', + "casa_legislativa": + "Assembléia Parlamentar", + "data_registro": "25/02/2016", + "cod_materia": "1", + "descricao_materia": "Assunto de teste", + "data": "25/02/2016", + "status": "Arquivado", + "texto_acao": "Deliberado", + "hash_txt": "abc01f", + "materia_id": "794", + "base_url": "http://localhost:8000", + "materia_url": + "/docadm/764/acompanhar-documento", + "excluir_url": + "/docadm/764/acompanhar-excluir"}) + + assert len(templates) == 2 + + +def test_enviar_email_distintos(): + NUM_MESSAGES = 10 + messages = [{'recipient': 'user-' + str(i) + '@test.com', + 'subject': 'subject: ' + str(i), + 'txt_message': 'txt: ' + str(i), + 'html_message': '', + } for i in range(NUM_MESSAGES)] + + recipients = [m['recipient'] for m in messages] + + enviar_emails('test@sapl.com', recipients, messages) + assert len(mail.outbox) == NUM_MESSAGES + + +def test_enviar_same_email(): + NUM_MESSAGES = 10 + messages = [{'recipient': 'user-' + str(i) + '@test.com', + 'subject': 'subject: ' + str(i), + 'txt_message': 'txt: ' + str(i), + 'html_message': '', + } for i in range(NUM_MESSAGES)] + + recipients = [m['recipient'] for m in messages] + + enviar_emails('test@sapl.com', recipients, [messages[0]]) + assert len(mail.outbox) == 1 diff --git a/sapl/protocoloadm/urls.py b/sapl/protocoloadm/urls.py index 8ed9216f2..67d5edb65 100644 --- a/sapl/protocoloadm/urls.py +++ b/sapl/protocoloadm/urls.py @@ -1,6 +1,9 @@ from django.conf.urls import include, url -from sapl.protocoloadm.views import (AnularProtocoloAdmView, +from sapl.protocoloadm.views import (AcompanhamentoDocumentoView, + AcompanhamentoConfirmarView, + AcompanhamentoExcluirView, + AnularProtocoloAdmView, ComprovanteProtocoloView, CriarDocumentoProtocolo, DocumentoAcessorioAdministrativoCrud, @@ -56,6 +59,15 @@ urlpatterns_protocolo = [ url(r'^protocoloadm/(?P\d+)/protocolo-mostrar$', ProtocoloMostrarView.as_view(), name='protocolo_mostrar'), + url(r'^docadm/(?P\d+)/acompanhar-documento/$', + AcompanhamentoDocumentoView.as_view(), name='acompanhar_documento'), + url(r'^docadm/(?P\d+)/acompanhar-confirmar$', + AcompanhamentoConfirmarView.as_view(), + name='acompanhar_confirmar'), + url(r'^docadm/(?P\d+)/acompanhar-excluir$', + AcompanhamentoExcluirView.as_view(), + name='acompanhar_excluir'), + url(r'^protocoloadm/(?P\d+)/continuar$', diff --git a/sapl/protocoloadm/views.py b/sapl/protocoloadm/views.py index d58b1e11b..84280796f 100644 --- a/sapl/protocoloadm/views.py +++ b/sapl/protocoloadm/views.py @@ -1,3 +1,6 @@ +from datetime import datetime +from random import choice +from string import ascii_letters, digits from braces.views import FormValidMessageMixin from django.contrib import messages @@ -18,25 +21,30 @@ from django.views.generic.edit import FormView from django_filters.views import FilterView import sapl -from sapl.base.models import Autor +from sapl.base.models import Autor, CasaLegislativa from sapl.comissoes.models import Comissao from sapl.crud.base import Crud, CrudAux, MasterDetailCrud, make_pagination from sapl.materia.models import MateriaLegislativa, TipoMateriaLegislativa from sapl.parlamentares.models import Legislatura, Parlamentar from sapl.protocoloadm.models import Protocolo -from sapl.utils import (create_barcode, get_client_ip, +from sapl.utils import (create_barcode, get_base_url, get_client_ip, get_mime_type_from_file_extension, show_results_filter_set) - -from .forms import (AnularProcoloAdmForm, DocumentoAcessorioAdministrativoForm, +from sapl.base.email_utils import do_envia_email_confirmacao +from .forms import (AcompanhamentoDocumentoForm, AnularProcoloAdmForm, + DocumentoAcessorioAdministrativoForm, DocumentoAdministrativoFilterSet, DocumentoAdministrativoForm, ProtocoloDocumentForm, ProtocoloFilterSet, ProtocoloMateriaForm, - TramitacaoAdmEditForm, TramitacaoAdmForm, DesvincularDocumentoForm, DesvincularMateriaForm, - filtra_tramitacao_adm_destino_and_status, filtra_tramitacao_adm_destino, filtra_tramitacao_adm_status) -from .models import (DocumentoAcessorioAdministrativo, DocumentoAdministrativo, - StatusTramitacaoAdministrativo, + TramitacaoAdmEditForm, TramitacaoAdmForm, + DesvincularDocumentoForm, DesvincularMateriaForm, + filtra_tramitacao_adm_destino_and_status, + filtra_tramitacao_adm_destino, filtra_tramitacao_adm_status) +from .models import (AcompanhamentoDocumento, DocumentoAcessorioAdministrativo, + DocumentoAdministrativo, StatusTramitacaoAdministrativo, TipoDocumentoAdministrativo, TramitacaoAdministrativo) +from sapl.base.signals import tramitacao_signal + TipoDocumentoAdministrativoCrud = CrudAux.build( TipoDocumentoAdministrativo, '') @@ -89,6 +97,136 @@ def doc_texto_integral(request, pk): return response raise Http404 +class AcompanhamentoConfirmarView(TemplateView): + + def get_redirect_url(self, email): + msg = _('Este documento está sendo acompanhado pelo e-mail: %s') % ( + email) + messages.add_message(self.request, messages.SUCCESS, msg) + return reverse('sapl.protocoloadm:documentoadministrativo_detail', + kwargs={'pk': self.kwargs['pk']}) + + def get(self, request, *args, **kwargs): + documento_id = kwargs['pk'] + hash_txt = request.GET.get('hash_txt', '') + + try: + acompanhar = AcompanhamentoDocumento.objects.get( + documento_id=documento_id, + hash=hash_txt) + except ObjectDoesNotExist: + raise Http404() + # except MultipleObjectsReturned: + # A melhor solução deve ser permitir que a exceção + # (MultipleObjectsReturned) seja lançada e vá para o log, + # pois só poderá ser causada por um erro de desenvolvimente + + acompanhar.confirmado = True + acompanhar.save() + + return HttpResponseRedirect(self.get_redirect_url(acompanhar.email)) + + +class AcompanhamentoExcluirView(TemplateView): + + def get_success_url(self): + msg = _('Você parou de acompanhar este Documento.') + messages.add_message(self.request, messages.INFO, msg) + return reverse('sapl.protocoloadm:documentoadministrativo_detail', + kwargs={'pk': self.kwargs['pk']}) + + def get(self, request, *args, **kwargs): + documento_id = kwargs['pk'] + hash_txt = request.GET.get('hash_txt', '') + + try: + AcompanhamentoDocumento.objects.get(documento_id=documento_id, + hash=hash_txt).delete() + except ObjectDoesNotExist: + pass + + return HttpResponseRedirect(self.get_success_url()) + + +class AcompanhamentoDocumentoView(CreateView): + template_name = "protocoloadm/acompanhamento_documento.html" + + def get_random_chars(self): + s = ascii_letters + digits + return ''.join(choice(s) for i in range(choice([6, 7]))) + + def get(self, request, *args, **kwargs): + pk = self.kwargs['pk'] + documento = DocumentoAdministrativo.objects.get(id=pk) + + return self.render_to_response( + {'form': AcompanhamentoDocumentoForm(), + 'documento': documento}) + + def post(self, request, *args, **kwargs): + form = AcompanhamentoDocumentoForm(request.POST) + pk = self.kwargs['pk'] + documento = DocumentoAdministrativo.objects.get(id=pk) + + if form.is_valid(): + email = form.cleaned_data['email'] + usuario = request.user + + hash_txt = self.get_random_chars() + + acompanhar = AcompanhamentoDocumento.objects.get_or_create( + documento=documento, + email=form.data['email']) + + # Se o segundo elemento do retorno do get_or_create for True + # quer dizer que o elemento não existia + if acompanhar[1]: + acompanhar = acompanhar[0] + acompanhar.hash = hash_txt + acompanhar.usuario = usuario.username + acompanhar.confirmado = False + acompanhar.save() + + base_url = get_base_url(request) + + destinatario = AcompanhamentoDocumento.objects.get( + documento=documento, + email=email, + confirmado=False) + casa = CasaLegislativa.objects.first() + + do_envia_email_confirmacao(base_url, + casa, + "documento", + documento, + destinatario) + + msg = _('Foi enviado um e-mail de confirmação. Confira sua caixa \ + de mensagens e clique no link que nós enviamos para \ + confirmar o acompanhamento deste documento.') + messages.add_message(request, messages.SUCCESS, msg) + + # Caso esse Acompanhamento já exista + # avisa ao usuário que esse documento já está sendo acompanhado + else: + msg = _('Este e-mail já está acompanhando esse documento.') + messages.add_message(request, messages.INFO, msg) + + return self.render_to_response( + {'form': form, + 'documento': documento, + 'error': _('Esse documento já está\ + sendo acompanhada por este e-mail.')}) + return HttpResponseRedirect(self.get_success_url()) + else: + return self.render_to_response( + {'form': form, + 'documento': documento}) + + def get_success_url(self): + return reverse('sapl.protocoloadm:documentoadministrativo_detail', + kwargs={'pk': self.kwargs['pk']}) + class DocumentoAdministrativoMixin: @@ -686,8 +824,38 @@ class TramitacaoAdmCrud(MasterDetailCrud): 'unidade_tramitacao_local'].widget.attrs['disabled'] = True return context + def form_valid(self, form): + self.object = form.save() + + try: + tramitacao_signal.send(sender=TramitacaoAdministrativo, + post=self.object, + request=self.request) + except Exception as e: + # TODO log error + msg = _('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') + messages.add_message(self.request, messages.WARNING, msg) + return HttpResponseRedirect(self.get_success_url()) + return super().form_valid(form) + class UpdateView(MasterDetailCrud.UpdateView): form_class = TramitacaoAdmEditForm + def form_valid(self, form): + self.object = form.save() + try: + tramitacao_signal.send(sender=TramitacaoAdministrativo, + post=self.object, + request=self.request) + except Exception as e: + # TODO log error + msg = _('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') + messages.add_message(self.request, messages.WARNING, msg) + return HttpResponseRedirect(self.get_success_url()) + return super().form_valid(form) class ListView(DocumentoAdministrativoMixin, MasterDetailCrud.ListView): diff --git a/sapl/relatorios/templates/pdf_pauta_sessao_gerar.py b/sapl/relatorios/templates/pdf_pauta_sessao_gerar.py index 31636401e..15a4147d6 100755 --- a/sapl/relatorios/templates/pdf_pauta_sessao_gerar.py +++ b/sapl/relatorios/templates/pdf_pauta_sessao_gerar.py @@ -122,8 +122,10 @@ def expediente_materia(lst_expediente_materia): tmp += '\n' tmp += 'MatériaEmentaSituação\n' for expediente_materia in lst_expediente_materia: - tmp += '' + str(expediente_materia['num_ordem']) + ' - ' + expediente_materia[ - 'id_materia'] + '\n' + 'Autor: ' + expediente_materia['nom_autor'] + '\n' + tmp += '' + str(expediente_materia['num_ordem']) + ' - ' + \ + expediente_materia["tipo_materia"] + ' No. ' + \ + expediente_materia['id_materia'] + '\n' + 'Autor: ' + \ + expediente_materia['nom_autor'] + '\n' txt_ementa = expediente_materia['txt_ementa'].replace('&', '&') tmp += '' + txt_ementa + '\n' tmp += '' + \ @@ -145,8 +147,12 @@ def votacao(lst_votacao): tmp += '\n' tmp += 'MatériaEmentaSituação\n' for votacao in lst_votacao: - tmp += '' + str(votacao['num_ordem']) + ' - ' + str(votacao['id_materia']) + '\n' + 'Processo: ' + str(votacao[ - 'des_numeracao']) + '\n' + 'Turno: ' + str(votacao['des_turno']) + '\n' + 'Autor: ' + str(votacao['nom_autor']) + '\n' + tmp += '' + str(votacao['num_ordem']) + ' - ' + \ + votacao["tipo_materia"] + ' No. ' + \ + str(votacao['id_materia']) + '\n' + 'Processo: ' + \ + str(votacao['des_numeracao']) + '\n' + 'Turno: ' + \ + str(votacao['des_turno']) + '\n' + 'Autor: ' + \ + str(votacao['nom_autor']) + '\n' tmp += '' + \ str(votacao['txt_ementa']) + '\n' tmp += '' + \ diff --git a/sapl/relatorios/templates/pdf_sessao_plenaria_gerar.py b/sapl/relatorios/templates/pdf_sessao_plenaria_gerar.py index 1c01bfb08..1f0f6482c 100644 --- a/sapl/relatorios/templates/pdf_sessao_plenaria_gerar.py +++ b/sapl/relatorios/templates/pdf_sessao_plenaria_gerar.py @@ -6,6 +6,9 @@ """ import time +from django.template.defaultfilters import safe +from django.utils.html import strip_tags + from sapl.sessao.models import ResumoOrdenacao from trml2pdf import parseString @@ -284,7 +287,25 @@ def oradores(lst_oradores): return tmp -def principal(cabecalho_dic, rodape_dic, imagem, sessao, inf_basicas_dic, lst_mesa, lst_presenca_sessao, lst_expedientes, lst_expediente_materia, lst_oradores_expediente, lst_presenca_ordem_dia, lst_votacao, lst_oradores): +def ocorrencias(lst_ocorrencias): + """ + + """ + tmp = '' + tmp += '\t\tOcorrências da Sessão\n' + tmp += '\t\t\n' + tmp += '\t\t\t \n' + tmp += '\t\t\n' + for idx, ocorrencia in enumerate(lst_ocorrencias): + tmp += '\t\t' + \ + str(ocorrencia.conteudo) + '\n' + tmp += '\t\t\n' + tmp += '\t\t\t \n' + tmp += '\t\t\n' + return tmp + + +def principal(cabecalho_dic, rodape_dic, imagem, sessao, inf_basicas_dic, lst_mesa, lst_presenca_sessao, lst_expedientes, lst_expediente_materia, lst_oradores_expediente, lst_presenca_ordem_dia, lst_votacao, lst_oradores, lst_ocorrencias): """ """ arquivoPdf = str(int(time.time() * 100)) + ".pdf" @@ -316,7 +337,8 @@ def principal(cabecalho_dic, rodape_dic, imagem, sessao, inf_basicas_dic, lst_me 'mat_o_d': votacao(lst_votacao), 'mesa_d': mesa(lst_mesa), 'oradores_exped': oradores_expediente(lst_oradores_expediente), - 'oradores_expli': oradores(lst_oradores) + 'oradores_expli': oradores(lst_oradores), + 'ocorr_sessao': ocorrencias(lst_ocorrencias) } if ordenacao: @@ -330,6 +352,7 @@ def principal(cabecalho_dic, rodape_dic, imagem, sessao, inf_basicas_dic, lst_me tmp += dict_ord_template[ordenacao.oitavo] tmp += dict_ord_template[ordenacao.nono] tmp += dict_ord_template[ordenacao.decimo] + else: tmp += inf_basicas(inf_basicas_dic) tmp += mesa(lst_mesa) @@ -340,6 +363,7 @@ def principal(cabecalho_dic, rodape_dic, imagem, sessao, inf_basicas_dic, lst_me tmp += presenca_ordem_dia(lst_presenca_ordem_dia) tmp += votacao(lst_votacao) tmp += oradores(lst_oradores) + tmp += ocorrencias(lst_ocorrencias) tmp += '\t\n' tmp += '\n' diff --git a/sapl/relatorios/views.py b/sapl/relatorios/views.py index 4d273b6da..cfbf784eb 100644 --- a/sapl/relatorios/views.py +++ b/sapl/relatorios/views.py @@ -17,7 +17,7 @@ from sapl.protocoloadm.models import (DocumentoAdministrativo, Protocolo, from sapl.sessao.models import (ExpedienteMateria, ExpedienteSessao, IntegranteMesa, Orador, OradorExpediente, OrdemDia, PresencaOrdemDia, SessaoPlenaria, - SessaoPlenariaPresenca) + SessaoPlenariaPresenca, OcorrenciaSessao) from sapl.settings import STATIC_ROOT from sapl.utils import LISTA_DE_UFS, ExtraiTag, TrocaTag, filiacao_data @@ -514,13 +514,13 @@ def get_sessao_plenaria(sessao, casa): dic_presenca['sgl_partido'] = partido_sigla lst_presenca_sessao.append(dic_presenca) + # Exibe os Expedientes lst_expedientes = [] expedientes = ExpedienteSessao.objects.filter( sessao_plenaria=sessao).order_by('tipo__nome') for e in expedientes: - dic_expedientes = {} dic_expedientes["nom_expediente"] = e.tipo.nome conteudo = e.conteudo @@ -539,6 +539,7 @@ def get_sessao_plenaria(sessao, casa): if dic_expedientes: lst_expedientes.append(dic_expedientes) + # Lista das matérias do Expediente, incluindo o resultado das votacoes lst_expediente_materia = [] for expediente_materia in ExpedienteMateria.objects.filter( @@ -727,6 +728,28 @@ def get_sessao_plenaria(sessao, casa): dic_oradores['sgl_partido'] = sigla lst_oradores.append(dic_oradores) + # Ocorrências da Sessão + lst_ocorrencias = [] + ocorrencias = OcorrenciaSessao.objects.filter( + sessao_plenaria=sessao) + + for o in ocorrencias: + conteudo = o.conteudo + + # unescape HTML codes + # https://github.com/interlegis/sapl/issues/1046 + conteudo = re.sub('style=".*?"', '', conteudo) + conteudo = html.unescape(conteudo) + + # escape special character '&' + # https://github.com/interlegis/sapl/issues/1009 + conteudo = conteudo.replace('&', '&') + + o.conteudo = conteudo + + lst_ocorrencias.append(o) + + return (inf_basicas_dic, lst_mesa, lst_presenca_sessao, @@ -735,7 +758,8 @@ def get_sessao_plenaria(sessao, casa): lst_oradores_expediente, lst_presenca_ordem_dia, lst_votacao, - lst_oradores) + lst_oradores, + lst_ocorrencias) def get_turno(dic, materia, sessao_data_inicio): @@ -785,7 +809,9 @@ def relatorio_sessao_plenaria(request, pk): lst_oradores_expediente, lst_presenca_ordem_dia, lst_votacao, - lst_oradores) = get_sessao_plenaria(sessao, casa) + lst_oradores, + lst_ocorrencias) = get_sessao_plenaria(sessao, casa) + for idx in range(len(lst_expedientes)): txt_expedientes = lst_expedientes[idx]['txt_expediente'] @@ -806,7 +832,8 @@ def relatorio_sessao_plenaria(request, pk): lst_oradores_expediente, lst_presenca_ordem_dia, lst_votacao, - lst_oradores) + lst_oradores, + lst_ocorrencias) response.write(pdf) return response @@ -1034,6 +1061,7 @@ def get_pauta_sessao(sessao, casa): id=expediente_materia.materia.id).first() dic_expediente_materia = {} + dic_expediente_materia["tipo_materia"] = materia.tipo.sigla + ' - ' + materia.tipo.descricao dic_expediente_materia["num_ordem"] = str( expediente_materia.numero_ordem) dic_expediente_materia["id_materia"] = str( @@ -1086,6 +1114,7 @@ def get_pauta_sessao(sessao, casa): id=votacao.materia.id).first() dic_votacao = {} + dic_votacao["tipo_materia"] = materia.tipo.sigla + ' - ' + materia.tipo.descricao dic_votacao["num_ordem"] = votacao.numero_ordem dic_votacao["id_materia"] = str( materia.numero) + "/" + str(materia.ano) diff --git a/sapl/rules/map_rules.py b/sapl/rules/map_rules.py index 61c354962..68f9cf325 100644 --- a/sapl/rules/map_rules.py +++ b/sapl/rules/map_rules.py @@ -1,23 +1,3 @@ -from sapl.base import models as base -from sapl.comissoes import models as comissoes -from sapl.compilacao import models as compilacao -from sapl.lexml import models as lexml -from sapl.materia import models as materia -from sapl.norma import models as norma -from sapl.painel import models as painel -from sapl.parlamentares import models as parlamentares -from sapl.protocoloadm import models as protocoloadm -from sapl.audiencia import models as audiencia -from sapl.rules import (RP_ADD, RP_CHANGE, RP_DELETE, RP_DETAIL, RP_LIST, - SAPL_GROUP_ADMINISTRATIVO, SAPL_GROUP_ANONYMOUS, - SAPL_GROUP_AUTOR, SAPL_GROUP_COMISSOES, - SAPL_GROUP_GERAL, SAPL_GROUP_LOGIN_SOCIAL, - SAPL_GROUP_MATERIA, SAPL_GROUP_NORMA, - SAPL_GROUP_PAINEL, SAPL_GROUP_PARLAMENTAR, - SAPL_GROUP_PROTOCOLO, SAPL_GROUP_SESSAO, - SAPL_GROUP_VOTANTE) -from sapl.sessao import models as sessao - """ Todas as permissões do django framework seguem o padrão @@ -46,6 +26,26 @@ negócio trabalham com os cinco radiais de permissão e com qualquer outro tipo de permissão customizada, nesta ordem de precedência. """ +from sapl.audiencia import models as audiencia +from sapl.base import models as base +from sapl.comissoes import models as comissoes +from sapl.compilacao import models as compilacao +from sapl.lexml import models as lexml +from sapl.materia import models as materia +from sapl.norma import models as norma +from sapl.painel import models as painel +from sapl.parlamentares import models as parlamentares +from sapl.protocoloadm import models as protocoloadm +from sapl.rules import (RP_ADD, RP_CHANGE, RP_DELETE, RP_DETAIL, RP_LIST, + SAPL_GROUP_ADMINISTRATIVO, SAPL_GROUP_ANONYMOUS, + SAPL_GROUP_AUTOR, SAPL_GROUP_COMISSOES, + SAPL_GROUP_GERAL, SAPL_GROUP_LOGIN_SOCIAL, + SAPL_GROUP_MATERIA, SAPL_GROUP_NORMA, + SAPL_GROUP_PAINEL, SAPL_GROUP_PARLAMENTAR, + SAPL_GROUP_PROTOCOLO, SAPL_GROUP_SESSAO, + SAPL_GROUP_VOTANTE) +from sapl.sessao import models as sessao + __base__ = [RP_LIST, RP_DETAIL, RP_ADD, RP_CHANGE, RP_DELETE] __listdetailchange__ = [RP_LIST, RP_DETAIL, RP_CHANGE] @@ -116,6 +116,7 @@ rules_group_materia = { (materia.Numeracao, __base__), (materia.Tramitacao, __base__), (norma.LegislacaoCitada, __base__), + (norma.AutoriaNorma, __base__), (compilacao.Dispositivo, __base__ + [ 'change_dispositivo_edicao_dinamica', @@ -136,6 +137,7 @@ rules_group_norma = { (norma.NormaJuridica, __base__), (norma.NormaRelacionada, __base__), (norma.AnexoNormaJuridica, __base__), + (norma.AutoriaNorma, __base__), # Publicacao está com permissão apenas para norma e não para matéria # e proposições apenas por análise do contexto, não é uma limitação @@ -159,6 +161,7 @@ rules_group_sessao = { (sessao.SessaoPlenaria, __base__), (sessao.SessaoPlenariaPresenca, __base__), (sessao.ExpedienteMateria, __base__), + (sessao.OcorrenciaSessao, __base__), (sessao.IntegranteMesa, __base__), (sessao.ExpedienteSessao, __base__), (sessao.Orador, __base__), @@ -302,6 +305,7 @@ rules_group_anonymous = { 'group': SAPL_GROUP_ANONYMOUS, 'rules': [ (materia.AcompanhamentoMateria, [RP_ADD, RP_DELETE]), + (protocoloadm.AcompanhamentoDocumento, [RP_ADD, RP_DELETE]), ] } diff --git a/sapl/rules/tests/test_rules.py b/sapl/rules/tests/test_rules.py index 07302bae5..e1ed7f7e4 100644 --- a/sapl/rules/tests/test_rules.py +++ b/sapl/rules/tests/test_rules.py @@ -11,6 +11,7 @@ from sapl.compilacao.models import (PerfilEstruturalTextoArticulado, TipoDispositivo, TipoDispositivoRelationship) from sapl.materia.models import AcompanhamentoMateria +from sapl.protocoloadm.models import AcompanhamentoDocumento from sapl.rules import SAPL_GROUPS, map_rules from sapl.test_urls import create_perms_post_migrate from scripts.lista_permissions_in_decorators import \ @@ -61,6 +62,7 @@ __fp__in__test_permission_of_models_in_rules_patterns = { PerfilEstruturalTextoArticulado], map_rules.RP_CHANGE: [AcompanhamentoMateria, + AcompanhamentoDocumento, TipoDispositivo, TipoDispositivoRelationship, PerfilEstruturalTextoArticulado], @@ -71,11 +73,13 @@ __fp__in__test_permission_of_models_in_rules_patterns = { PerfilEstruturalTextoArticulado], map_rules.RP_LIST: [AcompanhamentoMateria, + AcompanhamentoDocumento, TipoDispositivo, TipoDispositivoRelationship, PerfilEstruturalTextoArticulado], map_rules.RP_DETAIL: [AcompanhamentoMateria, + AcompanhamentoDocumento, TipoDispositivo, TipoDispositivoRelationship, PerfilEstruturalTextoArticulado] diff --git a/sapl/sessao/forms.py b/sapl/sessao/forms.py index b00063777..830905adf 100644 --- a/sapl/sessao/forms.py +++ b/sapl/sessao/forms.py @@ -20,9 +20,11 @@ from sapl.utils import (RANGE_DIAS_MES, RANGE_MESES, MateriaPesquisaOrderingFilter, autor_label, autor_modal, timezone) -from .models import (Bancada, Bloco, ExpedienteMateria, JustificativaAusencia, - Orador,OradorExpediente, OrdemDia, SessaoPlenaria, - SessaoPlenariaPresenca, TipoJustificativa, TipoResultadoVotacao) +from .models import (Bancada, Bloco, ExpedienteMateria, JustificativaAusencia, + Orador, OradorExpediente, OrdemDia, SessaoPlenaria, + SessaoPlenariaPresenca, TipoJustificativa, TipoResultadoVotacao, + OcorrenciaSessao) + def recupera_anos(): @@ -54,7 +56,8 @@ ORDENACAO_RESUMO = [('cont_mult', 'Conteúdo Multimídia'), ('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')] + ('oradores_expli', 'Oradores das Explicações Pessoais'), + ('ocorrencia_sessao', 'Ocorrências da Sessão')] class SessaoPlenariaForm(ModelForm): @@ -412,6 +415,12 @@ class MesaForm(forms.Form): class ExpedienteForm(forms.Form): conteudo = forms.CharField(required=False, widget=forms.Textarea) +class OcorrenciaSessaoForm(ModelForm): + class Meta: + model = OcorrenciaSessao + fields = ['conteudo'] + + class VotacaoForm(forms.Form): votos_sim = forms.CharField(label='Sim') diff --git a/sapl/sessao/migrations/0024_ocorrenciasessao.py b/sapl/sessao/migrations/0024_ocorrenciasessao.py new file mode 100644 index 000000000..6c612363f --- /dev/null +++ b/sapl/sessao/migrations/0024_ocorrenciasessao.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2018-09-18 13:44 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('sessao', '0023_auto_20180914_1315'), + ] + + operations = [ + migrations.CreateModel( + name='OcorrenciaSessao', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('conteudo', models.TextField(blank=True, verbose_name='Ocorrências da Sessão Plenária')), + ('sessao_plenaria', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='sessao.SessaoPlenaria')), + ], + options={ + 'verbose_name_plural': 'Ocorrências da Sessão Plenaria', + 'verbose_name': 'Ocorrência da Sessão Plenaria', + }, + ), + ] diff --git a/sapl/sessao/migrations/0025_auto_20180919_1116.py b/sapl/sessao/migrations/0025_auto_20180919_1116.py new file mode 100644 index 000000000..fa4c0a435 --- /dev/null +++ b/sapl/sessao/migrations/0025_auto_20180919_1116.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.13 on 2018-09-19 14:16 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('sessao', '0024_ocorrenciasessao'), + ] + + operations = [ + migrations.AlterField( + model_name='ocorrenciasessao', + name='sessao_plenaria', + field=models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, to='sessao.SessaoPlenaria'), + ), + ] diff --git a/sapl/sessao/migrations/0026_auto_20181016_1944.py b/sapl/sessao/migrations/0026_auto_20181016_1944.py new file mode 100644 index 000000000..97ae02603 --- /dev/null +++ b/sapl/sessao/migrations/0026_auto_20181016_1944.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.13 on 2018-10-16 22:44 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('sessao', '0025_auto_20180919_1116'), + ] + + operations = [ + migrations.AlterField( + model_name='expedientesessao', + name='sessao_plenaria', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sessao.SessaoPlenaria'), + ), + migrations.AlterField( + model_name='integrantemesa', + name='sessao_plenaria', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sessao.SessaoPlenaria'), + ), + migrations.AlterField( + model_name='orador', + name='sessao_plenaria', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sessao.SessaoPlenaria'), + ), + migrations.AlterField( + model_name='oradorexpediente', + name='sessao_plenaria', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sessao.SessaoPlenaria'), + ), + migrations.AlterField( + model_name='presencaordemdia', + name='sessao_plenaria', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sessao.SessaoPlenaria'), + ), + migrations.AlterField( + model_name='registrovotacao', + name='expediente', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='sessao.ExpedienteMateria'), + ), + migrations.AlterField( + model_name='registrovotacao', + name='materia', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='materia.MateriaLegislativa'), + ), + migrations.AlterField( + model_name='registrovotacao', + name='ordem', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='sessao.OrdemDia'), + ), + migrations.AlterField( + model_name='sessaoplenaria', + name='sessao_legislativa', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='parlamentares.SessaoLegislativa', verbose_name='Sessão Legislativa'), + ), + migrations.AlterField( + model_name='sessaoplenariapresenca', + name='sessao_plenaria', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sessao.SessaoPlenaria'), + ), + ] diff --git a/sapl/sessao/models.py b/sapl/sessao/models.py index 9ed8d5fc4..a3864343a 100644 --- a/sapl/sessao/models.py +++ b/sapl/sessao/models.py @@ -121,7 +121,7 @@ class SessaoPlenaria(models.Model): verbose_name=_('Tipo')) sessao_legislativa = models.ForeignKey( SessaoLegislativa, - on_delete=models.PROTECT, + on_delete=models.CASCADE, verbose_name=_('Sessão Legislativa')) legislatura = models.ForeignKey(Legislatura, on_delete=models.PROTECT, @@ -288,7 +288,7 @@ class TipoExpediente(models.Model): @reversion.register() class ExpedienteSessao(models.Model): # ExpedienteSessaoPlenaria sessao_plenaria = models.ForeignKey(SessaoPlenaria, - on_delete=models.PROTECT) + on_delete=models.CASCADE) tipo = models.ForeignKey(TipoExpediente, on_delete=models.PROTECT) conteudo = models.TextField( blank=True, verbose_name=_('Conteúdo do expediente')) @@ -301,10 +301,25 @@ class ExpedienteSessao(models.Model): # ExpedienteSessaoPlenaria return '%s - %s' % (self.tipo, self.sessao_plenaria) +@reversion.register() +class OcorrenciaSessao(models.Model): # OcorrenciaSessaoPlenaria + sessao_plenaria = models.OneToOneField(SessaoPlenaria, + on_delete=models.PROTECT) + conteudo = models.TextField( + blank=True, verbose_name=_('Ocorrências da Sessão Plenária')) + + class Meta: + verbose_name = _('Ocorrência da Sessão Plenaria') + verbose_name_plural = _('Ocorrências da Sessão Plenaria') + + def __str__(self): + return '%s - %s' % (self.sessao_plenaria, self.conteudo) + + @reversion.register() class IntegranteMesa(models.Model): # MesaSessaoPlenaria sessao_plenaria = models.ForeignKey(SessaoPlenaria, - on_delete=models.PROTECT) + on_delete=models.CASCADE) cargo = models.ForeignKey(CargoMesa, on_delete=models.PROTECT) parlamentar = models.ForeignKey(Parlamentar, on_delete=models.PROTECT) @@ -319,7 +334,7 @@ class IntegranteMesa(models.Model): # MesaSessaoPlenaria @reversion.register() class AbstractOrador(models.Model): # Oradores sessao_plenaria = models.ForeignKey(SessaoPlenaria, - on_delete=models.PROTECT) + on_delete=models.CASCADE) parlamentar = models.ForeignKey(Parlamentar, on_delete=models.PROTECT, verbose_name=_('Parlamentar')) @@ -372,7 +387,7 @@ class OrdemDia(AbstractOrdemDia): @reversion.register() class PresencaOrdemDia(models.Model): # OrdemDiaPresenca sessao_plenaria = models.ForeignKey(SessaoPlenaria, - on_delete=models.PROTECT) + on_delete=models.CASCADE) parlamentar = models.ForeignKey(Parlamentar, on_delete=models.PROTECT) class Meta: @@ -412,15 +427,15 @@ class RegistroVotacao(models.Model): TipoResultadoVotacao, on_delete=models.PROTECT, verbose_name=_('Resultado da Votação')) - materia = models.ForeignKey(MateriaLegislativa, on_delete=models.PROTECT) + materia = models.ForeignKey(MateriaLegislativa, on_delete=models.CASCADE) ordem = models.ForeignKey(OrdemDia, blank=True, null=True, - on_delete=models.PROTECT) + on_delete=models.CASCADE) expediente = models.ForeignKey(ExpedienteMateria, blank=True, null=True, - on_delete=models.PROTECT) + on_delete=models.CASCADE) numero_votos_sim = models.PositiveIntegerField(verbose_name=_('Sim')) numero_votos_nao = models.PositiveIntegerField(verbose_name=_('Não')) numero_abstencoes = models.PositiveIntegerField( @@ -461,7 +476,7 @@ class VotoParlamentar(models.Model): # RegistroVotacaoParlamentar ''' votacao = models.ForeignKey(RegistroVotacao, blank=True, - null=True) + null=True,on_delete=models.CASCADE) parlamentar = models.ForeignKey(Parlamentar, on_delete=models.PROTECT) voto = models.CharField(max_length=10) @@ -481,10 +496,10 @@ class VotoParlamentar(models.Model): # RegistroVotacaoParlamentar ordem = models.ForeignKey(OrdemDia, blank=True, - null=True) + null=True, on_delete=models.CASCADE) expediente = models.ForeignKey(ExpedienteMateria, blank=True, - null=True) + null=True, on_delete=models.CASCADE) class Meta: verbose_name = _('Registro de Votação de Parlamentar') @@ -498,7 +513,7 @@ class VotoParlamentar(models.Model): # RegistroVotacaoParlamentar @reversion.register() class SessaoPlenariaPresenca(models.Model): sessao_plenaria = models.ForeignKey(SessaoPlenaria, - on_delete=models.PROTECT) + on_delete=models.CASCADE) parlamentar = models.ForeignKey(Parlamentar, on_delete=models.PROTECT) data_sessao = models.DateField(blank=True, null=True) diff --git a/sapl/sessao/tests/test_sessao.py b/sapl/sessao/tests/test_sessao.py index c1af4fc7e..3ed1e47f6 100644 --- a/sapl/sessao/tests/test_sessao.py +++ b/sapl/sessao/tests/test_sessao.py @@ -5,11 +5,14 @@ from django.utils.translation import ugettext_lazy as _ from model_mommy import mommy from sapl.materia.models import MateriaLegislativa, TipoMateriaLegislativa -from sapl.parlamentares.models import Legislatura, Partido, SessaoLegislativa +from sapl.parlamentares.models import Legislatura, Parlamentar, Partido,SessaoLegislativa from sapl.sessao import forms -from sapl.sessao.models import (ExpedienteMateria, OrdemDia, RegistroVotacao, - SessaoPlenaria, TipoSessaoPlenaria) - +from sapl.sessao.models import (ExpedienteMateria, ExpedienteSessao, + IntegranteMesa, Orador, OrdemDia, + PresencaOrdemDia, RegistroVotacao, + SessaoPlenaria, SessaoPlenariaPresenca, + TipoResultadoVotacao, TipoSessaoPlenaria, + VotoParlamentar) def test_valida_campos_obrigatorios_sessao_plenaria_form(): form = forms.SessaoPlenariaForm(data={}) @@ -170,3 +173,124 @@ def test_registro_votacao_tem_ordem_xor_expediente(): # a validação NÃO funciona quando ambos são preenchidos with pytest.raises(ValidationError): registro_votacao_com(ordem, expediente).full_clean() + +def create_sessao_plenaria(): + legislatura = mommy.make(Legislatura) + sessao = mommy.make(SessaoLegislativa) + tipo = mommy.make(TipoSessaoPlenaria) + return mommy.make(SessaoPlenaria, + legislatura=legislatura, + sessao_legislativa=sessao, + tipo=tipo, + numero=1) + +def create_materia_legislativa(): + tipo_materia = mommy.make(TipoMateriaLegislativa) + return mommy.make(MateriaLegislativa, tipo=tipo_materia) + +@pytest.mark.django_db(transaction=False) +def test_delete_sessao_plenaria_cascade_registro_votacao_ordemdia(): + materia = create_materia_legislativa() + sessao_plenaria = create_sessao_plenaria() + ordem = mommy.make(OrdemDia, + sessao_plenaria=sessao_plenaria, + materia=materia, + tipo_votacao='2') + tipo_resultado_votacao = mommy.make(TipoResultadoVotacao, + nome='ok', + natureza="A") + registro = mommy.make(RegistroVotacao, + tipo_resultado_votacao=tipo_resultado_votacao, + materia=materia, + ordem=ordem) + presenca = mommy.make(PresencaOrdemDia, + sessao_plenaria=sessao_plenaria) + parlamentar = mommy.make(Parlamentar) + voto_parlamentar = mommy.make(VotoParlamentar, + votacao=registro, + parlamentar=parlamentar, + ordem=ordem) + sessao_plenaria.delete() + presenca_filter = PresencaOrdemDia.objects.filter( + sessao_plenaria=sessao_plenaria).exists() + ordem_filter = OrdemDia.objects.filter( + sessao_plenaria=sessao_plenaria).exists() + registro_filter = RegistroVotacao.objects.filter( + tipo_resultado_votacao=tipo_resultado_votacao, + materia=materia, + ordem=ordem).exists() + materia_filter = MateriaLegislativa.objects.filter(id=materia.id).exists() + parlamentar_filter = Parlamentar.objects.exists() + voto_parlamentar_filter = VotoParlamentar.objects.filter( + ordem=ordem).exists() + assert not registro_filter + assert not ordem_filter + assert not presenca_filter + assert not voto_parlamentar_filter + assert materia_filter # Não exclui materia + assert parlamentar_filter # Não exclui Parlamentar + +@pytest.mark.django_db(transaction=False) +def test_delete_sessao_plenaria_cascade_registro_votacao_expediente(): + materia = create_materia_legislativa() + sessao_plenaria = create_sessao_plenaria() + expediente = mommy.make(ExpedienteMateria, + sessao_plenaria=sessao_plenaria, + materia=materia, + tipo_votacao='2') + tipo_resultado_votacao = mommy.make(TipoResultadoVotacao, + nome='ok', + natureza="A") + registro = mommy.make(RegistroVotacao, + tipo_resultado_votacao=tipo_resultado_votacao, + materia=materia, + expediente=expediente) + presenca = mommy.make(SessaoPlenariaPresenca, + sessao_plenaria=sessao_plenaria) + parlamentar = mommy.make(Parlamentar) + voto_parlamentar = mommy.make(VotoParlamentar, + votacao=registro, + parlamentar=parlamentar, + expediente=expediente) + sessao_plenaria.delete() + expediente_filter = ExpedienteMateria.objects.filter( + sessao_plenaria=sessao_plenaria).exists() + registro_filter = RegistroVotacao.objects.filter( + tipo_resultado_votacao=tipo_resultado_votacao, + materia=materia, + expediente=expediente).exists() + presenca_filter = SessaoPlenariaPresenca.objects.filter( + sessao_plenaria=sessao_plenaria).exists() + parlamentar_filter = Parlamentar.objects.exists() + voto_parlamentar_filter = VotoParlamentar.objects.filter( + expediente=expediente).exists() + assert not registro_filter + assert not expediente_filter + assert not presenca_filter + assert not voto_parlamentar_filter + assert parlamentar_filter # Não exclui Parlamentar + + +@pytest.mark.django_db(transaction=False) +def test_delete_sessao_plenaria_cascade_integrante_mesa(): + sessao_plenaria = create_sessao_plenaria() + mesa = mommy.make(IntegranteMesa,sessao_plenaria=sessao_plenaria) + sessao_plenaria.delete() + mesa_filter = IntegranteMesa.objects.filter(sessao_plenaria=sessao_plenaria).exists() + assert not mesa_filter + +@pytest.mark.django_db(transaction=False) +def test_delete_sessao_plenaria_cascade_expedientesessao(): + sessao_plenaria = create_sessao_plenaria() + expediente_sessao = mommy.make(ExpedienteSessao, sessao_plenaria=sessao_plenaria) + sessao_plenaria.delete() + expediente_sessao_filter = ExpedienteSessao.objects.filter(sessao_plenaria=sessao_plenaria).exists() + assert not expediente_sessao_filter + +@pytest.mark.django_db(transaction=False) +def test_delete_sessao_plenaria_cascade_orador(): + sessao_plenaria = create_sessao_plenaria() + expediente_sessao = mommy.make(Orador, sessao_plenaria=sessao_plenaria) + sessao_plenaria.delete() + orador_filter = Orador.objects.filter(sessao_plenaria=sessao_plenaria).exists() + assert not orador_filter diff --git a/sapl/sessao/urls.py b/sapl/sessao/urls.py index 40b8696fa..f2e2fbbcc 100644 --- a/sapl/sessao/urls.py +++ b/sapl/sessao/urls.py @@ -3,9 +3,9 @@ from django.conf.urls import include, url from sapl.sessao.views import (AdicionarVariasMateriasExpediente, AdicionarVariasMateriasOrdemDia, BancadaCrud, BlocoCrud, CargoBancadaCrud, - ExpedienteMateriaCrud, ExpedienteView, - JustificativaAusenciaCrud, MateriaOrdemDiaCrud, MesaView, - OradorCrud, OradorExpedienteCrud, PainelView, + ExpedienteMateriaCrud, ExpedienteView, JustificativaAusenciaCrud, + OcorrenciaSessaoView, MateriaOrdemDiaCrud, MesaView, OradorCrud, + OradorExpedienteCrud, PainelView, PautaSessaoDetailView, PautaSessaoView, PesquisarPautaSessaoView, PesquisarSessaoPlenariaView, @@ -103,6 +103,8 @@ urlpatterns = [ # Subnav sessão url(r'^sessao/(?P\d+)/expediente$', ExpedienteView.as_view(), name='expediente'), + url(r'^sessao/(?P\d+)/ocorrencia_sessao$', + OcorrenciaSessaoView.as_view(), name='ocorrencia_sessao'), url(r'^sessao/(?P\d+)/presenca$', PresencaView.as_view(), name='presenca'), url(r'^sessao/(?P\d+)/painel$', @@ -113,7 +115,7 @@ urlpatterns = [ url(r'^sessao/(?P\d+)/resumo$', ResumoView.as_view(), name='resumo'), url(r'^sessao/(?P\d+)/resumo_ata$', - ResumoAtaView.as_view(), name='resumo_ata'), + ResumoAtaView.as_view(), name='resumo_ata'), url(r'^sessao/pesquisar-sessao$', PesquisarSessaoPlenariaView.as_view(), name='pesquisar_sessao'), url(r'^sessao/(?P\d+)/matordemdia/votnom/(?P\d+)/(?P\d+)$', diff --git a/sapl/sessao/views.py b/sapl/sessao/views.py index 2c3b00437..b9caeac87 100644 --- a/sapl/sessao/views.py +++ b/sapl/sessao/views.py @@ -37,14 +37,14 @@ from sapl.sessao.forms import ExpedienteMateriaForm, OrdemDiaForm from sapl.utils import show_results_filter_set, remover_acentos from .forms import (AdicionarVariasMateriasFilterSet, BancadaForm, BlocoForm, - JustificativaAusenciaForm, ExpedienteForm, ListMateriaForm, MesaForm, - OradorExpedienteForm, OradorForm, PautaSessaoFilterSet, + ExpedienteForm, JustificativaAusenciaForm, OcorrenciaSessaoForm, ListMateriaForm, + MesaForm, OradorExpedienteForm, OradorForm, PautaSessaoFilterSet, PresencaForm, ResumoOrdenacaoForm, SessaoPlenariaFilterSet, SessaoPlenariaForm, VotacaoEditForm, VotacaoForm, VotacaoNominalForm) -from .models import (Bancada, Bloco, CargoBancada, CargoMesa, - ExpedienteMateria, ExpedienteSessao, JustificativaAusencia, - IntegranteMesa, MateriaLegislativa, Orador, OradorExpediente, OrdemDia, +from .models import (Bancada, Bloco, CargoBancada, CargoMesa, ExpedienteMateria, + ExpedienteSessao, JustificativaAusencia, OcorrenciaSessao, IntegranteMesa, + MateriaLegislativa, Orador, OradorExpediente, OrdemDia, PresencaOrdemDia, RegistroVotacao, ResumoOrdenacao, SessaoPlenaria, SessaoPlenariaPresenca, TipoExpediente, TipoJustificativa, TipoResultadoVotacao, TipoSessaoPlenaria, @@ -1177,7 +1177,8 @@ class ResumoOrdenacaoView(PermissionRequiredMixin, FormView): 'setimo': ordenacao.setimo, 'oitavo': ordenacao.oitavo, 'nono': ordenacao.nono, - 'decimo': ordenacao.decimo}) + 'decimo': ordenacao.decimo, + 'decimo_primeiro': ordenacao.decimo_primeiro}) return initial def form_valid(self, form): @@ -1193,6 +1194,7 @@ class ResumoOrdenacaoView(PermissionRequiredMixin, FormView): 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.save() @@ -1282,6 +1284,7 @@ class ResumoView(DetailView): ex = {'tipo': tipo, 'conteudo': conteudo} expedientes.append(ex) context.update({'expedientes': expedientes}) + # ===================================================================== # Matérias Expediente materias = ExpedienteMateria.objects.filter( @@ -1415,6 +1418,12 @@ class ResumoView(DetailView): oradores_explicacoes.append(oradores) context.update({'oradores_explicacoes': oradores_explicacoes}) + # ===================================================================== + # Ocorrẽncias da Sessão + ocorrencias_sessao = OcorrenciaSessao.objects.filter(sessao_plenaria_id=self.object.id) + + context.update({'ocorrencias_da_sessao': ocorrencias_sessao}) + # ===================================================================== # Indica a ordem com a qual o template será renderizado ordenacao = ResumoOrdenacao.objects.first() @@ -1428,7 +1437,8 @@ class ResumoView(DetailView): 'mat_o_d': 'materias_ordem_dia.html', 'mesa_d': 'mesa_diretora.html', 'oradores_exped': 'oradores_expediente.html', - 'oradores_expli': 'oradores_explicacoes.html' + 'oradores_expli': 'oradores_explicacoes.html', + 'ocorr_sessao': 'ocorrencias_da_sessao.html' } if ordenacao: @@ -1454,12 +1464,16 @@ class ResumoView(DetailView): 'setimo_ordenacao': dict_ord_template['oradores_exped'], 'oitavo_ordenacao': dict_ord_template['lista_p_o_d'], 'nono_ordenacao': dict_ord_template['mat_o_d'], - 'decimo_ordenacao': dict_ord_template['oradores_expli']}) + 'decimo_ordenacao': dict_ord_template['oradores_expli'], + 'decimo_primeiro_ordenacao': dict_ord_template['ocorr_sessao']}) return self.render_to_response(context) + + class ResumoAtaView(ResumoView): template_name = 'sessao/resumo_ata.html' + class ExpedienteView(FormMixin, DetailView): template_name = 'sessao/expediente.html' form_class = ExpedienteForm @@ -1539,6 +1553,52 @@ class ExpedienteView(FormMixin, DetailView): return reverse('sapl.sessao:expediente', kwargs={'pk': pk}) + +class OcorrenciaSessaoView(FormMixin, DetailView): + template_name = 'sessao/ocorrencia_sessao.html' + form_class = OcorrenciaSessaoForm + model = SessaoPlenaria + + def delete(self): + OcorrenciaSessao.objects.filter(sessao_plenaria=self.object).delete() + + msg = _('Registro deletado com sucesso') + messages.add_message(self.request, messages.SUCCESS, msg) + + def save(self,form): + conteudo = form.cleaned_data['conteudo'] + + OcorrenciaSessao.objects.filter(sessao_plenaria=self.object).delete() + + ocorrencia = OcorrenciaSessao() + ocorrencia.sessao_plenaria_id = self.object.id + ocorrencia.conteudo = conteudo + ocorrencia.save() + + msg = _('Registro salvo com sucesso') + messages.add_message(self.request, messages.SUCCESS, msg) + + @method_decorator(permission_required('sessao.add_ocorrenciasessao')) + def post(self, request, *args, **kwargs): + self.object = self.get_object() + form = OcorrenciaSessaoForm(request.POST) + + if not form.is_valid(): + return self.form_invalid(form) + + if request.POST.get('delete'): + self.delete() + + elif request.POST.get('save'): + self.save(form) + + return self.form_valid(form) + + def get_success_url(self): + pk = self.kwargs['pk'] + return reverse('sapl.sessao:ocorrencia_sessao', kwargs={'pk': pk}) + + class VotacaoEditView(SessaoPermissionMixin): ''' diff --git a/sapl/templates/404.html b/sapl/templates/404.html new file mode 100644 index 000000000..484a2a8f2 --- /dev/null +++ b/sapl/templates/404.html @@ -0,0 +1,171 @@ +{% load i18n staticfiles sass_tags menus %} +{% load common_tags %} + + + + + + + + + {% block head_title %}{% trans 'SAPL - Sistema de Apoio ao Processo Legislativo' %}{% endblock %} + + {% block head_content %} + + + {# Styles #} + + + + + + + + + {# Scripts #} + {# modernizr must be in head (see http://modernizr.com/docs/#installing) #} + {% endblock %} + + + +
+ + {% if not request|has_iframe %} + {% block navigation %} + + {% endblock navigation %} + + {# Header #} + {% block main_header %} +
+ +
+ {% endblock main_header %} + {% else %} +
+ +
+
+
+
+ {% subnav %} +
+
+
+ {% endif %} +
+ +

{% trans 'Página não encontrada! Erro 404' %}

+
+
+ {% block base_content %} + {% endblock %} + {% if not request|has_iframe %} + {% block footer_container %} + +
+ {% endblock footer_container %} + {% endif %} + + {% block foot_js %} + + + + + + + + + + + + + + + + + + + + + {% block extra_js %}{% endblock %} + + + + {% endblock foot_js %} + + diff --git a/sapl/templates/500.html b/sapl/templates/500.html new file mode 100644 index 000000000..ab1afae4f --- /dev/null +++ b/sapl/templates/500.html @@ -0,0 +1,135 @@ +{% load i18n staticfiles sass_tags menus %} +{% load common_tags %} + + + + + + + + + {% block head_title %}{% trans 'SAPL - Sistema de Apoio ao Processo Legislativo' %}{% endblock %} + + {% block head_content %} + + + {# Styles #} + + + + + + + + + {# Scripts #} + {# modernizr must be in head (see http://modernizr.com/docs/#installing) #} + {% endblock %} + + + +
+ + {% block navigation %} + + {% endblock navigation %} + + {# Header #} + {% block main_header %} +
+
+ +
+ {% block sections_nav %} {% subnav %} {% endblock sections_nav %} +
+
+
+ {% endblock main_header %} + +
+ +

{% trans 'Ocorreu um erro inesperado! Erro 500' %}

+
+
+ {% block base_content %} + {% endblock %} + {% block footer_container %} + +
+ {% endblock footer_container %} + + {% block foot_js %} + + + + + + + + + + + + + + + + + + + + + {% block extra_js %}{% endblock %} + + {% endblock foot_js %} + + diff --git a/sapl/templates/base.html b/sapl/templates/base.html index 1953032a3..58f191e19 100644 --- a/sapl/templates/base.html +++ b/sapl/templates/base.html @@ -1,6 +1,6 @@ + {% load i18n staticfiles sass_tags menus %} {% load common_tags %} - @@ -29,7 +29,7 @@
- + {% if not request|has_iframe %} {% block navigation %}
@@ -252,7 +252,7 @@ diff --git a/sapl/templates/compilacao/dispositivo_form_search_fragment.html b/sapl/templates/compilacao/dispositivo_form_search_fragment.html index 95d91a2cf..2df5aa0de 100644 --- a/sapl/templates/compilacao/dispositivo_form_search_fragment.html +++ b/sapl/templates/compilacao/dispositivo_form_search_fragment.html @@ -1,5 +1,14 @@ {% load i18n compilacao_filters %} +{% for message in messages %} + +{% endfor %} + {% if object_list.count >= 100 %}
{% trans 'Use argumentos para simplificar listagem...' %} diff --git a/sapl/templates/compilacao/messages.html b/sapl/templates/compilacao/messages.html new file mode 100644 index 000000000..1777831c7 --- /dev/null +++ b/sapl/templates/compilacao/messages.html @@ -0,0 +1,10 @@ +{% load i18n compilacao_filters %} + +{% for message in messages %} + +{% endfor %} \ No newline at end of file diff --git a/sapl/templates/crud/detail.html b/sapl/templates/crud/detail.html index 4a1cad276..c1b2c6445 100644 --- a/sapl/templates/crud/detail.html +++ b/sapl/templates/crud/detail.html @@ -65,7 +65,21 @@

{{ column.verbose_name }}

- {% if column.text|url %} + {% if column.text|audio_url %} +
+ +
+ {% elif column.text|video_url %} +
+ +
+ {% elif column.text|url %} {% else %}
{{ column.text|safe|default:"" }}
diff --git a/sapl/templates/email/acompanhar_documento.html b/sapl/templates/email/acompanhar_documento.html new file mode 100644 index 000000000..f90558568 --- /dev/null +++ b/sapl/templates/email/acompanhar_documento.html @@ -0,0 +1,25 @@ +{% load i18n %} +{% load static %} + + +

{{casa_legislativa}} +
+ Sistema de Apoio ao Processo Legislativo +

+ +

Registramos seu pedido para acompanhamento por e-mail do documento administrativo identificado a seguir:

+{{documento}} - {{descricao_documento}}
+{{assunto}}
+ +

+

Para garantia de sua privacidade, solicitamos que ative o recebimento das futuras mensagens clicando no link:

+ +

+ {{base_url}}{{confirmacao_url}}?hash_txt={{hash_txt}} +

+
+
+

Caso não tenha realizado o cadastramento em nosso sistema, favor desconsiderar a presente mensagem
+Esta é uma mensagem automática. Por favor, não responda.

+ + diff --git a/sapl/templates/email/acompanhar_documento.txt b/sapl/templates/email/acompanhar_documento.txt new file mode 100644 index 000000000..eab05d7d1 --- /dev/null +++ b/sapl/templates/email/acompanhar_documento.txt @@ -0,0 +1,16 @@ +{{casa_legislativa}} + +Sistema de Apoio ao Processo Legislativo + +>Registramos seu pedido para acompanhamento por e-mail do documento administrativo identificado a seguir: + +{{base_url}}{{documento_url}} - {{documento}} - {{descricao_documento}} + +{{assunto}} + +Para garantia de sua privacidade, solicitamos que ative o recebimento das futuras mensagens acessando no link: + +{{base_url}}{{url_confirmar}}?hash_txt={{hash_txt}} + +Caso não tenha realizado o cadastramento em nosso sistema, favor desconsiderar a presente mensagem +Esta é uma mensagem automática. Por favor, não responda. diff --git a/sapl/templates/email/tramitacao.html b/sapl/templates/email/tramitacao.html index 4b30a1dc5..362fc563a 100644 --- a/sapl/templates/email/tramitacao.html +++ b/sapl/templates/email/tramitacao.html @@ -13,10 +13,12 @@

{{materia}} - {{descricao_materia}}

+{% if autoria %} Autoria:
{% for autor in autoria %} {{ autor }}
{% endfor %} +{% endif %}

diff --git a/sapl/templates/email/tramitacao.txt b/sapl/templates/email/tramitacao.txt index f0a06687a..bb68a87a0 100644 --- a/sapl/templates/email/tramitacao.txt +++ b/sapl/templates/email/tramitacao.txt @@ -8,12 +8,12 @@ A seguinte matéria, de seu interesse, sofreu Tramitação registrada em {{data_ Matéria: {{materia}} - {{descricao_materia}} {{url_materia}} - +{% if autoria %} Autoria: {% for autor in autoria %} {{ autor }} {% endfor %} - +{% endif %} Data da ação: {{data}} Status: {{status}} diff --git a/sapl/templates/materia/materialegislativa_detail.html b/sapl/templates/materia/materialegislativa_detail.html index 0a5f06259..f2c986fd5 100644 --- a/sapl/templates/materia/materialegislativa_detail.html +++ b/sapl/templates/materia/materialegislativa_detail.html @@ -10,6 +10,21 @@ {% endblock sub_actions %} {% block detail_content %} {{ block.super }} + {% if object.registrovotacao_set.exists %} + Data Votação: + {% for rv in object.registrovotacao_set.all %} + {% if rv.ordem %} + + {{ rv.ordem.sessao_plenaria.data_inicio }} + + {% elif rv.expediente %} + + {{ rv.expediente.sessao_plenaria.data_inicio }} + + {% endif %} +
+ {% endfor %} + {% endif %} {% if object.normajuridica_set.last %}

  Norma Jurídica Relacionada

diff --git a/sapl/templates/norma/autorianorma_form.html b/sapl/templates/norma/autorianorma_form.html new file mode 100644 index 000000000..35338c9de --- /dev/null +++ b/sapl/templates/norma/autorianorma_form.html @@ -0,0 +1,47 @@ +{% extends "crud/form.html" %} +{% load i18n %} +{% load crispy_forms_tags %} +{% load common_tags %} + +{% block extra_js %} + +{% endblock %} \ No newline at end of file diff --git a/sapl/templates/norma/layouts.yaml b/sapl/templates/norma/layouts.yaml index 0075317c1..d881ae186 100644 --- a/sapl/templates/norma/layouts.yaml +++ b/sapl/templates/norma/layouts.yaml @@ -70,3 +70,7 @@ NormaRelacionadaDetail: {% trans 'Norma Relacionada' %}: - norma_relacionada - tipo_vinculo + +AutoriaNorma: + {% trans 'Autoria' %}: + - autor primeiro_autor \ No newline at end of file diff --git a/sapl/templates/norma/subnav.yaml b/sapl/templates/norma/subnav.yaml index 9358787ff..f85433e72 100644 --- a/sapl/templates/norma/subnav.yaml +++ b/sapl/templates/norma/subnav.yaml @@ -7,7 +7,8 @@ check_permission: norma.list_normarelacionada - title: {% trans 'Anexos da Norma' %} url: anexonormajuridica_list - +- title: {% trans 'Autoria' %} + url: autorianorma_list # Opção adicionada para chamar o TextoArticulado da norma. # para integração foram necessárias apenas criar a url norma_ta em urls.py # e a view NormaTaView(IntegracaoTaView) em views.py diff --git a/sapl/templates/protocoloadm/acompanhamento_documento.html b/sapl/templates/protocoloadm/acompanhamento_documento.html new file mode 100644 index 000000000..b6526cc52 --- /dev/null +++ b/sapl/templates/protocoloadm/acompanhamento_documento.html @@ -0,0 +1,21 @@ +{% extends "crud/detail.html" %} +{% load i18n %} +{% load crispy_forms_tags %} +{% block actions %} {% endblock %} +{% block detail_content %} + +

Acompanhamento de Documento

+
+
+
Tipo: {{documento.tipo.sigla}} - {{documento.tipo.descricao}}
+
Número: {{documento.numero}}
+
Ano: {{documento.ano}}
+ +
+
+
Assunto: {{documento.assunto|safe}}
+
+ +{% if error %}
{{ error }}
{% endif %} +{% crispy form %} +{% endblock %} diff --git a/sapl/templates/protocoloadm/documentoadministrativo_filter.html b/sapl/templates/protocoloadm/documentoadministrativo_filter.html index e87962273..6dbd95276 100644 --- a/sapl/templates/protocoloadm/documentoadministrativo_filter.html +++ b/sapl/templates/protocoloadm/documentoadministrativo_filter.html @@ -63,6 +63,9 @@ {% if d.texto_integral %} Texto Integral
{% endif %} + {% if d.tramitacao %} + Acompanhar Documento + {% endif %} diff --git a/sapl/templates/sessao/blocos_ata/ocorrencias_da_sessao.html b/sapl/templates/sessao/blocos_ata/ocorrencias_da_sessao.html new file mode 100644 index 000000000..9b65733f9 --- /dev/null +++ b/sapl/templates/sessao/blocos_ata/ocorrencias_da_sessao.html @@ -0,0 +1,6 @@ +
+

+ Ocorrências da Sessão: + {{object.ocorrenciasessao.conteudo|striptags|safe}} +

+
diff --git a/sapl/templates/sessao/blocos_resumo/ocorrencias_da_sessao.html b/sapl/templates/sessao/blocos_resumo/ocorrencias_da_sessao.html new file mode 100644 index 000000000..c56a1bb69 --- /dev/null +++ b/sapl/templates/sessao/blocos_resumo/ocorrencias_da_sessao.html @@ -0,0 +1,6 @@ +
+ Ocorrências da Sessão +
+

{{object.ocorrenciasessao.conteudo|safe}}

+
+
\ No newline at end of file diff --git a/sapl/templates/sessao/ocorrencia_sessao.html b/sapl/templates/sessao/ocorrencia_sessao.html new file mode 100644 index 000000000..0daa921d8 --- /dev/null +++ b/sapl/templates/sessao/ocorrencia_sessao.html @@ -0,0 +1,32 @@ +{% extends "crud/detail.html" %} +{% load i18n %} +{% load crispy_forms_tags %} +{% load common_tags %} + +{% block actions %}{% endblock %} + +{% block title %}Ocorrências da Sessão ({{ object }}) {% endblock %} + +{% block detail_content %} + {% if perms|get_add_perm:view %} +
+ {% csrf_token %} +
+ +
+ + +
+ {% else %} + {{object.ocorrenciasessao.conteudo|safe}} + {% endif %} +{% endblock detail_content %} + + +{% block extra_js %} + {% if perms|get_add_perm:view %} + + {% endif %} +{% endblock %} \ No newline at end of file diff --git a/sapl/templates/sessao/resumo.html b/sapl/templates/sessao/resumo.html index b5831bdca..c7c2070ce 100644 --- a/sapl/templates/sessao/resumo.html +++ b/sapl/templates/sessao/resumo.html @@ -50,5 +50,7 @@ {% include 'sessao/blocos_resumo/'|add:decimo_ordenacao %}


+ {% include 'sessao/blocos_resumo/'|add:decimo_primeiro_ordenacao %} +


{% endblock detail_content %} diff --git a/sapl/templates/sessao/resumo_ata.html b/sapl/templates/sessao/resumo_ata.html index a842b3f40..e1c1000a9 100644 --- a/sapl/templates/sessao/resumo_ata.html +++ b/sapl/templates/sessao/resumo_ata.html @@ -19,5 +19,6 @@ {% include 'sessao/blocos_ata/'|add:oitavo_ordenacao %} {% include 'sessao/blocos_ata/'|add:nono_ordenacao %} {% include 'sessao/blocos_ata/'|add:decimo_ordenacao %} + {% include 'sessao/blocos_ata/'|add:decimo_primeiro_ordenacao %} {% include 'sessao/blocos_ata/assinaturas.html' %} {% endblock detail_content %} \ No newline at end of file diff --git a/sapl/templates/sessao/subnav.yaml b/sapl/templates/sessao/subnav.yaml index 1bae7030f..40c5903c4 100644 --- a/sapl/templates/sessao/subnav.yaml +++ b/sapl/templates/sessao/subnav.yaml @@ -12,6 +12,8 @@ url: justificativaausencia_list - title: {% trans 'Explicações Pessoais' %} url: orador_list + - title: {% trans 'Ocorrências da Sessão' %} + url: ocorrencia_sessao - title: {% trans 'Expedientes' %} children: diff --git a/setup.py b/setup.py index fac072615..c11a354b1 100644 --- a/setup.py +++ b/setup.py @@ -49,7 +49,7 @@ install_requires = [ ] setup( name='interlegis-sapl', - version='3.1.122', + version='3.1.128', packages=find_packages(), include_package_data=True, license='GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007',