Browse Source

Ref mod proposições e parte da incorporação

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.
pull/752/head
LeandroRoberto 8 years ago
parent
commit
2f51d9d8aa
  1. 3
      sapl/base/forms.py
  2. 20
      sapl/base/migrations/0028_appconfig_proposicao_incorporacao_obrigatoria.py
  3. 20
      sapl/base/migrations/0029_auto_20161021_1445.py
  4. 20
      sapl/base/migrations/0030_auto_20161021_2017.py
  5. 14
      sapl/base/models.py
  6. 14
      sapl/compilacao/forms.py
  7. 16
      sapl/crispy_layout_mixin.py
  8. 3
      sapl/crud/base.py
  9. 367
      sapl/materia/forms.py
  10. 26
      sapl/materia/migrations/0062_auto_20161021_1424.py
  11. 25
      sapl/materia/migrations/0063_auto_20161021_1445.py
  12. 48
      sapl/materia/migrations/0064_auto_20161022_1405.py
  13. 21
      sapl/materia/migrations/0065_auto_20161022_1411.py
  14. 20
      sapl/materia/migrations/0066_proposicao_ano.py
  15. 131
      sapl/materia/models.py
  16. 3
      sapl/materia/urls.py
  17. 108
      sapl/materia/views.py
  18. 27
      sapl/norma/models.py
  19. 23
      sapl/protocoloadm/models.py
  20. 4
      sapl/static/styles/app.scss
  21. 6
      sapl/templates/base/layouts.yaml
  22. 4
      sapl/templates/crud/detail.html
  23. 2
      sapl/templates/crud/detail_detail.html
  24. 43
      sapl/templates/materia/confirmar_proposicao.html
  25. 2
      sapl/templates/materia/layouts.yaml
  26. 2
      sapl/templates/materia/prop_devolvidas_list.html
  27. 3
      sapl/templates/materia/prop_pendentes_list.html
  28. 12
      sapl/templates/materia/prop_recebidas_list.html
  29. 115
      sapl/templates/materia/proposicao_detail.html
  30. 9
      sapl/templates/materia/receber_proposicao.html
  31. 6
      sapl/templates/materia/subnav_prop.html
  32. 9
      sapl/templates/materia/subnav_prop.yaml
  33. 9
      sapl/templates/materia/tipoproposicao_form.html
  34. 33
      sapl/utils.py

3
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']

20
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'),
),
]

20
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'),
),
]

20
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'),
),
]

14
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')

14
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('<a class="btn btn-inverse btn-fechar">%s</a>' %
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)

16
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=[
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('<a href="{{ view.cancel_url }}"'
' class="btn btn-inverse">%s</a>' % label_cancel)])
_fields = list(to_fieldsets(fields)) + [to_row([(buttons, 12)])]
' class="btn btn-inverse">%s</a>' % cancel_label)
if cancel_label else None])
_fields = list(to_fieldsets(fields))
if buttons:
_fields += [to_row([(buttons, 12)])]
super(SaplFormLayout, self).__init__(*_fields)

3
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:

367
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 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

26
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'),
),
]

25
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'),
),
]

48
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')]),
),
]

21
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'),
),
]

20
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'),
),
]

131
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%(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):

3
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<pk>\d+)', ConfirmarProposicao.as_view(),
url(r'^proposicao/confirmar/P(?P<hash>[0-9A-Fa-f]+)/'
'(?P<pk>\d+)', ConfirmarProposicao.as_view(),
name='proposicao-confirmar'),
url(r'^sistema/proposicao/tipo/',
include(TipoProposicaoCrud.get_urls())),

108
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:
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):

27
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)

23
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):

4
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

6
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

4
sapl/templates/crud/detail.html

@ -51,7 +51,7 @@
<div id="div_id_{{ column.id }}" class="form-group">
<p class="control-label">{{ column.verbose_name }}</p>
<div class="controls">
<!-- TODO Transformar os links em URLs diretamente no CRUD -->
{% comment %}TODO Transformar os links em URLs diretamente no CRUD{% endcomment %}
{% if column.text|url %}
<div class="form-control-static"><a href="{{ column.text|safe }}"> {{ column.text|safe }} </a></div>
{% else %}
@ -87,7 +87,7 @@
<td>
{% if href %}
<a href="{{ href }}">{{ value }}</a>
{% elif value != 'core.Cep.None' %}
{% elif 'None' not in value %}
{{ value|safe }}
{% endif %}
</td>

2
sapl/templates/crud/detail_detail.html

@ -80,7 +80,7 @@
<td>
{% if href %}
<a href="{{ href }}">{{ value }}</a>
{% elif valu != 'core.Cep.None' %}
{% elif 'None' in value %}
{{ value|safe }}
{% endif %}
</td>

43
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 %}
<style>
table {
border-collapse: collapse;
}
table, th, td {
border: 2px solid black;
}
</style>
<fieldset>
<legend>Confirmar recebimento de Proposição</legend>
<table class="table table-striped">
<tr><td><b>Tipo: </b>{{proposicao.tipo}}</td></tr>
<tr><td><b>Autor: </b>{{proposicao.autor}}</td></tr>
<tr><td><b>Descrição: </b>{{proposicao.descricao}}</td></tr>
<tr><td><b>Data de Envio: </b>{{proposicao.data_envio|date:'d/m/Y H:i:s'}}</td></tr>
</table>
<form method="POST">
{% csrf_token %}
<div align="center">
<input type="submit" value="Devolver ao autor" name="devolver" class="btn btn-danger">
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<input type="submit" value="Incorporar" name="incorporar" class="btn btn-primary">
<div class="context-actions clearfix">
{% block actions %}{{block.super}}
<div class="actions btn-group btn-group-sm pull-right" role="group">
{% if object.texto_articulado.exists %}
<a class="btn btn-default" href="{% url 'sapl.materia:proposicao_ta' object.pk%}">{% trans "Texto Eletrônico da Proposição" %}</a>
{% endif %}
{% if object.texto_original %}
<a class="btn btn-default" href="{{ object.texto_original.url }}">{% trans "Texto Original da Proposição" %}</a>
{% endif %}
</div>
</form>
</fieldset>
{% endblock actions%}
</div>
{{block.super}}
{% endblock %}

2
sapl/templates/materia/layouts.yaml

@ -82,7 +82,7 @@ Proposicao:
- tipo data_envio
- descricao
{% trans 'Materia' %}:
- materia
- materia_de_vinculo
{% trans 'Complemento' %}:
- texto_original

2
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 %}
<fieldset>
<legend>Proposições Não Incorporadas</legend>

3
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 %}
<fieldset>
<legend>Proposições Não Recebidas</legend>

12
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 %}
<fieldset>
<legend>Proposições Incorporadas</legend>
@ -27,10 +24,11 @@
<td>{{ prop.descricao }}</td>
<td>{{ prop.autor }}</td>
<td>
{% if prop.materia_gerada %}
<a href="{% url 'sapl.materia:materialegislativa_detail' prop.materia_gerada.pk %}">{{ prop.materia_gerada.tipo.sigla }} {{ prop.materia_gerada.numero }}/{{ prop.materia_gerada.ano }}</a>
{% elif prop.documento_gerado %}
<a href="{% url 'sapl.materia:documentoacessorio_detail' prop.documento_gerado.pk %}">{{ prop.documento_gerado.materia.tipo.sigla }} {{ prop.documento_gerado.materia.numero }}/{{ prop.documento_gerado.materia.ano }}</a>
{{ MateriaLegislativa.Meta}}
{% if prop.content_type.model == 'materialegislativa' %}
<a href="{% url 'sapl.materia:materialegislativa_detail' prop.object_id %}">{{ prop.conteudo_gerado_related.tipo.sigla }} {{ prop.conteudo_gerado_related.numero }}/{{ prop.conteudo_gerado_related.ano }}</a>
{% elif prop.content_type.model == 'documentoacessorio' %}
<a href="{% url 'sapl.materia:documentoacessorio_detail' prop.object_id %}">{{ prop.conteudo_gerado_related.materia.tipo.sigla }} {{ prop.conteudo_gerado_related.materia.numero }}/{{ prop.conteudo_gerado_related.materia.ano }}</a>
{% endif %}
</td>
</tr>

115
sapl/templates/materia/proposicao_detail.html

@ -1,32 +1,32 @@
{% extends "crud/detail.html" %}
{% load i18n %}
{% load common_tags %}
{% load i18n common_tags %}
{% block sub_actions %}{{block.super}}
{% endblock sub_actions%}
{% block editions %}
{% if proposicao.data_envio %}
<div class="actions btn-group btn-group-sm" role="group">
{% if proposicao.texto_articulado.exists %}
<a class="btn btn-success" href="{% url 'sapl.materia:proposicao_ta' proposicao.pk%}">{% trans "Texto Eletrônico" %}</a>
<div class="actions btn-group btn-group-sm {%block sub_actions_pull%}{% endblock%}" role="group">
{% if object.texto_articulado.exists %}
<a class="btn btn-success" href="{% url 'sapl.materia:proposicao_ta' object.pk%}">{% trans "Texto Eletrônico" %}</a>
{% endif %}
{% if proposicao.texto_original %}
<a class="btn btn-success" href="{{ proposicao.texto_original.url }}">{% trans "Texto Original" %}</a>
{% if object.texto_original %}
<a class="btn btn-success" href="{{ object.texto_original.url }}">{% trans "Texto Original" %}</a>
{% endif %}
</div>
{% endblock sub_actions%}
{% block editions %}
{% if object.data_envio %}
{% block editions_actions_return %}
<div class="actions btn-group" role="group">
{% if not object.data_recebimento %}
<a class="btn btn-default" onclick="window.open('{% url 'sapl.materia:recibo-proposicao' object.pk %}','Recibo','width=1100, height=600, scrollbars=yes')">{% trans "Recibo de Envio" %}</a>
{% if not object.data_recebimento %}
<a href="{{ view.detail_url }}?action=return" class="btn btn-default btn-excluir">{% trans 'Retornar Proposição Enviada' %}</a>
{% else %}
<a class="btn btn-default" onclick="window.open('{% url 'sapl.materia:recibo-proposicao' object.pk %}','Recibo','width=1100, height=600, scrollbars=yes')">{% trans "Homologação do Protocolo" %}</a>
{% endif %}
</div>
{% endblock %}
{% else %}
{% block editions_actions_send %}
<div class="actions btn-group" role="group">
<a href="{{ view.detail_url }}?action=send" class="btn btn-primary">{% trans 'Enviar' %}</a>
</div>
@ -35,6 +35,93 @@
<a href="{{ view.update_url }}" class="btn btn-default">{% trans 'Editar' %}</a>
<a href="{{ view.delete_url }}" class="btn btn-default btn-excluir">{% trans 'Excluir' %}</a>
</div>
{% endblock %}
{% endif %}
{% endblock editions %}
{% block detail_content %}
<h2 class="legend">{% model_verbose_name 'sapl.materia.models.Proposicao' %}</h2>
<div class="row-fluid">
<div class="col-sm-3">
<div id="div_id_tipo" class="form-group">
<p class="control-label">{%field_verbose_name object 'tipo'%}</p>
<div class="controls">
<div class="form-control-static">{{object.tipo}}</div>
</div>
</div>
</div>
{% if object.data_devolucao %}
<div class="col-sm-9">
<div class="alert alert-danger alert-dismissible fade in" role="alert">
<strong>{% trans "Proposição devolvida em:" %} {{ object.data_devolucao|date:"DATETIME_FORMAT"}}</strong>
<div >{% trans "Justificativa:" %} {{object.justificativa_devolucao}}</div>
</div>
</div>
{% else %}
{% if object.data_envio %}
<div class="col-sm-3">
<div id="div_id_data_envio" class="form-group">
<p class="control-label">{%field_verbose_name object 'data_envio'%}</p>
<div class="controls">
<div class="form-control-static">{{object.data_envio}}</div>
</div>
</div>
</div>
{% endif %}
{% if object.data_recebimento %}
<div class="col-sm-3">
<div id="div_id_data_envio" class="form-group">
<p class="control-label">{%field_verbose_name object 'data_recebimento'%}</p>
<div class="controls">
<div class="form-control-static">{{object.data_recebimento}}</div>
</div>
</div>
</div>
{% elif object.data_envio %}
<div class="col-sm-6">
<div class="alert alert-info alert-dismissible fade in" role="alert">
<div >{% trans "Proposição aguardando recebimento" %}</div>
</div>
</div>
{% endif %}
{% endif %}
</div>
<div class="row-fluid">
<div class="col-sm-12">
<div id="div_id_descricao" class="form-group">
<p class="control-label">{%field_verbose_name object 'descricao'%}</p>
<div class="controls">
<div class="form-control-static">{{object.descricao}}</div>
</div>
</div>
</div>
</div>
{% if object.materia_de_vinculo %}
<h2 class="legend">{% trans "Vínculo com a Matéria Legislativa" %}</h2>
<div class="row-fluid">
<div class="col-sm-12">
<div id="div_id_materia_de_vinculo" class="form-group">
<div class="controls">
<div class="form-control-static">{{object.materia_de_vinculo}}</div>
<div class="alert alert-info alert-dismissible fade in" role="alert">
<div >{{object.materia_de_vinculo.ementa}}</div>
</div>
</div>
</div>
</div>
</div>
{% endif %}
{% endblock detail_content %}

9
sapl/templates/materia/receber_proposicao.html

@ -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 %}
<p align="center"><font size="4" color="red"><b>{{msg}}</b></font></p>
{% endblock %}

6
sapl/templates/materia/subnav_prop.html

@ -1,6 +0,0 @@
<ul class="nav nav-pills navbar-right">
<li class=""><a href="{% url 'sapl.materia:receber-proposicao' %}">Receber Proposição</a></li>
<li class=""><a href="{% url 'sapl.materia:proposicao-pendente' %}">Proposições Não Recebidas</a></li>
<li class=""><a href="{% url 'sapl.materia:proposicao-devolvida' %}">Proposições Não Incorporadas</a></li>
<li class=""><a href="{% url 'sapl.materia:proposicao-recebida' %}">Proposições Incorporadas</a></li>
</ul>

9
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

9
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 = '<label class="radio'+(initial_select==val.value?' checked':'')+'"><span class="icons"><span class="first-icon"></span><span class="second-icon"></span></span><input type="radio" name="tipo_conteudo_related" id="id_tipo_conteudo_related_'+index+'" value="'+val.value+'"'+(initial_select?' checked="checked"':'')+'>'+val.text+'</label>';
var html_radio = '<div class="radio'+(initial_select==val.value?' checked':'')+'"> <label><span class="icons"><span class="first-icon"></span><span class="second-icon"></span></span><input type="radio" name="tipo_conteudo_related" id="id_tipo_conteudo_related_'+index+'" value="'+val.value+'"'+(initial_select?' checked="checked"':'')+' style="display:none;">'+val.text+'</label></div>';
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');
});

33
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 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

Loading…
Cancel
Save