From 2f51d9d8aaf48f2a34b5bd900950f9b1d6d469f6 Mon Sep 17 00:00:00 2001 From: LeandroRoberto Date: Sun, 23 Oct 2016 03:14:39 -0200 Subject: [PATCH] =?UTF-8?q?Ref=20mod=20proposi=C3=A7=C3=B5es=20e=20parte?= =?UTF-8?q?=20da=20incorpora=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit O módulo de proposições está encapsulado em autores com usuário de acesso com perfil de Autor com cadastro de proposições com textos original e eletrônico (este sem controle de acesso ainda), além das funcionalidades de envio, retomada e recibo. Na incorporação a devolução com justificativa está pronta, já a incorporação está realizando o cadastro da matéria ou documento, gerando a anexação e autoria necessária, mas falta ainda gerar o protocolo se assim estiver definido nas configurações gerais. --- sapl/base/forms.py | 3 +- ...fig_proposicao_incorporacao_obrigatoria.py | 20 + .../migrations/0029_auto_20161021_1445.py | 20 + .../migrations/0030_auto_20161021_2017.py | 20 + sapl/base/models.py | 14 +- sapl/compilacao/forms.py | 14 +- sapl/crispy_layout_mixin.py | 18 +- sapl/crud/base.py | 3 +- sapl/materia/forms.py | 367 +++++++++++++++++- .../migrations/0062_auto_20161021_1424.py | 26 ++ .../migrations/0063_auto_20161021_1445.py | 25 ++ .../migrations/0064_auto_20161022_1405.py | 48 +++ .../migrations/0065_auto_20161022_1411.py | 21 + .../materia/migrations/0066_proposicao_ano.py | 20 + sapl/materia/models.py | 131 +++++-- sapl/materia/urls.py | 3 +- sapl/materia/views.py | 108 +++--- sapl/norma/models.py | 27 +- sapl/protocoloadm/models.py | 23 +- sapl/static/styles/app.scss | 4 +- sapl/templates/base/layouts.yaml | 6 +- sapl/templates/crud/detail.html | 4 +- sapl/templates/crud/detail_detail.html | 2 +- .../materia/confirmar_proposicao.html | 43 +- sapl/templates/materia/layouts.yaml | 2 +- .../materia/prop_devolvidas_list.html | 2 - .../materia/prop_pendentes_list.html | 3 - .../materia/prop_recebidas_list.html | 12 +- sapl/templates/materia/proposicao_detail.html | 137 +++++-- .../templates/materia/receber_proposicao.html | 9 - sapl/templates/materia/subnav_prop.html | 6 - sapl/templates/materia/subnav_prop.yaml | 9 + .../materia/tipoproposicao_form.html | 9 +- sapl/utils.py | 33 ++ 34 files changed, 993 insertions(+), 199 deletions(-) create mode 100644 sapl/base/migrations/0028_appconfig_proposicao_incorporacao_obrigatoria.py create mode 100644 sapl/base/migrations/0029_auto_20161021_1445.py create mode 100644 sapl/base/migrations/0030_auto_20161021_2017.py create mode 100644 sapl/materia/migrations/0062_auto_20161021_1424.py create mode 100644 sapl/materia/migrations/0063_auto_20161021_1445.py create mode 100644 sapl/materia/migrations/0064_auto_20161022_1405.py create mode 100644 sapl/materia/migrations/0065_auto_20161022_1411.py create mode 100644 sapl/materia/migrations/0066_proposicao_ano.py delete mode 100644 sapl/templates/materia/receber_proposicao.html delete mode 100644 sapl/templates/materia/subnav_prop.html create mode 100644 sapl/templates/materia/subnav_prop.yaml diff --git a/sapl/base/forms.py b/sapl/base/forms.py index fde5c9393..921647421 100644 --- a/sapl/base/forms.py +++ b/sapl/base/forms.py @@ -676,4 +676,5 @@ class ConfiguracoesAppForm(ModelForm): 'painel_aberto', 'texto_articulado_proposicao', 'texto_articulado_materia', - 'texto_articulado_norma'] + 'texto_articulado_norma', + 'proposicao_incorporacao_obrigatoria'] diff --git a/sapl/base/migrations/0028_appconfig_proposicao_incorporacao_obrigatoria.py b/sapl/base/migrations/0028_appconfig_proposicao_incorporacao_obrigatoria.py new file mode 100644 index 000000000..131aa99a7 --- /dev/null +++ b/sapl/base/migrations/0028_appconfig_proposicao_incorporacao_obrigatoria.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-10-21 14:24 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0027_auto_20161011_1624'), + ] + + operations = [ + migrations.AddField( + model_name='appconfig', + name='proposicao_incorporacao_obrigatoria', + field=models.BooleanField(choices=[('O', 'Sempre Gerar Protocolo.'), ('C', 'Perguntar se é pra gerar protocolo ao incorporar.'), ('N', 'Nunca Protocolar ao incorporar uma proposição.')], default='O', verbose_name='Regra de incorporação e protocolo'), + ), + ] diff --git a/sapl/base/migrations/0029_auto_20161021_1445.py b/sapl/base/migrations/0029_auto_20161021_1445.py new file mode 100644 index 000000000..4f01852f1 --- /dev/null +++ b/sapl/base/migrations/0029_auto_20161021_1445.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-10-21 14:45 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0028_appconfig_proposicao_incorporacao_obrigatoria'), + ] + + operations = [ + migrations.AlterField( + model_name='appconfig', + name='proposicao_incorporacao_obrigatoria', + field=models.BooleanField(choices=[('O', 'Sempre Gerar Protocolo'), ('C', 'Perguntar se é pra gerar protocolo ao incorporar'), ('N', 'Nunca Protocolar ao incorporar uma proposição')], default='O', verbose_name='Regra de incorporação de proposições e protocolo'), + ), + ] diff --git a/sapl/base/migrations/0030_auto_20161021_2017.py b/sapl/base/migrations/0030_auto_20161021_2017.py new file mode 100644 index 000000000..1aa346a40 --- /dev/null +++ b/sapl/base/migrations/0030_auto_20161021_2017.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-10-21 20:17 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0029_auto_20161021_1445'), + ] + + operations = [ + migrations.AlterField( + model_name='appconfig', + name='proposicao_incorporacao_obrigatoria', + field=models.CharField(choices=[('O', 'Sempre Gerar Protocolo'), ('C', 'Perguntar se é pra gerar protocolo ao incorporar'), ('N', 'Nunca Protocolar ao incorporar uma proposição')], default='O', max_length=1, verbose_name='Regra de incorporação de proposições e protocolo'), + ), + ] diff --git a/sapl/base/models.py b/sapl/base/models.py index b1db9e0a6..28c8233b0 100644 --- a/sapl/base/models.py +++ b/sapl/base/models.py @@ -7,11 +7,12 @@ from django.contrib.contenttypes.models import ContentType from django.core import exceptions from django.db import models, router from django.db.utils import DEFAULT_DB_ALIAS -from django.utils.translation import ugettext_lazy as _ from django.utils.translation import string_concat +from django.utils.translation import ugettext_lazy as _ from sapl.utils import UF, YES_NO_CHOICES, get_settings_auth_user_model + TIPO_DOCUMENTO_ADMINISTRATIVO = (('O', _('Ostensivo')), ('R', _('Restritivo'))) @@ -84,6 +85,13 @@ class ProblemaMigracao(models.Model): class AppConfig(models.Model): + + POLITICA_PROTOCOLO_CHOICES = ( + ('O', _('Sempre Gerar Protocolo')), + ('C', _('Perguntar se é pra gerar protocolo ao incorporar')), + ('N', _('Nunca Protocolar ao incorporar uma proposição')), + ) + documentos_administrativos = models.CharField( max_length=1, verbose_name=_('Ostensivo/Restritivo'), @@ -110,6 +118,10 @@ class AppConfig(models.Model): verbose_name=_('Usar Textos Articulados para Normas'), choices=YES_NO_CHOICES, default=True) + proposicao_incorporacao_obrigatoria = models.CharField( + verbose_name=_('Regra de incorporação de proposições e protocolo'), + max_length=1, choices=POLITICA_PROTOCOLO_CHOICES, default='O') + class Meta: verbose_name = _('Configurações da Aplicação') verbose_name_plural = _('Configurações da Aplicação') diff --git a/sapl/compilacao/forms.py b/sapl/compilacao/forms.py index 1d7ff00da..b5b7cc09c 100644 --- a/sapl/compilacao/forms.py +++ b/sapl/compilacao/forms.py @@ -631,9 +631,9 @@ class DispositivoEdicaoBasicaForm(ModelForm): self.helper = FormHelper() if not editor_type: - label_cancel = _('Ir para o Editor Sequencial') + cancel_label = _('Ir para o Editor Sequencial') self.helper.layout = SaplFormLayout( - *layout, label_cancel=label_cancel) + *layout, cancel_label=cancel_label) elif editor_type == "get_form_base": getattr(self, "actions_" + editor_type)( @@ -644,11 +644,11 @@ class DispositivoEdicaoBasicaForm(ModelForm): def actions_get_form_base(self, layout, inst, texto_articulado_do_editor=None): - label_cancel = _('Fechar') + cancel_label = _('Fechar') more = [ HTML('%s' % - label_cancel), + cancel_label), ] btns_excluir = [] @@ -860,7 +860,7 @@ class DispositivoEdicaoVigenciaForm(ModelForm): self.helper = FormHelper() self.helper.layout = SaplFormLayout( *layout, - label_cancel=_('Ir para o Editor Sequencial')) + cancel_label=_('Ir para o Editor Sequencial')) super(DispositivoEdicaoVigenciaForm, self).__init__(*args, **kwargs) @@ -951,7 +951,7 @@ class DispositivoDefinidorVigenciaForm(Form): self.helper = FormHelper() self.helper.layout = SaplFormLayout( *layout, - label_cancel=_('Ir para o Editor Sequencial')) + cancel_label=_('Ir para o Editor Sequencial')) pk = kwargs.pop('pk') super(DispositivoDefinidorVigenciaForm, self).__init__(*args, **kwargs) @@ -1090,7 +1090,7 @@ class DispositivoEdicaoAlteracaoForm(ModelForm): self.helper = FormHelper() self.helper.layout = SaplFormLayout( *layout, - label_cancel=_('Ir para o Editor Sequencial')) + cancel_label=_('Ir para o Editor Sequencial')) super(DispositivoEdicaoAlteracaoForm, self).__init__(*args, **kwargs) diff --git a/sapl/crispy_layout_mixin.py b/sapl/crispy_layout_mixin.py index bcf686bbc..51b386d9c 100644 --- a/sapl/crispy_layout_mixin.py +++ b/sapl/crispy_layout_mixin.py @@ -40,11 +40,19 @@ def form_actions(more=[], save_label=_('Salvar')): class SaplFormLayout(Layout): - def __init__(self, *fields, label_cancel=_('Cancelar')): - buttons = form_actions(more=[ - HTML('%s' % label_cancel)]) - _fields = list(to_fieldsets(fields)) + [to_row([(buttons, 12)])] + def __init__(self, *fields, cancel_label=_('Cancelar'), + save_label=_('Salvar'), actions=None): + + buttons = actions + if not buttons: + buttons = form_actions(save_label=save_label, more=[ + HTML('%s' % cancel_label) + if cancel_label else None]) + + _fields = list(to_fieldsets(fields)) + if buttons: + _fields += [to_row([(buttons, 12)])] super(SaplFormLayout, self).__init__(*_fields) diff --git a/sapl/crud/base.py b/sapl/crud/base.py index 77ec08e4d..37414cd98 100644 --- a/sapl/crud/base.py +++ b/sapl/crud/base.py @@ -636,7 +636,8 @@ class CrudCreateView(PermissionRequiredContainerCrudMixin, if not container_data: raise Exception( _('Não é permitido adicionar um registro ' - 'sem estar em um Container')) + 'sem estar em um Container %s' + ) % container_model._meta.verbose_name) if hasattr(self, 'crud') and\ hasattr(self.crud, 'is_m2m') and self.crud.is_m2m: diff --git a/sapl/materia/forms.py b/sapl/materia/forms.py index bcda7725f..d14f165d2 100644 --- a/sapl/materia/forms.py +++ b/sapl/materia/forms.py @@ -1,13 +1,16 @@ from datetime import datetime +import os -from crispy_forms.bootstrap import Alert, InlineCheckboxes +from crispy_forms.bootstrap import Alert, InlineCheckboxes, FormActions,\ + InlineRadios from crispy_forms.helper import FormHelper from crispy_forms.layout import HTML, Button, Column, Fieldset, Layout, Row,\ - Field + Field, Submit from django import forms from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist, ValidationError +from django.core.files.base import File from django.db import models, transaction from django.db.models import Max from django.forms import ModelForm, widgets @@ -15,19 +18,20 @@ from django.forms.forms import Form from django.utils.translation import ugettext_lazy as _ import django_filters -from sapl import base from sapl.base.models import Autor from sapl.comissoes.models import Comissao from sapl.crispy_layout_mixin import form_actions, to_row, to_column,\ SaplFormLayout -from sapl.materia.models import TipoProposicao +from sapl.materia.models import TipoProposicao, RegimeTramitacao, TipoDocumento from sapl.norma.models import (LegislacaoCitada, NormaJuridica, TipoNormaJuridica) from sapl.parlamentares.models import Parlamentar +from sapl.protocoloadm.models import Protocolo from sapl.settings import MAX_DOC_UPLOAD_SIZE from sapl.utils import (RANGE_ANOS, RangeWidgetOverride, autor_label, autor_modal, models_with_gr_for_model, - ChoiceWithoutValidationField) + ChoiceWithoutValidationField, YES_NO_CHOICES) +import sapl from .models import (AcompanhamentoMateria, Anexada, Autoria, DespachoInicial, DocumentoAcessorio, MateriaLegislativa, @@ -873,26 +877,36 @@ class ProposicaoForm(forms.ModelForm): choices=TIPO_TEXTO_CHOICE, widget=widgets.CheckboxSelectMultiple()) + materia_de_vinculo = forms.ModelChoiceField( + queryset=MateriaLegislativa.objects.all(), + widget=widgets.HiddenInput(), + required=False) + class Meta: model = Proposicao fields = ['tipo', 'descricao', 'texto_original', + 'materia_de_vinculo', 'tipo_materia', 'numero_materia', 'ano_materia', 'tipo_texto'] + widgets = { + 'descricao': widgets.Textarea(attrs={'rows': 4})} + def __init__(self, *args, **kwargs): - self.texto_articulado_proposicao = base.models.AppConfig.attr( + self.texto_articulado_proposicao = sapl.base.models.AppConfig.attr( 'texto_articulado_proposicao') if not self.texto_articulado_proposicao: - self.tipo_texto = None - self.TIPO_TEXTO_CHOICE = None - if 'tipo_texto' in self.Meta.fields: - self.Meta.fields.remove('tipo_texto') + if 'tipo_texto' in self._meta.fields: + self._meta.fields.remove('tipo_texto') + else: + if 'tipo_texto' not in self._meta.fields: + self._meta.fields.append('tipo_texto') fields = [ to_column((Fieldset( @@ -914,8 +928,8 @@ class ProposicaoForm(forms.ModelForm): fields.append( to_column((InlineCheckboxes('tipo_texto'), 5)),) - fields.append( - to_column(('texto_original', 7)),) + fields.append(to_column(( + 'texto_original', 7 if self.texto_articulado_proposicao else 12))) self.helper = FormHelper() self.helper.layout = SaplFormLayout(*fields) @@ -930,6 +944,14 @@ class ProposicaoForm(forms.ModelForm): if self.instance.texto_articulado.exists(): self.fields['tipo_texto'].initial.append('T') + if self.instance.materia_de_vinculo: + self.fields[ + 'tipo_materia'].initial = self.instance.materia_de_vinculo.tipo + self.fields[ + 'numero_materia'].initial = self.instance.materia_de_vinculo.numero + self.fields[ + 'ano_materia'].initial = self.instance.materia_de_vinculo.ano + def clean_texto_original(self): texto_original = self.cleaned_data.get('texto_original', False) if texto_original: @@ -946,14 +968,327 @@ class ProposicaoForm(forms.ModelForm): if tm and am and nm: try: - materia = MateriaLegislativa.objects.get( + materia_de_vinculo = MateriaLegislativa.objects.get( tipo_id=tm, ano=am, numero=nm ) except ObjectDoesNotExist: - msg = _('Matéria Vinculada não existe!') - raise ValidationError(msg) + raise ValidationError(_('Matéria Vinculada não existe!')) else: - cd['materia'] = materia + cd['materia_de_vinculo'] = materia_de_vinculo return cd + + def save(self, commit=True): + + if self.instance.pk: + return super().save(commit) + + self.instance.ano = datetime.now().year + numero__max = Proposicao.objects.filter( + autor=self.instance.autor, + ano=datetime.now().year).aggregate(Max('numero_proposicao')) + numero__max = numero__max['numero_proposicao__max'] + self.instance.numero_proposicao = ( + numero__max + 1) if numero__max else 1 + + self.instance.save() + + return self.instance + + +class ConfirmarProposicaoForm(ProposicaoForm): + + tipo_readonly = forms.CharField( + label=TipoProposicao._meta.verbose_name, + required=False, widget=widgets.TextInput( + attrs={'readonly': 'readonly'})) + + autor_readonly = forms.CharField( + label=Autor._meta.verbose_name, + required=False, widget=widgets.TextInput( + attrs={'readonly': 'readonly'})) + + justificativa_devolucao = forms.CharField( + required=False, widget=widgets.Textarea(attrs={'rows': 5})) + + regime_tramitacao = forms.ModelChoiceField( + required=False, queryset=RegimeTramitacao.objects.all()) + + gerar_protocolo = forms.ChoiceField( + label=_('Gerar Protocolo na incorporação?'), + choices=YES_NO_CHOICES, + widget=widgets.RadioSelect()) + + numero_de_paginas = forms.IntegerField(required=False, + label=_('Número de Páginas'),) + + class Meta: + model = Proposicao + fields = [ + 'data_envio', + 'descricao', + 'justificativa_devolucao', + 'gerar_protocolo', + 'numero_de_paginas' + ] + widgets = { + 'descricao': widgets.Textarea( + attrs={'readonly': 'readonly', 'rows': 4}), + 'data_envio': widgets.DateTimeInput( + attrs={'readonly': 'readonly'}), + + } + + def __init__(self, *args, **kwargs): + + self.proposicao_incorporacao_obrigatoria = \ + sapl.base.models.AppConfig.attr( + 'proposicao_incorporacao_obrigatoria') + + if self.proposicao_incorporacao_obrigatoria != 'C': + self.gerar_protocolo.required = False + if 'gerar_protocolo' in self._meta.fields: + self._meta.fields.remove('gerar_protocolo') + else: + if 'gerar_protocolo' not in self._meta.fields: + self._meta.fields.append('gerar_protocolo') + self.gerar_protocolo.required = False # FIXME True + + if self.proposicao_incorporacao_obrigatoria == 'N': + if 'numero_de_paginas' in self._meta.fields: + self._meta.fields.remove('numero_de_paginas') + else: + if 'numero_de_paginas' not in self._meta.fields: + self._meta.fields.append('numero_de_paginas') + + # esta chamada isola o __init__ de ProposicaoForm + super(ProposicaoForm, self).__init__(*args, **kwargs) + + fields = [ + Fieldset( + _('Dados Básicos'), + to_column(('tipo_readonly', 4)), + to_column(('data_envio', 3)), + to_column(('autor_readonly', 5)), + to_column(('descricao', 12)))] + + fields.append( + Fieldset(_('Vinculado a Matéria Legislativa'), + to_column(('tipo_materia', 3)), + to_column(('numero_materia', 2)), + to_column(('ano_materia', 2)), + to_column( + (Alert(_('O responsável pela incorporação pode ' + 'alterar a anexação. Limpar os campos ' + 'de Vinculação gera um %s independente ' + 'sem anexação se for possível para esta ' + 'Proposição. Não sendo, a rotina de incorporação ' + 'não permitirá estes campos serem vazios.' + ) % self.instance.tipo.conteudo, + css_class="alert-info", + dismiss=False), 5)), + to_column( + (Alert('', + css_class="ementa_materia hidden alert-info", + dismiss=False), 12)))) + + submit_incorporar = Submit('incorporar', _('Incorporar')) + itens_incorporacao = ['regime_tramitacao'] + if self.proposicao_incorporacao_obrigatoria == 'C': + itens_incorporacao.append(InlineRadios('gerar_protocolo')) + + if self.proposicao_incorporacao_obrigatoria != 'N': + itens_incorporacao.append('numero_de_paginas') + + fields.append( + Fieldset( + _('Registro de Incorporação'), + *[to_column((itens, 4)) for itens in itens_incorporacao], + FormActions(submit_incorporar, css_class='pull-right') + )) + + fields.append( + Fieldset( + _('Registro de Devolução'), + 'justificativa_devolucao', + FormActions(Submit( + 'devolver', _('Devolver'), + css_class='btn-danger'), css_class='pull-right') + )) + self.helper = FormHelper() + self.helper.layout = Layout(*fields) + + self.fields['tipo_readonly'].initial = self.instance.tipo.descricao + self.fields['autor_readonly'].initial = str(self.instance.autor) + + if self.instance.materia_de_vinculo: + self.fields[ + 'tipo_materia'].initial = self.instance.materia_de_vinculo.tipo + self.fields[ + 'numero_materia'].initial = self.instance.materia_de_vinculo.numero + self.fields[ + 'ano_materia'].initial = self.instance.materia_de_vinculo.ano + + if self.proposicao_incorporacao_obrigatoria == 'C': + self.fields['gerar_protocolo'].initial = True + + def clean(self): + if 'incorporar' in self.data: + cd = ProposicaoForm.clean(self) + + # FIXME em caso de incorporação validar regime e numero de páginas + + if self.instance.tipo.conteudo.model_class() == TipoDocumento and\ + not cd['materia_de_vinculo']: + + raise ValidationError( + _('Documentos não podem ser incorporados sem definir ' + 'para qual Matéria Legislativa ele se destina.')) + + elif 'devolver' in self.data: + cd = self.cleaned_data + + if 'justificativa_devolucao' not in cd or\ + not cd['justificativa_devolucao']: + # TODO Implementar notificação ao autor por email + raise ValidationError( + _('Adicione uma Justificativa para devolução.')) + else: + raise ValidationError( + _('Dados de Confirmação invalidos.')) + return cd + + def save(self, commit=False): + # TODO Implementar workflow entre protocolo e autores + cd = self.cleaned_data + + if 'devolver' in self.data: + self.instance.data_devolucao = datetime.now() + self.instance.data_recebimento = None + self.instance.data_envio = None + self.instance.save() + return self.instance + + elif 'incorporar' in self.data: + self.instance.justificativa_devolucao = '' + self.instance.data_devolucao = None + self.instance.data_recebimento = datetime.now() + + self.instance.save() + + """ + TipoProposicao possui conteúdo genérico para a modelegam de tipos + relacionados e, a esta modelagem, qual o objeto que está associado. + Porem, cada registro a ser gerado pode possuir uma estrutura diferente, + é os casos básicos já implementados, + TipoDocumento e TipoMateriaLegislativa, que são modelos utilizados + em DocumentoAcessorio e MateriaLegislativa geradas, + por sua vez a partir de uma Proposição. + Portanto para estas duas e para outras implementações que possam surgir + possuindo com matéria prima uma Proposição, dada sua estrutura, + deverá contar também com uma implementação particular aqui no código + abaixo. + """ + + proposicao = self.instance + conteudo_gerado = None + + if self.instance.tipo.conteudo.model_class() == TipoMateriaLegislativa: + numero__max = MateriaLegislativa.objects.filter( + tipo=proposicao.tipo.tipo_conteudo_related, + ano=datetime.now().year).aggregate(Max('numero')) + numero__max = numero__max['numero__max'] + + # dados básicos + materia = MateriaLegislativa() + materia.numero = (numero__max + 1) if numero__max else 1 + materia.tipo = proposicao.tipo.tipo_conteudo_related + materia.ementa = proposicao.descricao + materia.ano = datetime.now().year + materia.data_apresentacao = datetime.now() + materia.em_tramitacao = True + materia.regime_tramitacao = cd['regime_tramitacao'] + materia.texto_original = File( + proposicao.texto_original, + os.path.basename(proposicao.texto_original.path)) + materia.texto_articulo = proposicao.texto_articulado + materia.save() + conteudo_gerado = materia + + # autoria + autoria = Autoria() + autoria.autor = proposicao.autor + autoria.materia = materia + autoria.primeiro_autor = True + autoria.save() + + # Matéria de vinlculo + if proposicao.materia_de_vinculo: + anexada = Anexada() + anexada.materia_principal = proposicao.materia_de_vinculo + anexada.materia_anexada = materia + anexada.data_anexacao = datetime.now() + anexada.save() + + elif self.instance.tipo.conteudo.model_class() == TipoDocumento: + + # dados básicos + doc = DocumentoAcessorio() + doc.materia = proposicao.materia_de_vinculo + """ + FIXME Esta forma de registrar autoria é falha. + Dificilmente o usuário que possui perfil de Autor será o autor + de um Documento Acessório. + Solução pode passar pela parametrização em TipoProposicao que + possibilite abrir ou não espaço, dado o Tipo, para quem está + incorporando a proposição rediga o nome do Autor do Doc Acessório. + + """ + doc.autor = str(proposicao.autor) + doc.tipo = proposicao.tipo.tipo_conteudo_related + + doc.ementa = proposicao.descricao + """ FIXME verificar questão de nome e data de documento, + doc acessório. Possivelmente pode possuir data anterior a + data de envio e/ou recebimento dada a incorporação. + """ + doc.nome = str(proposicao.tipo.tipo_conteudo_related)[:30] + doc.data = proposicao.data_envio + + doc.arquivo = proposicao.texto_original = File( + proposicao.texto_original, + os.path.basename(proposicao.texto_original.path)) + doc.save() + conteudo_gerado = doc + + proposicao.conteudo_gerado_related = conteudo_gerado + proposicao.save() + + # Nunca gerar protocolo + if self.proposicao_incorporacao_obrigatoria == 'N': + return self.instance + + # ocorre se proposicao_incorporacao_obrigatoria == 'C' (condicional) + # and gerar_protocolo == False + if 'gerar_protocolo' in cd or not cd['gerar_protocolo']: + return self.instance + + # resta a opção proposicao_incorporacao_obrigatoria == 'C' + # and gerar_protocolo == True + # ou, proposicao_incorporacao_obrigatoria == 'O' + # que são idênticas. + + """ + apesar de TipoProposicao estar com conteudo e tipo conteudo genérico, + aqui na incorporação de proposições, para gerar protocolo, cada caso + possível de conteudo em tipo de proposição deverá ser tratado + isoladamente justamente por Protocolo não estar generalizado com + GenericForeignKey + """ + + # FIXME - Implementar protocolo + # protocolo = Protocolo() + # protocolo.ano = + + return self.instance diff --git a/sapl/materia/migrations/0062_auto_20161021_1424.py b/sapl/materia/migrations/0062_auto_20161021_1424.py new file mode 100644 index 000000000..c9d8985ea --- /dev/null +++ b/sapl/materia/migrations/0062_auto_20161021_1424.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-10-21 14:24 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('materia', '0061_auto_20161017_1655'), + ] + + operations = [ + migrations.AlterField( + model_name='proposicao', + name='autor', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='base.Autor'), + ), + migrations.AlterField( + model_name='proposicao', + name='materia', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='materia_vinculada', to='materia.MateriaLegislativa', verbose_name='Matéria Vinculada'), + ), + ] diff --git a/sapl/materia/migrations/0063_auto_20161021_1445.py b/sapl/materia/migrations/0063_auto_20161021_1445.py new file mode 100644 index 000000000..4a9a8a0dd --- /dev/null +++ b/sapl/materia/migrations/0063_auto_20161021_1445.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-10-21 14:45 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('materia', '0062_auto_20161021_1424'), + ] + + operations = [ + migrations.RemoveField( + model_name='proposicao', + name='materia', + ), + migrations.AddField( + model_name='proposicao', + name='materia_vinculada', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='proposicao_set', to='materia.MateriaLegislativa', verbose_name='Matéria Vinculada'), + ), + ] diff --git a/sapl/materia/migrations/0064_auto_20161022_1405.py b/sapl/materia/migrations/0064_auto_20161022_1405.py new file mode 100644 index 000000000..e7db0ecd7 --- /dev/null +++ b/sapl/materia/migrations/0064_auto_20161022_1405.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-10-22 14:05 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('materia', '0063_auto_20161021_1445'), + ] + + operations = [ + migrations.AddField( + model_name='proposicao', + name='content_type', + field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType', verbose_name='Tipo de Material Gerado'), + ), + migrations.AddField( + model_name='proposicao', + name='materia_de_vinculo', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='proposicao_set', to='materia.MateriaLegislativa', verbose_name='Matéria anexadora'), + ), + migrations.AddField( + model_name='proposicao', + name='object_id', + field=models.PositiveIntegerField(blank=True, default=None, null=True), + ), + migrations.RemoveField( + model_name='proposicao', + name='documento_gerado', + ), + migrations.RemoveField( + model_name='proposicao', + name='materia_gerada', + ), + migrations.RemoveField( + model_name='proposicao', + name='materia_vinculada', + ), + migrations.AlterUniqueTogether( + name='proposicao', + unique_together=set([('content_type', 'object_id')]), + ), + ] diff --git a/sapl/materia/migrations/0065_auto_20161022_1411.py b/sapl/materia/migrations/0065_auto_20161022_1411.py new file mode 100644 index 000000000..ce3a060df --- /dev/null +++ b/sapl/materia/migrations/0065_auto_20161022_1411.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-10-22 14:11 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('materia', '0064_auto_20161022_1405'), + ] + + operations = [ + migrations.AlterField( + model_name='proposicao', + name='content_type', + field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType', verbose_name='Tipo de Material Gerado'), + ), + ] diff --git a/sapl/materia/migrations/0066_proposicao_ano.py b/sapl/materia/migrations/0066_proposicao_ano.py new file mode 100644 index 000000000..d7152e987 --- /dev/null +++ b/sapl/materia/migrations/0066_proposicao_ano.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-10-22 23:03 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('materia', '0065_auto_20161022_1411'), + ] + + operations = [ + migrations.AddField( + model_name='proposicao', + name='ano', + field=models.PositiveSmallIntegerField(blank=True, choices=[(2016, 2016), (2015, 2015), (2014, 2014), (2013, 2013), (2012, 2012), (2011, 2011), (2010, 2010), (2009, 2009), (2008, 2008), (2007, 2007), (2006, 2006), (2005, 2005), (2004, 2004), (2003, 2003), (2002, 2002), (2001, 2001), (2000, 2000), (1999, 1999), (1998, 1998), (1997, 1997), (1996, 1996), (1995, 1995), (1994, 1994), (1993, 1993), (1992, 1992), (1991, 1991), (1990, 1990), (1989, 1989), (1988, 1988), (1987, 1987), (1986, 1986), (1985, 1985), (1984, 1984), (1983, 1983), (1982, 1982), (1981, 1981), (1980, 1980), (1979, 1979), (1978, 1978), (1977, 1977), (1976, 1976), (1975, 1975), (1974, 1974), (1973, 1973), (1972, 1972), (1971, 1971), (1970, 1970), (1969, 1969), (1968, 1968), (1967, 1967), (1966, 1966), (1965, 1965), (1964, 1964), (1963, 1963), (1962, 1962), (1961, 1961), (1960, 1960), (1959, 1959), (1958, 1958), (1957, 1957), (1956, 1956), (1955, 1955), (1954, 1954), (1953, 1953), (1952, 1952), (1951, 1951), (1950, 1950), (1949, 1949), (1948, 1948), (1947, 1947), (1946, 1946), (1945, 1945), (1944, 1944), (1943, 1943), (1942, 1942), (1941, 1941), (1940, 1940), (1939, 1939), (1938, 1938), (1937, 1937), (1936, 1936), (1935, 1935), (1934, 1934), (1933, 1933), (1932, 1932), (1931, 1931), (1930, 1930), (1929, 1929), (1928, 1928), (1927, 1927), (1926, 1926), (1925, 1925), (1924, 1924), (1923, 1923), (1922, 1922), (1921, 1921), (1920, 1920), (1919, 1919), (1918, 1918), (1917, 1917), (1916, 1916), (1915, 1915), (1914, 1914), (1913, 1913), (1912, 1912), (1911, 1911), (1910, 1910), (1909, 1909), (1908, 1908), (1907, 1907), (1906, 1906), (1905, 1905), (1904, 1904), (1903, 1903), (1902, 1902), (1901, 1901), (1900, 1900), (1899, 1899), (1898, 1898), (1897, 1897), (1896, 1896), (1895, 1895), (1894, 1894), (1893, 1893), (1892, 1892), (1891, 1891), (1890, 1890)], default=None, null=True, verbose_name='Ano'), + ), + ] diff --git a/sapl/materia/models.py b/sapl/materia/models.py index 80342941b..c3f22b59f 100644 --- a/sapl/materia/models.py +++ b/sapl/materia/models.py @@ -1,3 +1,6 @@ +import datetime +import re + from django.contrib.auth.models import Group from django.contrib.contenttypes.fields import GenericForeignKey,\ GenericRelation @@ -12,9 +15,8 @@ from sapl.comissoes.models import Comissao from sapl.compilacao.models import TextoArticulado from sapl.parlamentares.models import Parlamentar from sapl.utils import (RANGE_ANOS, YES_NO_CHOICES, - get_settings_auth_user_model, restringe_tipos_de_arquivo_txt, SaplGenericRelation, - SaplGenericForeignKey) + SaplGenericForeignKey, texto_upload_path) EM_TRAMITACAO = [(1, 'Sim'), @@ -32,6 +34,8 @@ def grupo_autor(): class TipoProposicao(models.Model): descricao = models.CharField(max_length=50, verbose_name=_('Descrição')) + # FIXME - para a rotina de migração - estes campos mudaram + # retire o comentário quando resolver conteudo = models.ForeignKey(ContentType, default=None, verbose_name=_('Definição de Tipo')) object_id = models.PositiveIntegerField( @@ -109,14 +113,6 @@ class Origem(models.Model): return self.nome -def get_materia_media_path(instance, subpath, filename): - return './sapl/materia/%s/%s/%s' % (instance, subpath, filename) - - -def texto_upload_path(instance, filename): - return get_materia_media_path(instance, 'materia', filename) - - TIPO_APRESENTACAO_CHOICES = Choices(('O', 'oral', _('Oral')), ('E', 'escrita', _('Escrita'))) @@ -206,6 +202,23 @@ class MateriaLegislativa(models.Model): return _('%(tipo)s nº %(numero)s de %(ano)s') % { 'tipo': self.tipo, 'numero': self.numero, 'ano': self.ano} + def save(self, force_insert=False, force_update=False, using=None, + update_fields=None): + + if not self.pk and self.texto_original: + texto_original = self.texto_original + self.texto_original = None + models.Model.save(self, force_insert=force_insert, + force_update=force_update, + using=using, + update_fields=update_fields) + self.texto_original = texto_original + + return models.Model.save(self, force_insert=force_insert, + force_update=force_update, + using=using, + update_fields=update_fields) + class Autoria(models.Model): autor = models.ForeignKey(Autor, verbose_name=_('Autor')) @@ -335,6 +348,23 @@ class DocumentoAcessorio(models.Model): 'data': self.data, 'autor': self.autor} + def save(self, force_insert=False, force_update=False, using=None, + update_fields=None): + + if not self.pk and self.arquivo: + arquivo = self.arquivo + self.arquivo = None + models.Model.save(self, force_insert=force_insert, + force_update=force_update, + using=using, + update_fields=update_fields) + self.arquivo = arquivo + + return models.Model.save(self, force_insert=force_insert, + force_update=force_update, + using=using, + update_fields=update_fields) + class MateriaAssunto(models.Model): # TODO M2M ?? @@ -469,8 +499,29 @@ class Proposicao(models.Model): max_length=200, blank=True, verbose_name=_('Justificativa da Devolução')) + + ano = models.PositiveSmallIntegerField(verbose_name=_('Ano'), + default=None, blank=True, null=True, + choices=RANGE_ANOS) + numero_proposicao = models.PositiveIntegerField( blank=True, null=True, verbose_name=_('Número')) + + """ + FIXME Campo não é necessário na modelagem e implementação atual para o + módulo de proposições. + E - Enviada é tratado pela condição do campo data_envio - se None n enviado + se possui uma data, enviada + R - Recebida é uma condição do campo data_recebimento - se None não receb. + se possui uma data, enviada, recebida e incorporada + I - A incorporação é automática ao ser recebida + + e ainda possui a condição de Devolvida onde o campo data_devolucao é + direfente de None, fornecedo a informação para o usuário da data que o + responsável devolveu bem como a justificativa da devolução. + Essa informação fica disponível para o Autor até que ele envie novamente + sua proposição ou resolva excluir. + """ # ind_enviado and ind_devolvido collapsed as char field (status) status = models.CharField(blank=True, max_length=1, @@ -478,19 +529,6 @@ class Proposicao(models.Model): ('R', 'Recebida'), ('I', 'Incorporada')), verbose_name=_('Status Proposição')) - # mutually exclusive (depend on tipo.materia_ou_documento) - materia = models.ForeignKey( - MateriaLegislativa, blank=True, null=True, verbose_name=_('Matéria'), - related_name=_('materia_vinculada')) - - # Ao ser recebida, irá gerar uma nova matéria ou um documento acessorio - # de uma já existente - materia_gerada = models.ForeignKey( - MateriaLegislativa, blank=True, null=True, - related_name=_('materia_gerada')) - documento_gerado = models.ForeignKey( - DocumentoAcessorio, blank=True, null=True) - texto_original = models.FileField( upload_to=texto_upload_path, blank=True, @@ -501,12 +539,57 @@ class Proposicao(models.Model): texto_articulado = GenericRelation( TextoArticulado, related_query_name='texto_articulado') + # FIXME - para a rotina de migração - este campo mudou + # retire o comentário quando resolver + materia_de_vinculo = models.ForeignKey( + MateriaLegislativa, blank=True, null=True, + verbose_name=_('Matéria anexadora'), + related_name=_('proposicao_set')) + + # FIXME - para a rotina de migração - estes campos mudaram + # retire o comentário quando resolver + content_type = models.ForeignKey( + ContentType, default=None, blank=True, null=True, + verbose_name=_('Tipo de Material Gerado')) + object_id = models.PositiveIntegerField( + blank=True, null=True, default=None) + conteudo_gerado_related = SaplGenericForeignKey( + 'content_type', 'object_id', verbose_name=_('Conteúdo Gerado')) + + """# Ao ser recebida, irá gerar uma nova matéria ou um documento acessorio + # de uma já existente + materia_gerada = models.ForeignKey( + MateriaLegislativa, blank=True, null=True, + related_name=_('materia_gerada')) + documento_gerado = models.ForeignKey( + DocumentoAcessorio, blank=True, null=True)""" + class Meta: verbose_name = _('Proposição') verbose_name_plural = _('Proposições') + unique_together = (('content_type', 'object_id'), ) def __str__(self): - return self.descricao + return '%s %s/%s' % (Proposicao._meta.verbose_name, + self.numero_proposicao, + self.ano) + + def save(self, force_insert=False, force_update=False, using=None, + update_fields=None): + + if not self.pk and self.texto_original: + texto_original = self.texto_original + self.texto_original = None + models.Model.save(self, force_insert=force_insert, + force_update=force_update, + using=using, + update_fields=update_fields) + self.texto_original = texto_original + + return models.Model.save(self, force_insert=force_insert, + force_update=force_update, + using=using, + update_fields=update_fields) class StatusTramitacao(models.Model): diff --git a/sapl/materia/urls.py b/sapl/materia/urls.py index 5400978c6..1fde6247d 100644 --- a/sapl/materia/urls.py +++ b/sapl/materia/urls.py @@ -73,7 +73,8 @@ urlpatterns_proposicao = [ name='proposicao-recebida'), url(r'^proposicao/devolvida/', ProposicaoDevolvida.as_view(), name='proposicao-devolvida'), - url(r'^proposicao/confirmar/(?P\d+)', ConfirmarProposicao.as_view(), + url(r'^proposicao/confirmar/P(?P[0-9A-Fa-f]+)/' + '(?P\d+)', ConfirmarProposicao.as_view(), name='proposicao-confirmar'), url(r'^sistema/proposicao/tipo/', include(TipoProposicaoCrud.get_urls())), diff --git a/sapl/materia/views.py b/sapl/materia/views.py index 0c884465f..e903c70f5 100644 --- a/sapl/materia/views.py +++ b/sapl/materia/views.py @@ -33,7 +33,7 @@ from sapl.crud.base import (ACTION_CREATE, ACTION_DELETE, ACTION_DETAIL, make_pagination, PermissionRequiredForAppCrudMixin) from sapl.materia import apps from sapl.materia.forms import AnexadaForm, LegislacaoCitadaForm,\ - TipoProposicaoForm, ProposicaoForm + TipoProposicaoForm, ProposicaoForm, ConfirmarProposicaoForm from sapl.norma.models import LegislacaoCitada from sapl.utils import (TURNO_TRAMITACAO_CHOICES, YES_NO_CHOICES, autor_label, autor_modal, gerar_hash_arquivo, get_base_url, @@ -195,7 +195,7 @@ class ProposicaoDevolvida(PermissionRequiredMixin, ListView): def get_queryset(self): return Proposicao.objects.filter( - data_envio__isnull=False, + data_envio__isnull=True, data_recebimento__isnull=True, data_devolucao__isnull=False) @@ -206,6 +206,7 @@ class ProposicaoDevolvida(PermissionRequiredMixin, ListView): context['page_range'] = make_pagination( page_obj.number, paginator.num_pages) context['NO_ENTRIES_MSG'] = 'Nenhuma proposição devolvida.' + context['subnav_template_name'] = 'materia/subnav_prop.yaml' return context @@ -229,6 +230,8 @@ class ProposicaoPendente(PermissionRequiredMixin, ListView): context['page_range'] = make_pagination( page_obj.number, paginator.num_pages) context['NO_ENTRIES_MSG'] = 'Nenhuma proposição pendente.' + + context['subnav_template_name'] = 'materia/subnav_prop.yaml' return context @@ -252,12 +255,13 @@ class ProposicaoRecebida(PermissionRequiredMixin, ListView): context['page_range'] = make_pagination( page_obj.number, paginator.num_pages) context['NO_ENTRIES_MSG'] = 'Nenhuma proposição recebida.' + context['subnav_template_name'] = 'materia/subnav_prop.yaml' return context class ReceberProposicao(PermissionRequiredForAppCrudMixin, FormView): app_label = sapl.protocoloadm.apps.AppConfig.label - template_name = "materia/receber_proposicao.html" + template_name = "crud/form.html" form_class = ReceberProposicaoForm def post(self, request, *args, **kwargs): @@ -265,15 +269,20 @@ class ReceberProposicao(PermissionRequiredForAppCrudMixin, FormView): form = ReceberProposicaoForm(request.POST) if form.is_valid(): - proposicoes = Proposicao.objects.filter(data_envio__isnull=False) + proposicoes = Proposicao.objects.filter( + data_envio__isnull=False, data_recebimento__isnull=True) for proposicao in proposicoes: - hasher = gerar_hash_arquivo(proposicao.texto_original.path, - str(proposicao.pk)) + # FIXME implementar hash para texto eletrônico + hasher = gerar_hash_arquivo( + proposicao.texto_original.path, + str(proposicao.pk)) if proposicao.texto_original else None if hasher == form.cleaned_data['cod_hash']: return HttpResponseRedirect( reverse('sapl.materia:proposicao-confirmar', - kwargs={'pk': proposicao.pk})) + kwargs={ + 'hash': hasher.split('/')[0][1:], + 'pk': proposicao.pk})) messages.error(request, _('Proposição não encontrada!')) return self.form_invalid(form) @@ -281,43 +290,55 @@ class ReceberProposicao(PermissionRequiredForAppCrudMixin, FormView): def get_success_url(self): return reverse('sapl.materia:receber-proposicao') + def get_context_data(self, **kwargs): + context = super(ReceberProposicao, self).get_context_data(**kwargs) + context['subnav_template_name'] = 'materia/subnav_prop.yaml' + return context + -class ConfirmarProposicao(PermissionRequiredForAppCrudMixin, TemplateView): +class ConfirmarProposicao(PermissionRequiredForAppCrudMixin, UpdateView): app_label = sapl.protocoloadm.apps.AppConfig.label template_name = "materia/confirmar_proposicao.html" + model = Proposicao + form_class = ConfirmarProposicaoForm - def get_context_data(self, **kwargs): - context = super(ConfirmarProposicao, self).get_context_data(**kwargs) - proposicao = Proposicao.objects.get(pk=self.kwargs['pk']) - context.update({'form': self.get_form(), 'proposicao': proposicao}) - return context + def get_success_url(self): + # FIXME redirecionamento trival, + # ainda por implementar se será para protocolo ou para doc resultante - def post(self, request, *args, **kwargs): - form = ConfirmarProposicaoForm(request.POST) - proposicao = Proposicao.objects.get(pk=self.kwargs['pk']) + messages.success(self.request, _('Devolução efetuada com sucesso.')) - if form.is_valid(): - if 'incorporar' in request.POST: - proposicao.data_recebimento = datetime.now() - if proposicao.tipo.descricao == 'Parecer': - documento = criar_doc_proposicao(proposicao) - proposicao.documento_gerado = documento - proposicao.save() - return HttpResponseRedirect( - reverse('sapl.materia:documentoacessorio_update', - kwargs={'pk': documento.pk})) - else: - materia = criar_materia_proposicao(proposicao) - proposicao.materia_gerada = materia - proposicao.save() - return HttpResponseRedirect( - reverse('sapl.materia:materialegislativa_update', - kwargs={'pk': materia.pk})) - else: - proposicao.data_devolucao = datetime.now() - proposicao.save() - return HttpResponseRedirect( - reverse('sapl.materia:proposicao-devolvida')) + return reverse('sapl.materia:receber-proposicao') + + def get_object(self, queryset=None): + try: + """Não deve haver acesso na rotina de confirmação a proposições: + já recebidas -> data_recebimento != None + não enviadas -> data_envio == None + """ + proposicao = Proposicao.objects.get(pk=self.kwargs['pk'], + data_envio__isnull=False, + data_recebimento__isnull=True) + self.object = None + # FIXME implementar hash para texto eletrônico + hasher = gerar_hash_arquivo( + proposicao.texto_original.path, + str(proposicao.pk)) if proposicao.texto_original else None + + if hasher == 'P%s/%s' % (self.kwargs['hash'], proposicao.pk): + self.object = proposicao + except: + raise Http404() + + if not self.object: + raise Http404() + + return self.object + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['subnav_template_name'] = '' + return context class ProposicaoCrud(Crud): @@ -339,9 +360,6 @@ class ProposicaoCrud(Crud): return context def get(self, request, *args, **kwargs): - """if not Proposicao.objects.filter( - pk=kwargs.get('pk'), autor__user=self.autor.user).exists(): - raise Http404()""" if not self._action_is_valid(request, *args, **kwargs): return redirect(reverse('sapl.materia:proposicao_detail', @@ -349,9 +367,6 @@ class ProposicaoCrud(Crud): return super().get(self, request, *args, **kwargs) def post(self, request, *args, **kwargs): - """if not Proposicao.objects.filter( - pk=kwargs.get('pk'), autor__user=self.autor.user).exists(): - raise Http404()""" if not self._action_is_valid(request, *args, **kwargs): return redirect(reverse('sapl.materia:proposicao_detail', @@ -386,6 +401,7 @@ class ProposicaoCrud(Crud): msg_error = _('Proposição não possui nenhum tipo de ' 'Texto associado.') else: + p.data_devolucao = None p.data_envio = datetime.now() p.save() messages.success(request, _( @@ -406,7 +422,9 @@ class ProposicaoCrud(Crud): if msg_error: messages.error(request, msg_error) - return Crud.DetailView.get(self, request, *args, **kwargs) + # retornar redirecionando para limpar a variavel action + return redirect(reverse('sapl.materia:proposicao_detail', + kwargs={'pk': kwargs['pk']})) class DeleteView(BaseLocalMixin, Crud.DeleteView): diff --git a/sapl/norma/models.py b/sapl/norma/models.py index 55f07ad16..f094df55f 100644 --- a/sapl/norma/models.py +++ b/sapl/norma/models.py @@ -6,7 +6,7 @@ from model_utils import Choices from sapl.compilacao.models import TextoArticulado from sapl.materia.models import MateriaLegislativa -from sapl.utils import RANGE_ANOS, YES_NO_CHOICES +from sapl.utils import RANGE_ANOS, YES_NO_CHOICES, texto_upload_path class AssuntoNorma(models.Model): @@ -55,14 +55,6 @@ class TipoNormaJuridica(models.Model): return self.descricao -def get_norma_media_path(instance, subpath, filename): - return './sapl/norma/%s/%s/%s' % (instance, subpath, filename) - - -def texto_upload_path(instance, filename): - return get_norma_media_path(instance, instance.ano, filename) - - class NormaJuridica(models.Model): ESFERA_FEDERACAO_CHOICES = Choices( ('E', 'estadual', _('Estadual')), @@ -124,6 +116,23 @@ class NormaJuridica(models.Model): 'numero': self.numero, 'data': defaultfilters.date(self.data, "d \d\e F \d\e Y")} + def save(self, force_insert=False, force_update=False, using=None, + update_fields=None): + + if not self.pk and self.texto_integral: + texto_integral = self.texto_integral + self.texto_integral = None + models.Model.save(self, force_insert=force_insert, + force_update=force_update, + using=using, + update_fields=update_fields) + self.texto_integral = texto_integral + + return models.Model.save(self, force_insert=force_insert, + force_update=force_update, + using=using, + update_fields=update_fields) + class AssuntoNormaRelationship(models.Model): assunto = models.ForeignKey(AssuntoNorma) diff --git a/sapl/protocoloadm/models.py b/sapl/protocoloadm/models.py index 5f713202b..6c4f0ffe1 100644 --- a/sapl/protocoloadm/models.py +++ b/sapl/protocoloadm/models.py @@ -7,7 +7,7 @@ from model_utils import Choices from sapl.base.models import Autor from sapl.materia.models import (TipoMateriaLegislativa, UnidadeTramitacao) -from sapl.utils import RANGE_ANOS, YES_NO_CHOICES +from sapl.utils import RANGE_ANOS, YES_NO_CHOICES, texto_upload_path class TipoDocumentoAdministrativo(models.Model): @@ -22,8 +22,29 @@ class TipoDocumentoAdministrativo(models.Model): return self.descricao +""" +uuid4 + filenames diversos apesar de tornar url de um arquivo praticamente +impossível de ser localizado não está controlando o acesso. +Exemplo: o SAPL está configurado para ser docs adm restritivo porém +alguem resolve perga o link e mostrar o tal arquivo para um amigo, ou um +vizinho de departamento que não possui acesso... ou mesmo alguem que nem ao +menos está logado... este arquivo estará livre + +outro caso, um funcionário bem intencionado, mas com um computador infectado +que consegue pegar todos os links da página que ele está acessando e esse +funcionário possui permissão para ver arquivos de docs administrativos. +Consequentemente os arquivos se tornarão públicos pois podem ser acessados +via url sem controle de acesso. + +* foi aberta uma issue no github para rever a questão de arquivos privados: +https://github.com/interlegis/sapl/issues/751 + +a solução dela deverá dar o correto tratamento a essa questão. + + def texto_upload_path(instance, filename): return '/'.join([instance._meta.model_name, str(uuid4()), filename]) +""" class DocumentoAdministrativo(models.Model): diff --git a/sapl/static/styles/app.scss b/sapl/static/styles/app.scss index ef959e2b4..2fccae252 100644 --- a/sapl/static/styles/app.scss +++ b/sapl/static/styles/app.scss @@ -180,7 +180,9 @@ p.control-label { border-bottom: 1px solid $legend-border-color; clear: both; } - +.grid-gutter-width-right { + margin-right: $grid-gutter-width / 2; +} // #### footer ########################################### // based on http://getbootstrap.com/examples/sticky-footer diff --git a/sapl/templates/base/layouts.yaml b/sapl/templates/base/layouts.yaml index 85be02341..be1901b0f 100644 --- a/sapl/templates/base/layouts.yaml +++ b/sapl/templates/base/layouts.yaml @@ -12,7 +12,11 @@ CasaLegislativa: AppConfig: {% trans 'Configurações da Aplicação' %}: - - documentos_administrativos sequencia_numeracao painel_aberto + - documentos_administrativos painel_aberto + + {% trans 'Proposições e Protocolo' %}: + - sequencia_numeracao proposicao_incorporacao_obrigatoria + {% trans 'Textos Articulados' %}: - texto_articulado_proposicao texto_articulado_materia texto_articulado_norma diff --git a/sapl/templates/crud/detail.html b/sapl/templates/crud/detail.html index 33618603a..8d824b0f2 100644 --- a/sapl/templates/crud/detail.html +++ b/sapl/templates/crud/detail.html @@ -51,7 +51,7 @@

{{ column.verbose_name }}

- + {% comment %}TODO Transformar os links em URLs diretamente no CRUD{% endcomment %} {% if column.text|url %} {% else %} @@ -87,7 +87,7 @@ {% if href %} {{ value }} - {% elif value != 'core.Cep.None' %} + {% elif 'None' not in value %} {{ value|safe }} {% endif %} diff --git a/sapl/templates/crud/detail_detail.html b/sapl/templates/crud/detail_detail.html index dde6abbd6..219e287df 100644 --- a/sapl/templates/crud/detail_detail.html +++ b/sapl/templates/crud/detail_detail.html @@ -80,7 +80,7 @@ {% if href %} {{ value }} - {% elif valu != 'core.Cep.None' %} + {% elif 'None' in value %} {{ value|safe }} {% endif %} diff --git a/sapl/templates/materia/confirmar_proposicao.html b/sapl/templates/materia/confirmar_proposicao.html index f81ad26dc..99f9647ff 100644 --- a/sapl/templates/materia/confirmar_proposicao.html +++ b/sapl/templates/materia/confirmar_proposicao.html @@ -1,32 +1,21 @@ -{% extends "base.html" %} -{% load i18n crispy_forms_tags %} +{% extends "materia/proposicao_form.html" %} +{% load i18n crispy_forms_tags common_tags %} {% block base_content %} - -
- Confirmar recebimento de Proposição - - - - - -
Tipo: {{proposicao.tipo}}
Autor: {{proposicao.autor}}
Descrição: {{proposicao.descricao}}
Data de Envio: {{proposicao.data_envio|date:'d/m/Y H:i:s'}}
+
+ {% block actions %}{{block.super}} +
+ {% if object.texto_articulado.exists %} + {% trans "Texto Eletrônico da Proposição" %} + {% endif %} + {% if object.texto_original %} + {% trans "Texto Original da Proposição" %} + {% endif %} +
+ {% endblock actions%} +
+ +{{block.super}} -
- {% csrf_token %} -
- -               - -
-
-
{% endblock %} diff --git a/sapl/templates/materia/layouts.yaml b/sapl/templates/materia/layouts.yaml index 6c0a3742b..a5070ef86 100644 --- a/sapl/templates/materia/layouts.yaml +++ b/sapl/templates/materia/layouts.yaml @@ -82,7 +82,7 @@ Proposicao: - tipo data_envio - descricao {% trans 'Materia' %}: - - materia + - materia_de_vinculo {% trans 'Complemento' %}: - texto_original diff --git a/sapl/templates/materia/prop_devolvidas_list.html b/sapl/templates/materia/prop_devolvidas_list.html index 04089432e..153c7d778 100644 --- a/sapl/templates/materia/prop_devolvidas_list.html +++ b/sapl/templates/materia/prop_devolvidas_list.html @@ -1,8 +1,6 @@ {% extends "base.html" %} {% load i18n %} -{% block sections_nav %} {% include 'materia/subnav_prop.html'%} {% endblock sections_nav %} - {% block base_content %}
Proposições Não Incorporadas diff --git a/sapl/templates/materia/prop_pendentes_list.html b/sapl/templates/materia/prop_pendentes_list.html index 825b1fad9..e07ba0ee7 100644 --- a/sapl/templates/materia/prop_pendentes_list.html +++ b/sapl/templates/materia/prop_pendentes_list.html @@ -1,8 +1,5 @@ {% extends "base.html" %} {% load i18n %} - -{% block sections_nav %} {% include 'materia/subnav_prop.html'%} {% endblock sections_nav %} - {% block base_content %}
Proposições Não Recebidas diff --git a/sapl/templates/materia/prop_recebidas_list.html b/sapl/templates/materia/prop_recebidas_list.html index 08ccfe473..4d4ff5ddb 100644 --- a/sapl/templates/materia/prop_recebidas_list.html +++ b/sapl/templates/materia/prop_recebidas_list.html @@ -1,8 +1,5 @@ {% extends "base.html" %} {% load i18n %} - -{% block sections_nav %} {% include 'materia/subnav_prop.html'%} {% endblock sections_nav %} - {% block base_content %}
Proposições Incorporadas @@ -27,10 +24,11 @@ {{ prop.descricao }} {{ prop.autor }} - {% if prop.materia_gerada %} - {{ prop.materia_gerada.tipo.sigla }} {{ prop.materia_gerada.numero }}/{{ prop.materia_gerada.ano }} - {% elif prop.documento_gerado %} - {{ prop.documento_gerado.materia.tipo.sigla }} {{ prop.documento_gerado.materia.numero }}/{{ prop.documento_gerado.materia.ano }} + {{ MateriaLegislativa.Meta}} + {% if prop.content_type.model == 'materialegislativa' %} + {{ prop.conteudo_gerado_related.tipo.sigla }} {{ prop.conteudo_gerado_related.numero }}/{{ prop.conteudo_gerado_related.ano }} + {% elif prop.content_type.model == 'documentoacessorio' %} + {{ prop.conteudo_gerado_related.materia.tipo.sigla }} {{ prop.conteudo_gerado_related.materia.numero }}/{{ prop.conteudo_gerado_related.materia.ano }} {% endif %} diff --git a/sapl/templates/materia/proposicao_detail.html b/sapl/templates/materia/proposicao_detail.html index 852586d83..dd6f68046 100644 --- a/sapl/templates/materia/proposicao_detail.html +++ b/sapl/templates/materia/proposicao_detail.html @@ -1,40 +1,127 @@ {% extends "crud/detail.html" %} -{% load i18n %} -{% load common_tags %} +{% load i18n common_tags %} {% block sub_actions %}{{block.super}} +
+ {% if object.texto_articulado.exists %} + {% trans "Texto Eletrônico" %} + {% endif %} + {% if object.texto_original %} + {% trans "Texto Original" %} + {% endif %} +
{% endblock sub_actions%} {% block editions %} - {% if proposicao.data_envio %} -
- {% if proposicao.texto_articulado.exists %} - {% trans "Texto Eletrônico" %} - {% endif %} - {% if proposicao.texto_original %} - {% trans "Texto Original" %} - {% endif %} -
-
- {% if not object.data_recebimento %} + {% if object.data_envio %} + {% block editions_actions_return %} + + {% if not object.data_recebimento %} + {% trans 'Retornar Proposição Enviada' %} + {% endif %} +
+ {% endblock %} {% else %} - + {% block editions_actions_send %} + - + + {% endblock %} {% endif %} {% endblock editions %} + + +{% block detail_content %} + + +

{% model_verbose_name 'sapl.materia.models.Proposicao' %}

+ +
+ +
+
+

{%field_verbose_name object 'tipo'%}

+
+
{{object.tipo}}
+
+
+
+ {% if object.data_devolucao %} + +
+ +
+ + {% else %} + + {% if object.data_envio %} +
+
+

{%field_verbose_name object 'data_envio'%}

+
+
{{object.data_envio}}
+
+
+
+ {% endif %} + + {% if object.data_recebimento %} +
+
+

{%field_verbose_name object 'data_recebimento'%}

+
+
{{object.data_recebimento}}
+
+
+
+ {% elif object.data_envio %} +
+ +
+ {% endif %} + {% endif %} +
+ +
+
+
+

{%field_verbose_name object 'descricao'%}

+
+
{{object.descricao}}
+
+
+
+
+ + {% if object.materia_de_vinculo %} +

{% trans "Vínculo com a Matéria Legislativa" %}

+
+
+
+
+
{{object.materia_de_vinculo}}
+ +
+
+
+
+ {% endif %} + +{% endblock detail_content %} diff --git a/sapl/templates/materia/receber_proposicao.html b/sapl/templates/materia/receber_proposicao.html deleted file mode 100644 index 888820934..000000000 --- a/sapl/templates/materia/receber_proposicao.html +++ /dev/null @@ -1,9 +0,0 @@ -{% extends "crud/form.html" %} -{% load i18n %} - -{% block sections_nav %} {% include 'materia/subnav_prop.html'%} {% endblock sections_nav %} - -{% load crispy_forms_tags %} -{% block extra_msg %} -

{{msg}}

-{% endblock %} diff --git a/sapl/templates/materia/subnav_prop.html b/sapl/templates/materia/subnav_prop.html deleted file mode 100644 index 0cf82c77e..000000000 --- a/sapl/templates/materia/subnav_prop.html +++ /dev/null @@ -1,6 +0,0 @@ - diff --git a/sapl/templates/materia/subnav_prop.yaml b/sapl/templates/materia/subnav_prop.yaml new file mode 100644 index 000000000..facfe876f --- /dev/null +++ b/sapl/templates/materia/subnav_prop.yaml @@ -0,0 +1,9 @@ +{% load i18n common_tags %} +- title: {% trans 'Receber Proposição' %} + url: receber-proposicao +- title: {% trans 'Proposições Não Recebidas' %} + url: proposicao-pendente +- title: {% trans 'Proposições Não Incorporadas' %} + url: proposicao-devolvida +- title: {% trans 'Proposições Incorporadas' %} + url: proposicao-recebida diff --git a/sapl/templates/materia/tipoproposicao_form.html b/sapl/templates/materia/tipoproposicao_form.html index ab950a878..5168cb29a 100644 --- a/sapl/templates/materia/tipoproposicao_form.html +++ b/sapl/templates/materia/tipoproposicao_form.html @@ -16,16 +16,19 @@ $(document).ready(function(){ $.get(url).done(function(data) { var radios = $("#div_id_tipo_conteudo_related_radio .controls").html(''); data.forEach(function (val, index) { - var html_radio = ''; + var html_radio = '
'; + + if (val === initial_select) + initial_select=''; radios.append(html_radio); }); - initial_select=''; + }); }); $('#id_conteudo').trigger('change'); - + $("#div_id_tipo_conteudo_related_radio .controls").addClass('controls-radio-checkbox'); }); diff --git a/sapl/utils.py b/sapl/utils.py index 928a252e3..bc82fb4f1 100644 --- a/sapl/utils.py +++ b/sapl/utils.py @@ -3,6 +3,7 @@ from functools import wraps from unicodedata import normalize as unicodedata_normalize import hashlib import logging +import re from crispy_forms.helper import FormHelper from crispy_forms.layout import HTML, Button @@ -519,3 +520,35 @@ def generic_relations_for_model(model): x._meta.get_fields(include_hidden=True)))), models_with_gr_for_model(model) )) + + +def texto_upload_path(instance, filename): + """ + O path gerado por essa função leva em conta a pk de instance. + isso não é possível naturalmente em uma inclusão pois a implementação + do django framework chama essa função antes do metodo save + + Por outro lado a forma como vinha sendo formada os paths para os arquivos + são improdutivas e inconsistentes. Exemplo: usava se o valor de __str__ + do model Proposicao que retornava a descrição da proposição, não retorna + mais, para uma pasta formar o path do texto_original. + Ora, o resultado do __str__ citado é totalmente impróprio para ser o nome + de uma pasta. + + Para colocar a pk no path, a solução encontrada foi implementar o método + save nas classes que possuem atributo do tipo FileField, implementação esta + que guarda o FileField em uma variável independente e temporária para savar + o object sem o arquivo e, logo em seguida, salvá-lo novamente com o arquivo + Ou seja, nas inclusões que já acomparem um arquivo, haverá dois saves, + um para armazenar toda a informação e recuperar o pk, e outro logo em + seguida para armazenar o arquivo. + """ + + filename = re.sub('[^a-zA-Z0-9]', '-', filename).strip('-').lower() + filename = re.sub('[-]+', '-', filename) + path = './sapl/%(model_name)s/%(pk)s/%(filename)s' % { + 'model_name': instance._meta.model_name, + 'pk': instance.pk, + 'filename': filename} + + return path