Browse Source

Merge pull request #752 from interlegis/738-refatorar-proposicoes

Refatora básica proposições com opc de protocolo na incorporação
pull/754/head
Leandro Roberto da Silva 8 years ago
committed by GitHub
parent
commit
d7be867e7a
  1. 34
      sapl/api/forms.py
  2. 3
      sapl/api/pagination.py
  3. 1
      sapl/api/permissions.py
  4. 31
      sapl/api/serializers.py
  5. 25
      sapl/api/urls.py
  6. 50
      sapl/api/views.py
  7. 133
      sapl/base/apps.py
  8. 71
      sapl/base/forms.py
  9. 20
      sapl/base/migrations/0028_appconfig_proposicao_incorporacao_obrigatoria.py
  10. 20
      sapl/base/migrations/0029_auto_20161021_1445.py
  11. 20
      sapl/base/migrations/0030_auto_20161021_2017.py
  12. 109
      sapl/base/models.py
  13. 6
      sapl/base/views.py
  14. 14
      sapl/compilacao/forms.py
  15. 24
      sapl/crispy_layout_mixin.py
  16. 30
      sapl/crud/base.py
  17. 637
      sapl/materia/forms.py
  18. 14
      sapl/materia/migrations/0054_auto_20161009_1222.py
  19. 20
      sapl/materia/migrations/0056_remove_tipo_proposicao.py
  20. 45
      sapl/materia/migrations/0057_auto_20161016_0156.py
  21. 26
      sapl/materia/migrations/0058_auto_20161016_0329.py
  22. 19
      sapl/materia/migrations/0059_auto_20161016_1333.py
  23. 22
      sapl/materia/migrations/0060_auto_20161017_0050.py
  24. 20
      sapl/materia/migrations/0061_auto_20161017_1655.py
  25. 26
      sapl/materia/migrations/0062_auto_20161021_1424.py
  26. 25
      sapl/materia/migrations/0063_auto_20161021_1445.py
  27. 48
      sapl/materia/migrations/0064_auto_20161022_1405.py
  28. 21
      sapl/materia/migrations/0065_auto_20161022_1411.py
  29. 20
      sapl/materia/migrations/0066_proposicao_ano.py
  30. 216
      sapl/materia/models.py
  31. 3
      sapl/materia/urls.py
  32. 448
      sapl/materia/views.py
  33. 27
      sapl/norma/models.py
  34. 14
      sapl/protocoloadm/migrations/0003_auto_20161009_1222.py
  35. 21
      sapl/protocoloadm/migrations/0004_auto_20161023_1444.py
  36. 28
      sapl/protocoloadm/models.py
  37. 9
      sapl/protocoloadm/urls.py
  38. 3
      sapl/relatorios/views.py
  39. 6
      sapl/settings.py
  40. 64
      sapl/static/js/app.js
  41. 17
      sapl/static/styles/app.scss
  42. 7
      sapl/templates/base/autor_form.html
  43. 6
      sapl/templates/base/layouts.yaml
  44. 2
      sapl/templates/compilacao/textoarticulado_detail.html
  45. 2
      sapl/templates/compilacao/textoarticulado_list.html
  46. 54
      sapl/templates/crud/detail.html
  47. 2
      sapl/templates/crud/detail_detail.html
  48. 6
      sapl/templates/materia/autor_form.html
  49. 43
      sapl/templates/materia/confirmar_proposicao.html
  50. 16
      sapl/templates/materia/layouts.yaml
  51. 1
      sapl/templates/materia/materialegislativa_form.html
  52. 2
      sapl/templates/materia/prop_devolvidas_list.html
  53. 3
      sapl/templates/materia/prop_pendentes_list.html
  54. 12
      sapl/templates/materia/prop_recebidas_list.html
  55. 144
      sapl/templates/materia/proposicao_detail.html
  56. 49
      sapl/templates/materia/proposicao_form.html
  57. 9
      sapl/templates/materia/receber_proposicao.html
  58. 6
      sapl/templates/materia/subnav_prop.html
  59. 9
      sapl/templates/materia/subnav_prop.yaml
  60. 37
      sapl/templates/materia/tipoproposicao_form.html
  61. 36
      sapl/templates/sessao/presenca_ordemdia.html
  62. 97
      sapl/utils.py

34
sapl/api/forms.py

@ -1,20 +1,12 @@
from django.db.models import Q
from django_filters.filters import MethodFilter, ModelChoiceFilter
from rest_framework.filters import FilterSet
from sapl.base.forms import autores_models_generic_relations
from sapl.base.models import Autor, TipoAutor
from sapl.utils import generic_relations_for_model
class AutorChoiceFilterSet(FilterSet):
class SaplGenericRelationSearchFilterSet(FilterSet):
q = MethodFilter()
tipo = ModelChoiceFilter(queryset=TipoAutor.objects.all())
class Meta:
model = Autor
fields = ['q',
'tipo',
'nome', ]
def filter_q(self, queryset, value):
@ -28,11 +20,12 @@ class AutorChoiceFilterSet(FilterSet):
order_by = []
for gr in autores_models_generic_relations():
# model = gr[0]
for gr in generic_relations_for_model(self._meta.model):
model = gr[0]
sgr = gr[1]
for item in sgr:
if item.related_model != Autor:
if item.related_model != self._meta.model:
continue
flag_order_by = True
for field in item.fields_search:
@ -53,3 +46,18 @@ class AutorChoiceFilterSet(FilterSet):
queryset = queryset.filter(q).order_by(*order_by)
return queryset
class AutorChoiceFilterSet(SaplGenericRelationSearchFilterSet):
q = MethodFilter()
tipo = ModelChoiceFilter(queryset=TipoAutor.objects.all())
class Meta:
model = Autor
fields = ['q',
'tipo',
'nome', ]
def filter_q(self, queryset, value):
return SaplGenericRelationSearchFilterSet.filter_q(
self, queryset, value).order_by('nome')

3
sapl/api/pagination.py

@ -29,5 +29,6 @@ class StandardPagination(pagination.PageNumberPagination):
'total_pages': self.page.paginator.num_pages,
'page': self.page.number,
},
'models': data,
'results': data,
})

1
sapl/api/permissions.py

@ -14,4 +14,5 @@ class DjangoModelPermissions(DjangoModelPermissions):
'PUT': ['%(app_label)s.change_%(model_name)s'],
'PATCH': ['%(app_label)s.change_%(model_name)s'],
'DELETE': ['%(app_label)s.delete_%(model_name)s'],
}

31
sapl/api/serializers.py

@ -1,6 +1,6 @@
from rest_framework import serializers
from sapl.base.models import Autor
from sapl.materia.models import MateriaLegislativa
class ChoiceSerializer(serializers.Serializer):
@ -14,28 +14,39 @@ class ChoiceSerializer(serializers.Serializer):
return obj[0]
class AutorChoiceSerializer(ChoiceSerializer):
class ModelChoiceSerializer(ChoiceSerializer):
def get_text(self, obj):
return obj.nome
return str(obj)
def get_value(self, obj):
return obj.id
class ModelChoiceObjectRelatedField(serializers.RelatedField):
def to_representation(self, value):
return ModelChoiceSerializer(value).data
class AutorChoiceSerializer(ModelChoiceSerializer):
def get_text(self, obj):
return obj.nome
class Meta:
model = Autor
fields = ['id', 'nome']
class AutorObjectRelatedField(serializers.RelatedField):
class AutorSerializer(serializers.ModelSerializer):
autor_related = ModelChoiceObjectRelatedField(read_only=True)
def to_representation(self, value):
return str(value)
class Meta:
model = Autor
class AutorSerializer(serializers.ModelSerializer):
autor_related = AutorObjectRelatedField(read_only=True)
class MateriaLegislativaSerializer(serializers.ModelSerializer):
class Meta:
model = Autor
fields = ['id', 'tipo', 'nome', 'object_id', 'autor_related', 'user']
model = MateriaLegislativa

25
sapl/api/urls.py

@ -1,23 +1,27 @@
from django.conf import settings
from django.conf.urls import include, url
from django.conf.urls import url, include
from rest_framework.routers import DefaultRouter
from sapl.api.views import AutorListView
from sapl.api.views import MateriaLegislativaViewSet, AutorListView,\
ModelChoiceView
from .apps import AppConfig
app_name = AppConfig.name
# router = DefaultRouter()
# urlpatterns += router.urls
router = DefaultRouter()
router.register(r'materia', MateriaLegislativaViewSet)
urlpatterns_router = router.urls
urlpatterns_api = [
# url(r'^$', api_root),
url(r'^autor',
AutorListView.as_view(),
name='autor_list'),
url(r'^autor', AutorListView.as_view(), name='autor_list'),
url(r'^model/(?P<content_type>\d+)/(?P<pk>\d*)$',
ModelChoiceView.as_view(), name='model_list'),
]
if settings.DEBUG:
@ -25,5 +29,6 @@ if settings.DEBUG:
url(r'^docs', include('rest_framework_docs.urls')), ]
urlpatterns = [
url(r'^api/', include(urlpatterns_api))
url(r'^api/', include(urlpatterns_api)),
url(r'^api/', include(urlpatterns_router))
]

50
sapl/api/views.py

@ -1,17 +1,43 @@
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
from django.http import Http404
from django.utils.translation import ugettext_lazy as _
from rest_framework.filters import DjangoFilterBackend
from rest_framework.generics import ListAPIView
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.mixins import ListModelMixin, RetrieveModelMixin
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.viewsets import GenericViewSet
from sapl.api.forms import AutorChoiceFilterSet
from sapl.api.serializers import (AutorChoiceSerializer, AutorSerializer,
ChoiceSerializer)
from sapl.api.serializers import ChoiceSerializer, AutorSerializer,\
AutorChoiceSerializer, ModelChoiceSerializer, MateriaLegislativaSerializer
from sapl.base.models import Autor, TipoAutor
from sapl.materia.models import MateriaLegislativa
from sapl.utils import SaplGenericRelation, sapl_logger
class ModelChoiceView(ListAPIView):
# FIXME aplicar permissão correta de usuário
permission_classes = (IsAuthenticated,)
serializer_class = ModelChoiceSerializer
def get(self, request, *args, **kwargs):
self.model = ContentType.objects.get_for_id(
self.kwargs['content_type']).model_class()
pagination = request.GET.get('pagination', '')
if pagination == 'False':
self.pagination_class = None
return ListAPIView.get(self, request, *args, **kwargs)
def get_queryset(self):
return self.model.objects.all()
class AutorListView(ListAPIView):
"""
Listagem de Autores com filtro para autores cadastrados
@ -32,6 +58,10 @@ class AutorListView(ListAPIView):
de Autores mas feito para Possíveis Autores armazenados
segundo o ContentType associado ao Tipo de Autor via
relacionamento genérico.
<<<<<<< HEAD
=======
>>>>>>> master
Busca feita sem django-filter processada no get_queryset
-> processo no cadastro de autores para seleção e busca
dos possíveis autores
@ -56,8 +86,7 @@ class AutorListView(ListAPIView):
TR_AUTOR_SERIALIZER = 3
# FIXME aplicar permissão correta de usuário
permission_classes = (AllowAny,)
serializer_class = AutorSerializer
permission_classes = (IsAuthenticated,)
queryset = Autor.objects.all()
model = Autor
@ -166,3 +195,14 @@ class AutorListView(ListAPIView):
if tipos.count() > 1:
r.sort(key=lambda x: x[1].upper())
return r
class MateriaLegislativaViewSet(ListModelMixin,
RetrieveModelMixin,
GenericViewSet):
permission_classes = (IsAuthenticated,)
serializer_class = MateriaLegislativaSerializer
queryset = MateriaLegislativa.objects.all()
filter_backends = (DjangoFilterBackend,)
filter_fields = ('numero', 'ano', 'tipo', )

133
sapl/base/apps.py

@ -1,8 +1,137 @@
from django import apps
from builtins import LookupError
from django.apps import apps
from django.contrib.auth.management import _get_all_permissions
from django.core import exceptions
from django.db import router
from django.db.models.signals import pre_migrate, post_migrate
from django.db.utils import DEFAULT_DB_ALIAS
from django.utils.translation import string_concat
from django.utils.translation import ugettext_lazy as _
import django
def create_proxy_permissions(
app_config, verbosity=2, interactive=True,
using=DEFAULT_DB_ALIAS, **kwargs):
if not app_config.models_module:
return
# print(app_config)
try:
Permission = apps.get_model('auth', 'Permission')
except LookupError:
return
if not router.allow_migrate_model(using, Permission):
return
from django.contrib.contenttypes.models import ContentType
permission_name_max_length = Permission._meta.get_field('name').max_length
# This will hold the permissions we're looking for as
# (content_type, (codename, name))
searched_perms = list()
# The codenames and ctypes that should exist.
ctypes = set()
for klass in list(app_config.get_models()):
opts = klass._meta
permissions = (
("list_" + opts.model_name,
string_concat(
_('Visualizaçao da lista de'), ' ',
opts.verbose_name_plural)),
("detail_" + opts.model_name,
string_concat(
_('Visualização dos detalhes de'), ' ',
opts.verbose_name_plural)),
)
opts.permissions = tuple(
set(list(permissions) + list(opts.permissions)))
if opts.proxy:
# Force looking up the content types in the current database
# before creating foreign keys to them.
app_label, model = opts.app_label, opts.model_name
try:
ctype = ContentType.objects.db_manager(
using).get_by_natural_key(app_label, model)
except:
ctype = ContentType.objects.db_manager(
using).create(app_label=app_label, model=model)
else:
ctype = ContentType.objects.db_manager(using).get_for_model(klass)
ctypes.add(ctype)
for perm in _get_all_permissions(klass._meta, ctype):
searched_perms.append((ctype, perm))
class AppConfig(apps.AppConfig):
# Find all the Permissions that have a content_type for a model we're
# looking for. We don't need to check for codenames since we already have
# a list of the ones we're going to create.
all_perms = set(Permission.objects.using(using).filter(
content_type__in=ctypes,
).values_list(
"content_type", "codename"
))
perms = [
Permission(codename=codename, name=name, content_type=ct)
for ct, (codename, name) in searched_perms
if (ct.pk, codename) not in all_perms
]
# Validate the permissions before bulk_creation to avoid cryptic database
# error when the name is longer than 255 characters
for perm in perms:
if len(perm.name) > permission_name_max_length:
raise exceptions.ValidationError(
'The permission name %s of %s.%s '
'is longer than %s characters' % (
perm.name,
perm.content_type.app_label,
perm.content_type.model,
permission_name_max_length,
)
)
Permission.objects.using(using).bulk_create(perms)
if verbosity >= 2:
for perm in perms:
print("Adding permission '%s'" % perm)
def run_sql_organizers(
app_config, verbosity=2, interactive=True,
using=DEFAULT_DB_ALIAS, **kwargs):
"""with connection.cursor() as cursor:
for line in lines:
line = line.strip()
if not line or line.startswith('#'):
continue
try:
cursor.execute(line)"""
print('aqui run_sql_organizer', app_config)
""" update protocoloadm_protocolo set autor_id = null;
delete from materia_autoria;
delete from materia_proposicao;
delete from materia_tipoproposicao;
"""
class AppConfig(django.apps.AppConfig):
name = 'sapl.base'
label = 'base'
verbose_name = _('Dados Básicos')
def ready(self):
pre_migrate.connect(run_sql_organizers, self)
post_migrate.connect(
receiver=create_proxy_permissions,
dispatch_uid="django.contrib.auth.management.create_permissions")

71
sapl/base/forms.py

@ -1,7 +1,8 @@
import django_filters
from crispy_forms.bootstrap import FieldWithButtons, InlineRadios, StrictButton
from crispy_forms.helper import FormHelper
from crispy_forms.layout import HTML, Button, Div, Field, Fieldset, Layout, Row
from crispy_forms.templatetags.crispy_forms_field import css_class
from django import forms
from django.conf import settings
from django.contrib.auth import get_user_model
@ -13,6 +14,9 @@ from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db import models, transaction
from django.forms import ModelForm
from django.utils.translation import ugettext_lazy as _, string_concat
import django_filters
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import string_concat
@ -23,8 +27,10 @@ from sapl.materia.models import MateriaLegislativa
from sapl.sessao.models import SessaoPlenaria
from sapl.settings import MAX_IMAGE_UPLOAD_SIZE
from sapl.utils import (RANGE_ANOS, ImageThumbnailFileInput,
RangeWidgetOverride, SaplGenericRelation, autor_label,
autor_modal)
RangeWidgetOverride, autor_label, autor_modal,
SaplGenericRelation, models_with_gr_for_model,
ChoiceWithoutValidationField)
from .models import AppConfig, CasaLegislativa
@ -35,6 +41,13 @@ ACTION_CREATE_USERS_AUTOR_CHOICE = [
]
ACTION_CREATE_USERS_AUTOR_CHOICE = [
('C', _('Criar novo Usuário')),
('A', _('Associar um usuário existente')),
('N', _('Autor sem Usuário de Acesso ao Sapl')),
]
STATUS_USER_CHOICE = [
('R', _('Apenas retirar Perfil de Autor do Usuário que está sendo'
' desvinculado')),
@ -44,31 +57,6 @@ STATUS_USER_CHOICE = [
]
def autores_models_generic_relations():
models_of_generic_relations = list(map(
lambda x: x.related_model,
filter(
lambda obj: obj.is_relation and
hasattr(obj, 'field') and
isinstance(obj, GenericRel),
Autor._meta.get_fields(include_hidden=True))
))
models = list(map(
lambda x: (x,
list(filter(
lambda field: (
isinstance(
field, SaplGenericRelation) and
field.related_model == Autor),
x._meta.get_fields(include_hidden=True)))),
models_of_generic_relations
))
return models
class TipoAutorForm(ModelForm):
content_type = forms.ModelChoiceField(
@ -85,32 +73,14 @@ class TipoAutorForm(ModelForm):
super(TipoAutorForm, self).__init__(*args, **kwargs)
# Models que apontaram uma GenericRelation com Autor
models_of_generic_relations = list(map(
lambda x: x.related_model,
filter(
lambda obj: obj.is_relation and
hasattr(obj, 'field') and
isinstance(obj, GenericRel),
Autor._meta.get_fields(include_hidden=True))
))
content_types = ContentType.objects.get_for_models(
*models_of_generic_relations)
*models_with_gr_for_model(Autor))
self.fields['content_type'].choices = [
('', _('Outros (Especifique)'))] + [
(ct.pk, ct) for key, ct in content_types.items()]
class ChoiceWithoutValidationField(forms.ChoiceField):
def validate(self, value):
if self.required and not value:
raise ValidationError(
self.error_messages['required'], code='required')
class AutorForm(ModelForm):
senha = forms.CharField(
max_length=20,
@ -171,8 +141,6 @@ class AutorForm(ModelForm):
StrictButton(
_('Filtrar'), css_class='btn-default btn-filtrar-autor',
type='button')),
css_class='hidden',
data_action='create',
data_application='AutorSearch',
@ -190,6 +158,7 @@ class AutorForm(ModelForm):
row2 = Row(to_column((InlineRadios('action_user'), 8)),
to_column((Div('username'), 4)))
row3 = Row(to_column(('senha', 3)),
to_column(('senha_confirma', 3)),
to_column(('email', 3)),
@ -220,6 +189,7 @@ class AutorForm(ModelForm):
self.fields['autor_related'].choices = [
(self.instance.autor_related.pk,
self.instance.autor_related)]
self.fields['q'].initial = ''
self.fields['autor_related'].initial = self.instance.autor_related
@ -706,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'),
),
]

109
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')
@ -191,98 +203,3 @@ class Autor(models.Model):
return str(self.partido)
else:
"""
def create_proxy_permissions(
app_config, verbosity=2, interactive=True,
using=DEFAULT_DB_ALIAS, **kwargs):
if not app_config.models_module:
return
# print(app_config)
try:
Permission = apps.get_model('auth', 'Permission')
except LookupError:
return
if not router.allow_migrate_model(using, Permission):
return
from django.contrib.contenttypes.models import ContentType
permission_name_max_length = Permission._meta.get_field('name').max_length
# This will hold the permissions we're looking for as
# (content_type, (codename, name))
searched_perms = list()
# The codenames and ctypes that should exist.
ctypes = set()
for klass in list(app_config.get_models()):
opts = klass._meta
permissions = (
("list_" + opts.model_name,
string_concat(
_('Visualizaçao da lista de'), ' ',
opts.verbose_name_plural)),
("detail_" + opts.model_name,
string_concat(
_('Visualização dos detalhes de'), ' ',
opts.verbose_name_plural)),
)
opts.permissions = tuple(
set(list(permissions) + list(opts.permissions)))
if opts.proxy:
# Force looking up the content types in the current database
# before creating foreign keys to them.
app_label, model = opts.app_label, opts.model_name
try:
ctype = ContentType.objects.db_manager(
using).get_by_natural_key(app_label, model)
except:
ctype = ContentType.objects.db_manager(
using).create(app_label=app_label, model=model)
else:
ctype = ContentType.objects.db_manager(using).get_for_model(klass)
ctypes.add(ctype)
for perm in _get_all_permissions(klass._meta, ctype):
searched_perms.append((ctype, perm))
# Find all the Permissions that have a content_type for a model we're
# looking for. We don't need to check for codenames since we already have
# a list of the ones we're going to create.
all_perms = set(Permission.objects.using(using).filter(
content_type__in=ctypes,
).values_list(
"content_type", "codename"
))
perms = [
Permission(codename=codename, name=name, content_type=ct)
for ct, (codename, name) in searched_perms
if (ct.pk, codename) not in all_perms
]
# Validate the permissions before bulk_creation to avoid cryptic database
# error when the name is longer than 255 characters
for perm in perms:
if len(perm.name) > permission_name_max_length:
raise exceptions.ValidationError(
'The permission name %s of %s.%s '
'is longer than %s characters' % (
perm.name,
perm.content_type.app_label,
perm.content_type.model,
permission_name_max_length,
)
)
Permission.objects.using(using).bulk_create(perms)
if verbosity >= 2:
for perm in perms:
print("Adding permission '%s'" % perm)
models.signals.post_migrate.connect(
receiver=create_proxy_permissions,
dispatch_uid="django.contrib.auth.management.create_permissions")

6
sapl/base/views.py

@ -13,8 +13,8 @@ from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
from django.utils.translation import ugettext_lazy as _
from django.views.generic.base import TemplateView
from django_filters.views import FilterView
from sapl.base.forms import AutorForm, TipoAutorForm, AutorFormForAdmin
from sapl.base.forms import AutorForm, AutorFormForAdmin, TipoAutorForm
from sapl.base.models import Autor, TipoAutor
from sapl.crud.base import CrudAux
from sapl.materia.models import MateriaLegislativa, TipoMateriaLegislativa
@ -135,10 +135,6 @@ class AutorCrud(CrudAux):
form_class = AutorForm
layout_key = None
def form_valid(self, form):
# devido a implement do form o form_valid do Crud deve ser pulado
return super(CrudAux.CreateView, self).form_valid(form)
def post(self, request, *args, **kwargs):
if request.user.is_superuser:
self.form_class = AutorFormForAdmin

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)

24
sapl/crispy_layout_mixin.py

@ -1,12 +1,12 @@
from math import ceil
import rtyaml
from crispy_forms.bootstrap import FormActions
from crispy_forms.helper import FormHelper
from crispy_forms.layout import HTML, Div, Fieldset, Layout, Submit
from django import template
from django.utils import formats
from django.utils.translation import ugettext as _
import rtyaml
def heads_and_tails(list_of_lists):
@ -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('<a href="{{ view.cancel_url }}"'
' class="btn btn-inverse">%s</a>' % 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('<a href="{{ view.cancel_url }}"'
' 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)
@ -62,9 +70,11 @@ def get_field_display(obj, fieldname):
else:
value = getattr(obj, fieldname)
str_type = str(type(value))
if value is None:
display = ''
elif 'date' in str(type(value)):
elif 'date' in str_type:
display = formats.date_format(value, "SHORT_DATE_FORMAT")
elif 'bool' in str(type(value)):
display = _('Sim') if value else _('Não')

30
sapl/crud/base.py

@ -7,11 +7,13 @@ from crispy_forms.helper import FormHelper
from crispy_forms.layout import Field, Layout
from django import forms
from django.conf.urls import url
from django.contrib import messages
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.core.urlresolvers import reverse
from django.db import models
from django.db.models.fields.related import ForeignKey
from django.http.response import Http404
from django.shortcuts import redirect
from django.utils.decorators import classonlymethod
from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _
@ -193,6 +195,26 @@ class PermissionRequiredContainerCrudMixin(PermissionRequiredMixin):
if not self.model.objects.filter(**params).exists():
raise Http404()
elif self.container_field:
container = self.container_field.split('__')
if len(container) > 1:
container_model = getattr(
self.model, container[0]).field.related_model
params = {}
params['__'.join(
container[1:])] = request.user.pk
if not container_model.objects.filter(**params).exists():
messages.error(
request,
'O Usuário (%s) não está registrado como (%s).' % (
request.user, container_model._meta.verbose_name))
return redirect('/')
else:
# TODO: implementar caso o user for o próprio o container
pass
return super(PermissionRequiredMixin, self).dispatch(
request, *args, **kwargs)
@ -585,7 +607,7 @@ class CrudCreateView(PermissionRequiredContainerCrudMixin,
return super(CrudCreateView, self).get_context_data(**kwargs)
def form_valid(self, form):
self.object = form.save(commit=False)
self.object = form.instance
try:
self.object.owner = self.request.user
self.object.modifier = self.request.user
@ -596,6 +618,7 @@ class CrudCreateView(PermissionRequiredContainerCrudMixin,
container = self.container_field.split('__')
if len(container) > 1:
# TODO: implementar caso o user for próprio o container
container_model = getattr(
self.model, container[0]).field.related_model
@ -612,7 +635,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:
@ -773,7 +797,7 @@ class CrudUpdateView(PermissionRequiredContainerCrudMixin,
permission_required = (RP_CHANGE, )
def form_valid(self, form):
self.object = form.save(commit=False)
self.object = form.instance
try:
self.object.modifier = self.request.user
except:

637
sapl/materia/forms.py

@ -1,24 +1,38 @@
from datetime import datetime
import django_filters
from datetime import datetime, date
import os
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
from crispy_forms.layout import HTML, Button, Column, Fieldset, Layout, Row,\
Field, Submit
from django import forms
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.db import models
from django.core.files.base import File
from django.core.urlresolvers import reverse
from django.db import models, transaction
from django.db.models import Max
from django.forms import ModelForm
from django.forms import ModelForm, widgets
from django.forms.forms import Form
from django.utils.translation import ugettext_lazy as _
import django_filters
from sapl.base.models import Autor
from sapl.comissoes.models import Comissao
from sapl.crispy_layout_mixin import form_actions, to_row
from sapl.crispy_layout_mixin import form_actions, to_row, to_column,\
SaplFormLayout
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)
autor_modal, models_with_gr_for_model,
ChoiceWithoutValidationField, YES_NO_CHOICES)
import sapl
from .models import (AcompanhamentoMateria, Anexada, Autoria, DespachoInicial,
DocumentoAcessorio, MateriaLegislativa, Numeracao,
@ -36,20 +50,9 @@ def em_tramitacao():
(False, 'Não')]
class ConfirmarProposicaoForm(ModelForm):
class Meta:
model = Proposicao
exclude = ['texto_original', 'descricao', 'tipo']
class ReceberProposicaoForm(ModelForm):
class ReceberProposicaoForm(Form):
cod_hash = forms.CharField(label='Código do Documento', required=True)
class Meta:
model = Proposicao
exclude = ['texto_original', 'descricao', 'tipo']
def __init__(self, *args, **kwargs):
row1 = to_row([('cod_hash', 12)])
self.helper = FormHelper()
@ -81,7 +84,7 @@ class UnidadeTramitacaoForm(ModelForm):
return cleaned_data
class ProposicaoForm(ModelForm):
class ProposicaoOldForm(ModelForm):
tipo_materia = forms.ModelChoiceField(
label=_('Matéria Vinculada'),
@ -129,7 +132,7 @@ class ProposicaoForm(ModelForm):
return cleaned_data
def save(self, commit=False):
proposicao = super(ProposicaoForm, self).save(commit)
proposicao = super(ProposicaoOldForm, self).save(commit)
if 'materia' in self.cleaned_data:
proposicao.materia = self.cleaned_data['materia']
proposicao.save()
@ -771,3 +774,595 @@ class TramitacaoEmLoteFilterSet(django_filters.FilterSet):
self.form.helper.layout = Layout(
Fieldset(_('Tramitação em Lote'),
row1, row2, form_actions(save_label='Pesquisar')))
class TipoProposicaoForm(ModelForm):
conteudo = forms.ModelChoiceField(
queryset=ContentType.objects.all(),
label=TipoProposicao._meta.get_field('conteudo').verbose_name,
required=True)
tipo_conteudo_related_radio = ChoiceWithoutValidationField(
label="Seleção de Tipo",
required=False,
widget=forms.RadioSelect())
tipo_conteudo_related = forms.IntegerField(
widget=forms.HiddenInput(),
required=True)
class Meta:
model = TipoProposicao
fields = ['descricao',
'conteudo',
'tipo_conteudo_related_radio',
'tipo_conteudo_related']
widgets = {'tipo_conteudo_related': forms.HiddenInput()}
def __init__(self, *args, **kwargs):
tipo_select = Fieldset(TipoProposicao._meta.verbose_name,
to_column(('descricao', 5)),
to_column(('conteudo', 7)),
to_column(('tipo_conteudo_related_radio', 12)))
self.helper = FormHelper()
self.helper.layout = SaplFormLayout(tipo_select)
super(TipoProposicaoForm, self).__init__(*args, **kwargs)
content_types = ContentType.objects.get_for_models(
*models_with_gr_for_model(TipoProposicao))
self.fields['conteudo'].choices = [
(ct.pk, ct) for k, ct in content_types.items()]
self.fields['conteudo'].choices.sort(key=lambda x: str(x[1]))
if self.instance.pk:
self.fields[
'tipo_conteudo_related'].initial = self.instance.object_id
def clean(self):
cd = self.cleaned_data
conteudo = cd['conteudo']
if 'tipo_conteudo_related' not in cd or not cd['tipo_conteudo_related']:
raise ValidationError(
_('Seleção de Tipo não definida'))
if not conteudo.model_class().objects.filter(
pk=cd['tipo_conteudo_related']).exists():
raise ValidationError(
_('O Registro definido (%s) não está na base de %s.'
) % (cd['tipo_conteudo_related'], cd['q'], conteudo))
return self.cleaned_data
@transaction.atomic
def save(self, commit=False):
tipo_proposicao = super(TipoProposicaoForm, self).save(commit)
assert tipo_proposicao.conteudo
tipo_proposicao.tipo_conteudo_related = \
tipo_proposicao.conteudo.model_class(
).objects.get(pk=self.cleaned_data['tipo_conteudo_related'])
tipo_proposicao.save()
return tipo_proposicao
class ProposicaoForm(forms.ModelForm):
TIPO_TEXTO_CHOICE = [
('D', _('Arquivo Digital')),
('T', _('Texto Articulado'))
]
tipo_materia = forms.ModelChoiceField(
label=TipoMateriaLegislativa._meta.verbose_name,
required=False,
queryset=TipoMateriaLegislativa.objects.all(),
empty_label='Selecione')
numero_materia = forms.CharField(
label='Número', required=False)
ano_materia = forms.CharField(
label='Ano', required=False)
tipo_texto = forms.MultipleChoiceField(
label=_('Tipo do Texto da Proposição'),
required=False,
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 = sapl.base.models.AppConfig.attr(
'texto_articulado_proposicao')
if not self.texto_articulado_proposicao:
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(
TipoProposicao._meta.verbose_name, Field('tipo')), 3)),
Fieldset(_('Vincular a Matéria Legislativa Existente'),
to_column(('tipo_materia', 4)),
to_column(('numero_materia', 4)),
to_column(('ano_materia', 4))
),
to_column(
(Alert('teste',
css_class="ementa_materia hidden alert-info",
dismiss=False), 12)),
to_column(('descricao', 12)),
]
if self.texto_articulado_proposicao:
fields.append(
to_column((InlineCheckboxes('tipo_texto'), 5)),)
fields.append(to_column((
'texto_original', 7 if self.texto_articulado_proposicao else 12)))
self.helper = FormHelper()
self.helper.layout = SaplFormLayout(*fields)
super(ProposicaoForm, self).__init__(*args, **kwargs)
if self.instance.pk:
self.fields['tipo_texto'].initial = []
if self.instance.texto_original:
self.fields['tipo_texto'].initial.append('D')
if self.texto_articulado_proposicao:
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:
if texto_original.size > MAX_DOC_UPLOAD_SIZE:
raise ValidationError("Arquivo muito grande. ( > 5mb )")
return texto_original
def clean(self):
cd = self.cleaned_data
tm, am, nm = (cd.get('tipo_materia', ''),
cd.get('ano_materia', ''),
cd.get('numero_materia', ''))
if tm and am and nm:
try:
materia_de_vinculo = MateriaLegislativa.objects.get(
tipo_id=tm,
ano=am,
numero=nm
)
except ObjectDoesNotExist:
raise ValidationError(_('Matéria Vinculada não existe!'))
else:
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()
self.instance.results = {
'messages': {
'success': [_('Devolução efetuada com sucesso.'), ]
},
'url': reverse('sapl.materia:receber-proposicao')
}
return self.instance
elif 'incorporar' in self.data:
self.instance.justificativa_devolucao = ''
self.instance.data_devolucao = None
self.instance.data_recebimento = datetime.now()
self.instance.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.
"""
self.instance.results = {
'messages': {
'success': [_('Proposição incorporada com sucesso'), ]
},
'url': reverse('sapl.materia:receber-proposicao')
}
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
self.instance.results['messages']['success'].append(_(
'Matéria Legislativa registrada com sucesso (%s)'
) % str(materia))
# autoria
autoria = Autoria()
autoria.autor = proposicao.autor
autoria.materia = materia
autoria.primeiro_autor = True
autoria.save()
self.instance.results['messages']['success'].append(_(
'Autoria registrada para (%s)'
) % str(autoria.autor))
# 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()
self.instance.results['messages']['success'].append(_(
'Matéria anexada a (%s)'
) % str(anexada.materia_principal))
self.instance.results['url'] = reverse(
'sapl.materia:materialegislativa_detail',
kwargs={'pk': materia.pk})
elif self.instance.tipo.conteudo.model_class() == TipoDocumento:
# dados básicos
doc = DocumentoAcessorio()
doc.materia = proposicao.materia_de_vinculo
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
self.instance.results['messages']['success'].append(_(
'Documento Acessório registrado com sucesso e anexado (%s)'
) % str(doc.materia))
self.instance.results['url'] = reverse(
'sapl.materia:documentoacessorio_detail',
kwargs={'pk': doc.pk})
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' not in cd or cd['gerar_protocolo'] == 'False':
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
"""
numeracao = sapl.base.models.AppConfig.attr('sequencia_numeracao')
if numeracao == 'A':
nm = Protocolo.objects.filter(
ano=date.today().year).aggregate(Max('numero'))
elif numeracao == 'U':
nm = Protocolo.objects.all().aggregate(Max('numero'))
protocolo = Protocolo()
protocolo.numero = (nm['numero__max'] + 1) if nm['numero__max'] else 1
protocolo.ano = date.today().year
protocolo.data = date.today()
protocolo.hora = datetime.now().time()
# TODO transformar campo timestamp em auto_now_add
protocolo.timestamp = datetime.now()
protocolo.tipo_protocolo = '1'
# 1 Processo Legislativo
# 0 Processo Administrativo
protocolo.tipo_processo = '1'
protocolo.interessado = str(proposicao.autor)
protocolo.autor = proposicao.autor
protocolo.numero_paginas = cd['numero_de_paginas']
protocolo.anulado = False
if self.instance.tipo.conteudo.model_class() == TipoMateriaLegislativa:
protocolo.tipo_materia = proposicao.tipo.tipo_conteudo_related
elif self.instance.tipo.conteudo.model_class() == TipoDocumento:
protocolo.tipo_documento = proposicao.tipo.tipo_conteudo_related
protocolo.save()
self.instance.results['messages']['success'].append(_(
'Protocolo realizado com sucesso'))
# FIXME qdo protocoloadm estiver homologado, verifique a necessidade
# de redirecionamento para o protocolo.
"""
self.instance.results['url'] = reverse(
'sapl.protocoloadm:...',
kwargs={'pk': protocolo.pk})
"""
conteudo_gerado.numero_protocolo = protocolo.numero
conteudo_gerado.save()
return self.instance

14
sapl/materia/migrations/0054_auto_20161009_1222.py

@ -6,6 +6,13 @@ from django.db import migrations, models
import django.db.models.deletion
def clear_model_autoria(apps, schema_editor):
Autoria = apps.get_model("materia", "Autoria")
Autoria.objects.all().delete()
Proposicao = apps.get_model("materia", "Proposicao")
Proposicao.objects.all().delete()
class Migration(migrations.Migration):
dependencies = [
@ -14,6 +21,7 @@ class Migration(migrations.Migration):
]
operations = [
migrations.RunPython(clear_model_autoria),
migrations.RemoveField(
model_name='autor',
name='comissao',
@ -37,12 +45,14 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='autoria',
name='autor',
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='base.Autor', verbose_name='Autor'),
field=models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE, to='base.Autor', verbose_name='Autor'),
),
migrations.AlterField(
model_name='proposicao',
name='autor',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='base.Autor'),
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='base.Autor'),
),
migrations.DeleteModel(
name='Autor',

20
sapl/materia/migrations/0056_remove_tipo_proposicao.py

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-10-11 19:45
from __future__ import unicode_literals
from django.db import migrations
def clear_model_tipo_proposicao(apps, schema_editor):
TipoProposicao = apps.get_model("materia", "TipoProposicao")
TipoProposicao.objects.all().delete()
class Migration(migrations.Migration):
dependencies = [
('materia', '0056_merge'),
]
operations = [
migrations.RunPython(clear_model_tipo_proposicao), ]

45
sapl/materia/migrations/0057_auto_20161016_0156.py

@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-10-16 03:56
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
def clear_model_tipo_proposicao(apps, schema_editor):
TipoProposicao = apps.get_model("materia", "TipoProposicao")
TipoProposicao.objects.all().delete()
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('materia', '0056_remove_tipo_proposicao'),
]
operations = [
migrations.RunPython(clear_model_tipo_proposicao),
migrations.RemoveField(
model_name='tipoproposicao',
name='materia_ou_documento',
),
migrations.RemoveField(
model_name='tipoproposicao',
name='modelo',
),
migrations.RemoveField(
model_name='tipoproposicao',
name='tipo_documento',
),
migrations.RemoveField(
model_name='tipoproposicao',
name='tipo_materia',
),
migrations.AddField(
model_name='tipoproposicao',
name='conteudo',
field=models.ForeignKey(
default=None, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType', verbose_name='Conteúdo'),
),
]

26
sapl/materia/migrations/0058_auto_20161016_0329.py

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-10-16 05:29
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('materia', '0057_auto_20161016_0156'),
]
operations = [
migrations.AddField(
model_name='tipoproposicao',
name='object_id',
field=models.PositiveIntegerField(blank=True, default=None, null=True),
),
migrations.AlterField(
model_name='tipoproposicao',
name='conteudo',
field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType', verbose_name='Definição de Tipo'),
),
]

19
sapl/materia/migrations/0059_auto_20161016_1333.py

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-10-16 15:33
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('materia', '0058_auto_20161016_0329'),
]
operations = [
migrations.AlterUniqueTogether(
name='tipoproposicao',
unique_together=set([('conteudo', 'object_id')]),
),
]

22
sapl/materia/migrations/0060_auto_20161017_0050.py

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-10-17 02:50
from __future__ import unicode_literals
from django.db import migrations, models
import sapl.materia.models
import sapl.utils
class Migration(migrations.Migration):
dependencies = [
('materia', '0059_auto_20161016_1333'),
]
operations = [
migrations.AlterField(
model_name='proposicao',
name='texto_original',
field=models.FileField(blank=True, null=True, upload_to=sapl.materia.models.texto_upload_path, validators=[sapl.utils.restringe_tipos_de_arquivo_txt], verbose_name='Texto Original'),
),
]

20
sapl/materia/migrations/0061_auto_20161017_1655.py

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-10-17 18:55
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('materia', '0060_auto_20161017_0050'),
]
operations = [
migrations.AlterField(
model_name='proposicao',
name='descricao',
field=models.TextField(verbose_name='Descrição'),
),
]

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

216
sapl/materia/models.py

@ -1,13 +1,23 @@
import datetime
import re
from django.contrib.auth.models import Group
from django.contrib.contenttypes.fields import GenericForeignKey,\
GenericRelation
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.db.models.deletion import PROTECT
from django.utils.translation import ugettext_lazy as _
from model_utils import Choices
from sapl.base.models import Autor
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,
restringe_tipos_de_arquivo_txt)
restringe_tipos_de_arquivo_txt, SaplGenericRelation,
SaplGenericForeignKey, texto_upload_path)
EM_TRAMITACAO = [(1, 'Sim'),
(0, 'Não')]
@ -21,6 +31,41 @@ def grupo_autor():
return grupo.id
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(
blank=True, null=True, default=None)
tipo_conteudo_related = SaplGenericForeignKey(
'conteudo', 'object_id', verbose_name=_('Seleção de Tipo'))
"""materia_ou_documento = models.CharField(
max_length=1, verbose_name=_('Gera'), choices=MAT_OU_DOC_CHOICES)
modelo = models.CharField(max_length=50, verbose_name=_('Modelo XML'))
# mutually exclusive (depend on materia_ou_documento)
tipo_materia = models.ForeignKey(
TipoMateriaLegislativa,
blank=True,
null=True,
verbose_name=_('Tipo de Matéria'))
tipo_documento = models.ForeignKey(
TipoDocumento, blank=True, null=True,
verbose_name=_('Tipo de Documento'))"""
class Meta:
verbose_name = _('Tipo de Proposição')
verbose_name_plural = _('Tipos de Proposições')
unique_together = (('conteudo', 'object_id'), )
def __str__(self):
return self.descricao
class TipoMateriaLegislativa(models.Model):
sigla = models.CharField(max_length=5, verbose_name=_('Sigla'))
descricao = models.CharField(max_length=50, verbose_name=_('Descrição '))
@ -29,6 +74,14 @@ class TipoMateriaLegislativa(models.Model):
# XXX o que é isso ?
quorum_minimo_votacao = models.PositiveIntegerField(blank=True, null=True)
tipo_proposicao = SaplGenericRelation(
TipoProposicao,
related_query_name='tipomaterialegislativa_set',
fields_search=(
('descricao', '__icontains'),
('sigla', '__icontains')
))
class Meta:
verbose_name = _('Tipo de Matéria Legislativa')
verbose_name_plural = _('Tipos de Matérias Legislativas')
@ -60,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')))
@ -157,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'))
@ -244,6 +306,13 @@ class TipoDocumento(models.Model):
descricao = models.CharField(
max_length=50, verbose_name=_('Tipo Documento'))
tipo_proposicao = SaplGenericRelation(
TipoProposicao,
related_query_name='tipodocumento_set',
fields_search=(
('descricao', '__icontains'),
))
class Meta:
verbose_name = _('Tipo de Documento')
verbose_name_plural = _('Tipos de Documento')
@ -279,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 ??
@ -396,34 +482,8 @@ class Parecer(models.Model):
}
class TipoProposicao(models.Model):
MAT_OU_DOC_CHOICES = Choices(('M', 'materia', _('Matéria')),
('D', 'documento', _('Documento')))
descricao = models.CharField(max_length=50, verbose_name=_('Descrição'))
materia_ou_documento = models.CharField(
max_length=1, verbose_name=_('Gera'), choices=MAT_OU_DOC_CHOICES)
modelo = models.CharField(max_length=50, verbose_name=_('Modelo XML'))
# mutually exclusive (depend on materia_ou_documento)
tipo_materia = models.ForeignKey(
TipoMateriaLegislativa,
blank=True,
null=True,
verbose_name=_('Tipo Matéria'))
tipo_documento = models.ForeignKey(
TipoDocumento, blank=True, null=True, verbose_name=_('Tipo Documento'))
class Meta:
verbose_name = _('Tipo de Proposição')
verbose_name_plural = _('Tipos de Proposições')
def __str__(self):
return self.descricao
class Proposicao(models.Model):
autor = models.ForeignKey(Autor, null=True, blank=True)
autor = models.ForeignKey(Autor, null=True, blank=True, on_delete=PROTECT)
tipo = models.ForeignKey(TipoProposicao, verbose_name=_('Tipo'))
# XXX data_envio was not null, but actual data said otherwise!!!
@ -434,13 +494,34 @@ class Proposicao(models.Model):
data_devolucao = models.DateTimeField(
blank=True, null=True, verbose_name=_('Data de Devolução'))
descricao = models.TextField(max_length=100, verbose_name=_('Descrição'))
descricao = models.TextField(verbose_name=_('Descrição'))
justificativa_devolucao = models.CharField(
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,
@ -448,30 +529,67 @@ 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'))
texto_original = models.FileField(
upload_to=texto_upload_path,
blank=True,
null=True,
verbose_name=_('Texto Original'),
validators=[restringe_tipos_de_arquivo_txt])
texto_articulado = GenericRelation(
TextoArticulado, related_query_name='texto_articulado')
# Ao ser recebida, irá gerar uma nova matéria ou um documento acessorio
# 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)
texto_original = models.FileField(
upload_to=texto_upload_path,
verbose_name=_('Texto Original'),
validators=[restringe_tipos_de_arquivo_txt])
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

@ -72,7 +72,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())),

448
sapl/materia/views.py

@ -6,39 +6,47 @@ from crispy_forms.helper import FormHelper
from crispy_forms.layout import HTML
from django.contrib import messages
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist,\
PermissionDenied
from django.core.mail import send_mail
from django.core.urlresolvers import reverse
from django.db.models import Q
from django.http import JsonResponse
from django.http.response import HttpResponseRedirect
from django.http.response import HttpResponseRedirect, Http404
from django.shortcuts import redirect
from django.template import Context, loader
from django.utils import dateformat, formats
from django.utils.http import urlsafe_base64_decode
from django.utils.translation import ugettext_lazy as _
from django.views.generic import CreateView, ListView, TemplateView, UpdateView
from django.views.generic.base import RedirectView
from django.views.generic.edit import FormView
from django_filters.views import FilterView
from sapl.base.models import AppConfig, Autor, CasaLegislativa, TipoAutor
from sapl.base.models import Autor, CasaLegislativa, TipoAutor
from sapl.compilacao.views import IntegracaoTaView
from sapl.crispy_layout_mixin import SaplFormLayout, form_actions
from sapl.crud.base import (ACTION_CREATE, ACTION_DELETE, ACTION_DETAIL,
ACTION_LIST, ACTION_UPDATE, RP_DETAIL, RP_LIST,
Crud, CrudAux, CrudDetailView, MasterDetailCrud,
make_pagination)
make_pagination, PermissionRequiredForAppCrudMixin)
from sapl.materia import apps
from sapl.materia.forms import AnexadaForm, LegislacaoCitadaForm
from sapl.materia.forms import AnexadaForm, LegislacaoCitadaForm,\
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,
montar_row_autor, permission_required_for_app,
permissoes_autor, permissoes_materia,
permissoes_protocoloadm)
permissoes_protocoloadm, permission_required_for_app,
montar_row_autor)
import sapl
from .forms import (AcessorioEmLoteFilterSet, AcompanhamentoMateriaForm,
ConfirmarProposicaoForm, DocumentoAcessorioForm,
DocumentoAcessorioForm,
MateriaLegislativaFilterSet,
PrimeiraTramitacaoEmLoteFilterSet, ProposicaoForm,
PrimeiraTramitacaoEmLoteFilterSet, ProposicaoOldForm,
ReceberProposicaoForm, TramitacaoEmLoteFilterSet,
filtra_tramitacao_destino,
filtra_tramitacao_destino_and_status,
@ -50,6 +58,7 @@ from .models import (AcompanhamentoMateria, Anexada, Autoria, DespachoInicial,
TipoMateriaLegislativa, TipoProposicao, Tramitacao,
UnidadeTramitacao)
OrigemCrud = Crud.build(Origem, '')
TipoMateriaCrud = CrudAux.build(
@ -75,7 +84,7 @@ class MateriaTaView(IntegracaoTaView):
este get foi implementado para tratar uma prerrogativa externa
de usuário.
"""
if AppConfig.attr('texto_articulado_materia'):
if sapl.base.models.AppConfig.attr('texto_articulado_materia'):
return IntegracaoTaView.get(self, request, *args, **kwargs)
else:
return self.get_redirect_deactivated()
@ -84,6 +93,9 @@ class MateriaTaView(IntegracaoTaView):
class ProposicaoTaView(IntegracaoTaView):
model = Proposicao
model_type_foreignkey = TipoProposicao
# TODO implmentar o mapa de fields e utiliza-lo em IntegracaoTaView
fields = {
}
def get(self, request, *args, **kwargs):
"""
@ -91,7 +103,7 @@ class ProposicaoTaView(IntegracaoTaView):
este get foi implementado para tratar uma prerrogativa externa
de usuário.
"""
if AppConfig.attr('texto_articulado_proposicao'):
if sapl.base.models.AppConfig.attr('texto_articulado_proposicao'):
return IntegracaoTaView.get(self, request, *args, **kwargs)
else:
return self.get_redirect_deactivated()
@ -118,10 +130,25 @@ def recuperar_materia(request):
OrgaoCrud = CrudAux.build(Orgao, 'orgao')
TipoProposicaoCrud = CrudAux.build(TipoProposicao, 'tipo_proposicao')
StatusTramitacaoCrud = CrudAux.build(StatusTramitacao, 'status_tramitacao')
class TipoProposicaoCrud(CrudAux):
model = TipoProposicao
help_text = 'tipo_proposicao'
class BaseMixin(CrudAux.BaseMixin):
list_field_names = ["descricao", "conteudo", 'tipo_conteudo_related']
class CreateView(CrudAux.CreateView):
form_class = TipoProposicaoForm
layout_key = None
class UpdateView(CrudAux.UpdateView):
form_class = TipoProposicaoForm
layout_key = None
def criar_materia_proposicao(proposicao):
tipo_materia = TipoMateriaLegislativa.objects.get(
descricao=proposicao.tipo.descricao)
@ -168,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)
@ -179,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
@ -202,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
@ -225,79 +255,94 @@ 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(PermissionRequiredMixin, CreateView):
template_name = "materia/receber_proposicao.html"
class ReceberProposicao(PermissionRequiredForAppCrudMixin, FormView):
app_label = sapl.protocoloadm.apps.AppConfig.label
template_name = "crud/form.html"
form_class = ReceberProposicaoForm
permission_required = permissoes_protocoloadm()
def get_context_data(self, **kwargs):
context = super(ReceberProposicao, self).get_context_data(**kwargs)
context.update({'form': self.get_form()})
return context
def post(self, request, *args, **kwargs):
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}))
msg = 'Proposição não encontrada!'
return self.render_to_response({'form': form, 'msg': msg})
else:
return self.render_to_response({'form': form})
messages.error(request, _('Proposição não encontrada!'))
return self.form_invalid(form)
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(PermissionRequiredMixin, CreateView):
class ConfirmarProposicao(PermissionRequiredForAppCrudMixin, UpdateView):
app_label = sapl.protocoloadm.apps.AppConfig.label
template_name = "materia/confirmar_proposicao.html"
model = Proposicao
form_class = ConfirmarProposicaoForm
permission_required = permissoes_protocoloadm()
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'])
msgs = self.object.results['messages']
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'))
for key, value in msgs.items():
for item in value:
getattr(messages, key)(self.request, item)
return self.object.results['url']
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 UnidadeTramitacaoCrud(CrudAux):
@ -327,97 +372,174 @@ class UnidadeTramitacaoCrud(CrudAux):
class ProposicaoCrud(Crud):
"""
TODO: Entre outros comportamento gerais, mesmo que um usuário tenha
Perfil de Autor o Crud de proposição não deverá permitir acesso a
proposições. O acesso deve ser permitido se existe um Autor registrado
e vinculado ao usuário. Essa tarefa deve ser realizada nas Tabelas Aux.
"""
model = Proposicao
help_path = ''
container_field = 'autor__user'
class BaseMixin(Crud.BaseMixin):
list_field_names = ['data_envio', 'descricao',
'tipo', 'data_recebimento']
list_field_names = ['data_envio', 'data_recebimento', 'descricao',
'tipo']
class CreateView(Crud.CreateView):
class BaseLocalMixin:
form_class = ProposicaoForm
layout_key = None
@property
def layout_key(self):
return 'ProposicaoCreate'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['subnav_template_name'] = ''
return context
def get_initial(self):
try:
autor_id = Autor.objects.get(user=self.request.user).id
except MultipleObjectsReturned:
msg = _('Este usuário está relacionado a mais de um autor. ' +
'Operação cancelada')
messages.add_message(self.request, messages.ERROR, msg)
return redirect(self.get_success_url())
except ObjectDoesNotExist:
# FIXME: Pensar em uma melhor forma
tipo = TipoAutor.objects.get(name='Externo')
autor_id = Autor.objects.create(
user=self.request.user,
nome=str(self.request.user),
tipo=tipo).id
return {'autor': autor_id}
else:
return {'autor': autor_id}
def get(self, request, *args, **kwargs):
class UpdateView(Crud.UpdateView):
form_class = ProposicaoForm
if not self._action_is_valid(request, *args, **kwargs):
return redirect(reverse('sapl.materia:proposicao_detail',
kwargs={'pk': kwargs['pk']}))
return super().get(self, request, *args, **kwargs)
def get_initial(self):
initial = self.initial.copy()
if self.object.materia:
initial['tipo_materia'] = self.object.materia.tipo.id
initial['numero_materia'] = self.object.materia.numero
initial['ano_materia'] = self.object.materia.ano
return initial
def post(self, request, *args, **kwargs):
@property
def layout_key(self):
return 'ProposicaoCreate'
if not self._action_is_valid(request, *args, **kwargs):
return redirect(reverse('sapl.materia:proposicao_detail',
kwargs={'pk': kwargs['pk']}))
return super().post(self, request, *args, **kwargs)
def has_permission(self):
perms = self.get_permission_required()
if not self.request.user.has_perms(perms):
return False
if (Proposicao.objects.filter(
id=self.kwargs['pk'],
autor__user_id=self.request.user.id).exists()):
proposicao = Proposicao.objects.get(
id=self.kwargs['pk'],
autor__user_id=self.request.user.id)
if (not proposicao.data_recebimento or
proposicao.data_devolucao):
return True
else:
msg = _('Essa proposição já foi recebida. ' +
class DetailView(Crud.DetailView):
layout_key = 'Proposicao'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['subnav_template_name'] = ''
return context
def get(self, request, *args, **kwargs):
action = request.GET.get('action', '')
if not action:
return Crud.DetailView.get(self, request, *args, **kwargs)
p = Proposicao.objects.get(id=kwargs['pk'])
msg_error = ''
if p:
if action == 'send':
if p.data_envio and p.data_recebimento:
msg_error = _('Proposição já foi enviada e recebida.')
elif p.data_envio:
msg_error = _('Proposição já foi enviada.')
elif not p.texto_original and\
not p.texto_articulado.exists():
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, _(
'Proposição enviada com sucesso.'))
elif action == 'return':
if not p.data_envio:
msg_error = _('Proposição ainda não foi enviada.')
elif p.data_recebimento:
msg_error = _('Proposição já foi recebida, não é '
'possível retorná-la.')
else:
p.data_envio = None
p.save()
messages.success(request, _(
'Proposição Retornada com sucesso.'))
if msg_error:
messages.error(request, msg_error)
# retornar redirecionando para limpar a variavel action
return redirect(reverse('sapl.materia:proposicao_detail',
kwargs={'pk': kwargs['pk']}))
class DeleteView(BaseLocalMixin, Crud.DeleteView):
def _action_is_valid(self, request, *args, **kwargs):
proposicao = Proposicao.objects.filter(
id=kwargs['pk']).values_list(
'data_envio', 'data_recebimento')
if proposicao:
if proposicao[0][0] and proposicao[0][1]:
msg = _('Proposição já foi enviada e recebida.'
'Não pode mais ser excluida.')
elif proposicao[0][0] and not proposicao[0][1]:
msg = _('Proposição já foi enviada mas ainda não recebida '
'pelo protocolo. Use a opção Recuperar Proposição '
'para depois excluí-la.')
if proposicao[0][0] or proposicao[0][1]:
messages.error(request, msg)
return False
return True
class UpdateView(BaseLocalMixin, Crud.UpdateView):
def _action_is_valid(self, request, *args, **kwargs):
proposicao = Proposicao.objects.filter(
id=kwargs['pk']).values_list(
'data_envio', 'data_recebimento')
if proposicao:
if proposicao[0][0] and proposicao[0][1]:
msg = _('Proposição já foi enviada e recebida.'
'Não pode mais ser editada')
messages.add_message(self.request, messages.ERROR, msg)
elif proposicao[0][0] and not proposicao[0][1]:
msg = _('Proposição já foi enviada mas ainda não recebida '
'pelo protocolo. Use a opção Recuperar Proposição '
'para voltar para edição.')
if proposicao[0][0] or proposicao[0][1]:
messages.error(request, msg)
return False
return True
class DetailView(Crud.DetailView):
def get_success_url(self):
def has_permission(self):
perms = self.get_permission_required()
if not self.request.user.has_perms(perms):
return False
tipo_texto = self.request.POST.get('tipo_texto', '')
if tipo_texto == 'T':
messages.info(self.request,
_('Sempre que uma Proposição é inclusa ou '
'alterada e a opção "Texto Articulado " for '
'marcada, você será redirecionado para o '
'Texto Eletrônico. Use a opção "Editar Texto" '
'para construir seu texto.'))
return reverse('sapl.materia:proposicao_ta',
kwargs={'pk': self.object.pk})
else:
return Crud.UpdateView.get_success_url(self)
return (Proposicao.objects.filter(
id=self.kwargs['pk'],
autor__user_id=self.request.user.id).exists())
class CreateView(Crud.CreateView):
form_class = ProposicaoForm
layout_key = None
def get_context_data(self, **kwargs):
context = CrudDetailView.get_context_data(self, **kwargs)
context = super().get_context_data(**kwargs)
context['subnav_template_name'] = ''
return context
def get_success_url(self):
tipo_texto = self.request.POST.get('tipo_texto', '')
if tipo_texto == 'T':
messages.info(self.request,
_('Sempre que uma Proposição é inclusa ou '
'alterada e a opção "Texto Articulado " for '
'marcada, você será redirecionado para o '
'Texto Eletrônico. Use a opção "Editar Texto" '
'para construir seu texto.'))
return reverse('sapl.materia:proposicao_ta',
kwargs={'pk': self.object.pk})
else:
return Crud.CreateView.get_success_url(self)
class ListView(Crud.ListView):
ordering = ['-data_envio', 'descricao']
@ -427,60 +549,17 @@ class ProposicaoCrud(Crud):
if obj.data_envio is None:
obj.data_envio = 'Em elaboração...'
else:
obj.data_envio = obj.data_envio.strftime("%d/%m/%Y %H:%M")
obj.data_envio = formats.date_format(
obj.data_envio, "DATETIME_FORMAT")
if obj.data_recebimento is None:
obj.data_recebimento = 'Não recebida'
else:
obj.data_recebimento = obj.data_recebimento.strftime(
"%d/%m/%Y %H:%M")
obj.data_envio = formats.date_format(
obj.data_recebimento, "DATETIME_FORMAT")
return [self._as_row(obj) for obj in object_list]
def get_queryset(self):
# Só tem acesso as Proposicoes criadas por ele que ainda nao foram
# recebidas ou foram devolvidas
lista = Proposicao.objects.filter(
autor__user_id=self.request.user.id)
lista = lista.filter(
Q(data_recebimento__isnull=True) |
Q(data_devolucao__isnull=False))
return lista
class DeleteView(Crud.DeleteView):
def has_permission(self):
perms = self.get_permission_required()
if not self.request.user.has_perms(perms):
return False
return (Proposicao.objects.filter(
id=self.kwargs['pk'],
autor__user_id=self.request.user.id).exists())
def delete(self, request, *args, **kwargs):
proposicao = Proposicao.objects.get(id=self.kwargs['pk'])
if not proposicao.data_envio or proposicao.data_devolucao:
proposicao.delete()
return HttpResponseRedirect(
reverse('sapl.materia:proposicao_list'))
elif not proposicao.data_recebimento:
proposicao.data_envio = None
proposicao.save()
return HttpResponseRedirect(
reverse('sapl.materia:proposicao_detail',
kwargs={'pk': proposicao.pk}))
else:
msg = _('Essa proposição já foi recebida. ' +
'Não pode mais ser excluída/recuperada')
messages.add_message(self.request, messages.ERROR, msg)
return HttpResponseRedirect(
reverse('sapl.materia:proposicao_detail',
kwargs={'pk': proposicao.pk}))
class ReciboProposicaoView(TemplateView):
template_name = "materia/recibo_proposicao.html"
@ -505,6 +584,21 @@ class ReciboProposicaoView(TemplateView):
self.kwargs['pk'])})
return context
def get(self, request, *args, **kwargs):
proposicao = Proposicao.objects.get(pk=self.kwargs['pk'])
if proposicao.data_envio:
return TemplateView.get(self, request, *args, **kwargs)
if not proposicao.data_envio and not proposicao.data_devolucao:
messages.error(request, _('Não é possível gerar recebo para uma '
'Proposição ainda não enviada.'))
elif proposicao.data_devolucao:
messages.error(request, _('Não é possível gerar recibo.'))
return redirect(reverse('sapl.materia:proposicao_detail',
kwargs={'pk': proposicao.pk}))
class RelatoriaCrud(MasterDetailCrud):
model = Relatoria

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)

14
sapl/protocoloadm/migrations/0003_auto_20161009_1222.py

@ -6,6 +6,13 @@ from django.db import migrations, models
import django.db.models.deletion
def clear_field_autor_in_protocolo(apps, schema_editor):
Protocolo = apps.get_model("protocoloadm", "Protocolo")
for p in Protocolo.objects.all():
p.autor = None
p.save()
class Migration(migrations.Migration):
dependencies = [
@ -13,14 +20,17 @@ class Migration(migrations.Migration):
]
operations = [
migrations.RunPython(clear_field_autor_in_protocolo),
migrations.AlterField(
model_name='documentoadministrativo',
name='autor',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='base.Autor'),
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='base.Autor'),
),
migrations.AlterField(
model_name='protocolo',
name='autor',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='base.Autor'),
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='base.Autor'),
),
]

21
sapl/protocoloadm/migrations/0004_auto_20161023_1444.py

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-10-23 14:44
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('protocoloadm', '0003_auto_20161009_1222'),
]
operations = [
migrations.AlterField(
model_name='tramitacaoadministrativo',
name='unidade_tramitacao_destino',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='adm_tramitacoes_destino', to='materia.UnidadeTramitacao', verbose_name='Unidade Destino'),
),
]

28
sapl/protocoloadm/models.py

@ -5,8 +5,10 @@ from django.utils.translation import ugettext_lazy as _
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.materia.models import (TipoMateriaLegislativa,
UnidadeTramitacao)
from sapl.utils import RANGE_ANOS, YES_NO_CHOICES, texto_upload_path
class TipoDocumentoAdministrativo(models.Model):
@ -21,8 +23,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):
@ -97,6 +120,7 @@ class Protocolo(models.Model):
verbose_name=_('Ano do Protocolo'))
data = models.DateField()
hora = models.TimeField()
# TODO transformar campo timestamp em auto_now_add
timestamp = models.DateTimeField()
tipo_protocolo = models.PositiveIntegerField(
verbose_name=_('Tipo de Protocolo'))

9
sapl/protocoloadm/urls.py

@ -50,14 +50,19 @@ urlpatterns_protocolo = [
ProtocoloPesquisaView.as_view(), name='protocolo'),
url(r'^protocoloadm/protocolo-list$',
ProtocoloListView.as_view(), name='protocolo_list'),
url(r'^protocoloadm/(?P<pk>\d+)/(?P<ano>\d+)/protocolo-mostrar$',
ProtocoloMostrarView.as_view(), name='protocolo_mostrar'),
url(r'^protocoloadm/anular-protocolo',
AnularProtocoloAdmView.as_view(), name='anular_protocolo'),
url(r'^protocoloadm/protocolar-doc',
ProtocoloDocumentoView.as_view(), name='protocolar_doc'),
url(r'^protocoloadm/protocolar-mat',
ProtocoloMateriaView.as_view(), name='protocolar_mat'),
# FIXME estas urls com pk e ano não fazem sentido
# se vai buscar por pk não precisa de nenhuma outra informação
# mas veja, apesar de chamar de pk aqui nas urls
# usou-se dentro da view como paramentro para ano.
url(r'^protocoloadm/(?P<pk>\d+)/(?P<ano>\d+)/protocolo-mostrar$',
ProtocoloMostrarView.as_view(), name='protocolo_mostrar'),
url(r'^protocoloadm/(?P<pk>\d+)/(?P<ano>\d+)/comprovante$',
ComprovanteProtocoloView.as_view(), name='comprovante_protocolo'),
url(r'^protocoloadm/(?P<pk>\d+)/(?P<ano>\d+)/criar-documento$',

3
sapl/relatorios/views.py

@ -1,7 +1,6 @@
from datetime import datetime
from bs4 import BeautifulSoup
from django.core.exceptions import ObjectDoesNotExist
from django.http import Http404, HttpResponse
from django.utils.translation import ugettext_lazy as _
@ -20,6 +19,7 @@ from sapl.sessao.models import (ExpedienteMateria, ExpedienteSessao, Orador,
SessaoPlenariaPresenca, TipoExpediente)
from sapl.settings import STATIC_ROOT
from sapl.utils import UF
import sapl
from .templates import (pdf_capa_processo_gerar,
pdf_documento_administrativo_gerar, pdf_espelho_gerar,
@ -27,6 +27,7 @@ from .templates import (pdf_capa_processo_gerar,
pdf_ordem_dia_gerar, pdf_pauta_sessao_gerar,
pdf_protocolo_gerar, pdf_sessao_plenaria_gerar)
uf_dic = dict(UF)

6
sapl/settings.py

@ -104,8 +104,6 @@ REST_FRAMEWORK = {
),
"DEFAULT_PERMISSION_CLASSES": (
"rest_framework.permissions.IsAuthenticated",
),
"DEFAULT_PERMISSION_CLASSES": (
"sapl.api.permissions.DjangoModelPermissions",
),
"DEFAULT_AUTHENTICATION_CLASSES": (
@ -178,10 +176,12 @@ LANGUAGES = (
TIME_ZONE = 'America/Sao_Paulo'
USE_I18N = True
USE_L10N = False
USE_TZ = True
USE_TZ = False
# DATE_FORMAT = 'N j, Y'
DATE_FORMAT = 'd/m/Y'
SHORT_DATE_FORMAT = 'd/m/Y'
DATETIME_FORMAT = 'd/m/Y H:i:s'
SHORT_DATETIME_FORMAT = 'd/m/Y H:i'
DATE_INPUT_FORMATS = ('%d/%m/%Y', '%m-%d-%Y', '%Y-%m-%d')
LOCALE_PATHS = (

64
sapl/static/js/app.js

@ -104,7 +104,7 @@ function autorModal() {
'<select id="resultados" \
style="min-width: 90%; max-width:90%;" size="5"/>');
data.models.forEach(function(item, index) {
data.results.forEach(function(item, index) {
select.append($("<option>").attr('value', item.value).text(item.text));
});
@ -150,13 +150,62 @@ function autorModal() {
get_nome_autor("#id_autoria__autor");*/
}
function OptionalCustomFrontEnd() {
// Adaptações opcionais de layout com a presença de JS.
// Não implementar customizações que a funcionalidade que fique dependente.
var instance;
if (!(this instanceof OptionalCustomFrontEnd)) {
if (!instance) {
instance = new OptionalCustomFrontEnd();
}
return instance;
}
instance = this;
OptionalCustomFrontEnd = function() {
return instance;
}
instance.customCheckBoxAndRadio = function() {
$('[type=radio], [type=checkbox]').each(function() {
var _this = $(this)
var _controls = _this.closest('.controls');
_controls && _controls.find(':file').length == 0 && !_controls.hasClass('controls-radio-checkbox') && _controls.addClass('controls-radio-checkbox');
_controls.find(':file').length > 0 && _controls.addClass('controls-file');
});
}
instance.customCheckBoxAndRadioWithoutLabel = function() {
var customsFront = function() {
$('[type=radio], [type=checkbox]').each(function() {
var $controls = $(this).closest('.controls')
$controls && !$controls.hasClass('controls-radio-checkbox') && $controls.addClass('controls-radio-checkbox')
});
$('[type=radio], [type=checkbox]').each(function() {
var _this = $(this);
var _label = _this.closest('label');
if (_label.length)
return;
if (this.id)
_label = $('label[for='+this.id+']');
else {
_label = $('<label/>').insertBefore(this)
}
if (_label.length) {
/*var _controls = _label.closest('.controls');
if (!_controls.length) {
_controls = $('<div class="controls"/>').insertBefore(_label)
_controls.append(_label)
}*/
_label.addClass('checkbox-inline');
_label.prepend(_this);
_this.checkbox();
}
});
}
instance.init = function() {
this.customCheckBoxAndRadio();
this.customCheckBoxAndRadioWithoutLabel();
}
instance.init();
}
$(document).ready(function(){
@ -165,6 +214,5 @@ $(document).ready(function(){
autorModal();
initTinymce("texto-rico");
customsFront();
OptionalCustomFrontEnd();
});

17
sapl/static/styles/app.scss

@ -113,6 +113,16 @@ h6, .h6 {
}
}
.controls-file {
padding: 10px;
border: 1px solid #d6e1e5;
border-radius: 4px;
label.checkbox-inline {
margin: 0px;
display: block;
}
}
.controls-radio-checkbox {
padding: 0px;
border: 1px solid #d6e1e5;
@ -142,7 +152,7 @@ h6, .h6 {
.checkbox, .radio, .checkbox-inline, .radio-inline {
margin: 0;
&:hover {
background-color: #d6e1e5;;
background-color: #d6e1e5;
}
}
}
@ -168,8 +178,11 @@ p.control-label {
color: $legend-color;
border: 0;
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

7
sapl/templates/base/autor_form.html

@ -1,4 +1,3 @@
{% extends "crud/form.html" %}
{% load i18n %}
{% block extra_js %}
@ -40,13 +39,12 @@ $(document).ready(function(){
active('pesquisa');
if (atualizar) {
var radios = $("#div_id_autor_related .controls").html('');
data.models.forEach(function (val, index) {
data.results.forEach(function (val, index) {
var html_radio = '<div class="radio"><label><span class="icons"><span class="first-icon"></span><span class="second-icon"></span></span><input type="radio" name="autor_related" id="id_autor_related_'+index+'" value="'+val.value+'" style="display:none;">'+val.text+'</label></div>';
radios.append(html_radio);
});
if (data.models.length > 1) {
if (data.results.length > 1) {
$('input[name=autor_related]').change(function(event){
if (this.checked)
$('#id_q').val(event.target.parentElement.textContent);
@ -69,7 +67,6 @@ $(document).ready(function(){
$('input[name=autor_related]').prop('checked', 'checked');
$('input[name=autor_related]').closest('.radio').addClass('checked');
}
}
}).fail(function(data) {
active('nome', atualizar);

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

2
sapl/templates/compilacao/textoarticulado_detail.html

@ -74,7 +74,6 @@
</div>
</div>
</div>
{% comment %}
<div class="row">
<div class="col-md-12">
<div id="div_id_ementa" class="holder">
@ -83,7 +82,6 @@
</div>
</div>
</div>
{% endcomment %}
</fieldset>
{% endblock detail_content %}
{% endblock base_content %}

2
sapl/templates/compilacao/textoarticulado_list.html

@ -32,7 +32,7 @@
<td><a href="{% url 'sapl.compilacao:ta_detail' ta.pk %}">{{ ta.tipo_ta }}</a></td>
<td>{{ ta.numero }}</td>
<td>{{ ta.ano }}</td>
<td>{{ ta.data }}</td>
<td>{{ ta.data|date:"D d M Y" }}</td>
<td>{{ ta.ementa|safe }}</td>
</tr>
{% endfor %}

54
sapl/templates/crud/detail.html

@ -5,29 +5,39 @@
<div class="context-actions clearfix">
{% block actions %}
<div class="actions btn-group btn-group-sm" role="group">
{% if view.list_url %}
<a href="{{ view.list_url }}" class="btn btn-default">{% trans 'Listar' %} {{view.verbose_name_plural}}</a>
{% endif %}
{% if view.search_url %}
<a href="{{ view.search_url }}" class="btn btn-default">{% trans 'Fazer Nova Pesquisa' %}</a>
{% endif %}
{% if view.create_url %}
<a href="{{ view.create_url }}" class="btn btn-default">
{% blocktrans with verbose_name=view.verbose_name %} Adicionar {{ verbose_name }} {% endblocktrans %}
</a>
{% endif %}
</div>
{% if view.update_url or view.delete_url %}
<div class="actions btn-group pull-right" role="group">
{% if view.update_url %}
<a href="{{ view.update_url }}" class="btn btn-default">{% trans 'Editar' %}</a>
{% block sub_actions %}
<div class="actions btn-group btn-group-sm" role="group">
{% if view.list_url %}
<a href="{{ view.list_url }}" class="btn btn-default">{% trans 'Listar' %} {{view.verbose_name_plural}}</a>
{% endif %}
{% if view.search_url %}
<a href="{{ view.search_url }}" class="btn btn-default">{% trans 'Fazer Nova Pesquisa' %}</a>
{% endif %}
{% if view.delete_url %}
<a href="{{ view.delete_url }}" class="btn btn-default btn-excluir">{% trans 'Excluir' %}</a>
{% if view.create_url %}
<a href="{{ view.create_url }}" class="btn btn-default">
{% blocktrans with verbose_name=view.verbose_name %} Adicionar {{ verbose_name }} {% endblocktrans %}
</a>
{% endif %}
</div>
{% endif %}
{% endblock sub_actions %}
<div class="editons pull-right">
{% block editions %}
{% if view.update_url or view.delete_url %}
<div class="actions btn-group" role="group">
{% if view.update_url %}
<a href="{{ view.update_url }}" class="btn btn-default">{% trans 'Editar' %}</a>
{% endif %}
{% if view.delete_url %}
<a href="{{ view.delete_url }}" class="btn btn-default btn-excluir">{% trans 'Excluir' %}</a>
{% endif %}
</div>
{% endif %}
{% endblock %}
</div>
{% endblock actions %}
</div>
@ -41,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 %}
@ -77,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>

6
sapl/templates/materia/autor_form.html

@ -1,6 +0,0 @@
{% extends "base.html" %}
{% load i18n crispy_forms_tags %}
{% block base_content %}
{% crispy form helper %}
{% endblock %}

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>
<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>
{% endblock actions%}
</div>
{{block.super}}
<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>
</form>
</fieldset>
{% endblock %}

16
sapl/templates/materia/layouts.yaml

@ -74,25 +74,15 @@ Relatoria:
TipoProposicao:
{% trans 'Tipo Proposição' %}:
- descricao
- materia_ou_documento tipo_documento
- modelo
ProposicaoCreate:
{% trans 'Proposição' %}:
- tipo data_envio
- descricao
{% trans 'Materia' %}:
- tipo_materia numero_materia ano_materia
{% trans 'Complemento' %}:
- texto_original
- descricao conteudo
- tipo_conteudo_related
Proposicao:
{% trans 'Proposição' %}:
- tipo data_envio
- descricao
{% trans 'Materia' %}:
- materia
- materia_de_vinculo
{% trans 'Complemento' %}:
- texto_original

1
sapl/templates/materia/materialegislativa_form.html

@ -1,3 +1,4 @@
{% extends "crud/form.html" %}
{% load i18n %}
{% load crispy_forms_tags %}

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>

144
sapl/templates/materia/proposicao_detail.html

@ -1,31 +1,127 @@
{% extends "crud/detail.html" %}
{% load i18n %}
{% load common_tags %}
{% block actions %}
<div class="actions btn-group pull-right" role="group">
{% if proposicao.data_envio %}
{% if perms|get_change_perm:view and not object.data_recebimento %}
<a href="{{ view.update_url }}" class="btn btn-default">{% trans 'Editar Proposição' %}</a>
{% endif %}
{% load i18n common_tags %}
{% if perms|get_delete_perm:view and not object.data_recebimento %}
<a href="{{ view.delete_url }}" class="btn btn-default">{% trans 'Retornar Proposição Enviada' %}</a>
{% endif %}
{% block sub_actions %}{{block.super}}
<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 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">
<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>
{% 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>
<div class="actions btn-group" role="group">
<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 %}
{% else %}
{% if perms|get_change_perm:view and not object.data_recebimento %}
<a href="{{ view.update_url }}" class="btn btn-default">{% trans 'Enviar/Editar Proposição' %}</a>
{% endif %}
{% if perms|get_delete_perm:view and not object.data_recebimento %}
<a href="{{ view.delete_url }}" class="btn btn-default">{% trans 'Excluir Proposição' %}</a>
{% 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 %}
{% endif %}
</div>
{% endblock actions %}
{% block extra_msg %}
{% if proposicao.data_envio and not proposicao.data_recebimento %}
<b><p align="center"><a href="" onclick="window.open('{% url 'sapl.materia:recibo-proposicao' object.pk %}','Recibo','width=1100, height=600, scrollbars=yes')">[Imprimir Recibo]</a></p></b>
<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 extra_msg %}
{% endblock detail_content %}

49
sapl/templates/materia/proposicao_form.html

@ -5,35 +5,34 @@
<script type="text/javascript">
$(document).ready(function(){
if($("#id_data_envio").val() != ''){
$("#submit-id-excluir").val('Retornar proposição enviada');
}else{
$("#submit-id-excluir").val('Excluir proposição');
}
});
function disable_fields() {
$("#id_tipo_materia").attr("disabled", "disabled");
$("#id_numero_materia").attr("disabled", "disabled");
$("#id_ano_materia").attr("disabled", "disabled");
$("#id_data_envio").attr("disabled", "disabled");
}
$("input[name=tipo_texto]").change(function(event) {
if (this.value == 'D' && this.checked)
$("#div_id_texto_original").removeClass('hidden');
else if (this.value == 'D' && !this.checked)
$("#div_id_texto_original").addClass('hidden');
});
function enable_fields() {
$("#id_tipo_materia").removeAttr('disabled');
$("#id_numero_materia").removeAttr('disabled');
$("#id_ano_materia").removeAttr('disabled');
}
$("select[name=tipo_materia], input[name=numero_materia], input[name=ano_materia]").change(function(event) {
var url = '{% url 'sapl.api:materialegislativa-list'%}'
$(function () {
disable_fields();
$("#id_tipo").change(function() {
if ($('#id_tipo option:selected').text() == 'Parecer') { // parecer
enable_fields();
}else {
disable_fields();
}
var formData = {
'tipo' : $("select[name=tipo_materia]").val(),
'ano' : $("input[name=ano_materia]").val(),
'numero' : $("input[name=numero_materia]").val(),
}
if (formData.tipo == '' || formData.ano == '' || formData.numero == '')
return;
$.get(url, formData).done(function(data) {
if (data.pagination.total_entries == 1)
$(".ementa_materia").html(data.results[0].ementa).removeClass('hidden');
else
$(".ementa_materia").html('').addClass('hidden');
});
});
$("input[name=tipo_texto], select[name=tipo_materia]").trigger('change');
});
</script>
{% endblock %}

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

37
sapl/templates/materia/tipoproposicao_form.html

@ -0,0 +1,37 @@
{% extends "crud/form.html" %}
{% load i18n %}
aaa
{% block extra_js %}
<script type="text/javascript">
$(document).ready(function(){
var initial_select = $("input[name=tipo_conteudo_related]").val();
$("input[name=tipo_conteudo_related]").remove();
$('#id_conteudo').change(function(event) {
var pk = this[event.target.selectedIndex].value;
var url = '{% url 'sapl.api:model_list' 0 ''%}'
url = url.replace('0', pk) + '?pagination=False'
$.get(url).done(function(data) {
var radios = $("#div_id_tipo_conteudo_related_radio .controls").html('');
data.forEach(function (val, index) {
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);
});
});
});
$('#id_conteudo').trigger('change');
$("#div_id_tipo_conteudo_related_radio .controls").addClass('controls-radio-checkbox');
});
</script>
{% endblock %}

36
sapl/templates/sessao/presenca_ordemdia.html

@ -10,21 +10,25 @@
<form method="POST">
{% csrf_token %}
<div class="row">
<div class="col-md-6">Presença</div>
<div class="col-md-6">Parlamentar</div>
</div>
<div class="row">
<div class="col-md-12"><input type="checkbox" onClick="checkAll(this)" /> Marcar/Desmarcar Todos</div>
<div class="controls">
<div class="checkbox">
<label for="id_check_all">
<input type="checkbox" id="id_check_all" onchange="checkAll(event)" /> Marcar/Desmarcar Todos
</label>
</div>
</div>
<br>
<div class="controls">
{% for parlamentar, check in view.get_presencas_ordem %}
<div class="row">
<div class="col-md-6"><input type="checkbox" name="presenca" value="{{ parlamentar.id }}" {% if check %} checked {% endif %}/></div>
<div class="col-md-6"><label for="parlamentar">{{ parlamentar }}</label></div>
</div>
<div class="checkbox">
<label for="id_presenca_{{forloop.counter}}">
<input type="checkbox" id="id_presenca_{{forloop.counter}}" name="presenca" value="{{ parlamentar.id }}" {% if check %} checked {% endif %}/>
{{ parlamentar }}
</label>
</div>
{% endfor %}
</div>
<br />
<input type="submit" value="Salvar" class="btn btn-primary" />
@ -51,11 +55,11 @@
{% block extra_js %}
<script language="JavaScript">
function checkAll(source) {
checkboxes = document.getElementsByName('presenca');
for(var i=0, n=checkboxes.length;i<n;i++) {
checkboxes[i].checked = source.checked;
}
function checkAll(event) {
$('[name=presenca]').each(function() {
$(this).prop('checked', event.target.checked ? 'checked': null);
$(this).trigger('click');
});
}
</script>
{% endblock %}

97
sapl/utils.py

@ -4,6 +4,10 @@ from datetime import date
from functools import wraps
from unicodedata import normalize as unicodedata_normalize
import hashlib
import logging
import re
import magic
from crispy_forms.helper import FormHelper
from crispy_forms.layout import HTML, Button
@ -13,12 +17,15 @@ from django.conf import settings
from django.contrib import admin
from django.contrib.auth.decorators import user_passes_test
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.fields import GenericRelation
from django.contrib.contenttypes.fields import GenericRelation, GenericRel,\
GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import PermissionDenied, ValidationError
from django.utils.translation import ugettext_lazy as _
from floppyforms import ClearableFileInput
import magic
from sapl.crispy_layout_mixin import SaplFormLayout, form_actions, to_row
from sapl.settings import BASE_DIR
@ -94,6 +101,13 @@ def montar_helper_autor(self):
' class="btn btn-inverse">Cancelar</a>')]))
class SaplGenericForeignKey(GenericForeignKey):
def __init__(self, ct_field='content_type', fk_field='object_id', for_concrete_model=True, verbose_name=''):
super().__init__(ct_field, fk_field, for_concrete_model)
self.verbose_name = verbose_name
class SaplGenericRelation(GenericRelation):
"""
Extenção da class GenericRelation para implmentar o atributo fields_search
@ -463,3 +477,84 @@ def gerar_hash_arquivo(arquivo, pk, block_size=2**20):
break
md5.update(data)
return 'P' + md5.hexdigest() + '/' + pk
class ChoiceWithoutValidationField(forms.ChoiceField):
def validate(self, value):
if self.required and not value:
raise ValidationError(
self.error_messages['required'], code='required')
def models_with_gr_for_model(model):
return list(map(
lambda x: x.related_model,
filter(
lambda obj: obj.is_relation and
hasattr(obj, 'field') and
isinstance(obj, GenericRel),
model._meta.get_fields(include_hidden=True))
))
def generic_relations_for_model(model):
"""
Esta função retorna uma lista de tuplas de dois elementos, onde o primeiro
elemento é um model qualquer que implementa SaplGenericRelation (SGR), o
segundo elemento é uma lista de todas as SGR's que pode haver dentro do
model retornado na primeira posição da tupla.
Exemplo: No Sapl, o model Parlamentar tem apenas uma SGR para Autor.
Se no Sapl existisse apenas essa SGR, o resultado dessa função
seria:
[ #Uma Lista de tuplas
( # cada tupla com dois elementos
sapl.parlamentares.models.Parlamentar,
[<sapl.utils.SaplGenericRelation: autor>]
),
]
"""
return list(map(
lambda x: (x,
list(filter(
lambda field: (
isinstance(
field, SaplGenericRelation) and
field.related_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