diff --git a/sapl/api/forms.py b/sapl/api/forms.py index fef0952c8..87e2c2990 100644 --- a/sapl/api/forms.py +++ b/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') diff --git a/sapl/api/pagination.py b/sapl/api/pagination.py index bb7096a55..47d35ee65 100644 --- a/sapl/api/pagination.py +++ b/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, + }) diff --git a/sapl/api/permissions.py b/sapl/api/permissions.py index 1149e8196..5e17d1fe1 100644 --- a/sapl/api/permissions.py +++ b/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'], + } diff --git a/sapl/api/serializers.py b/sapl/api/serializers.py index 44b6a44e6..0791ab22f 100644 --- a/sapl/api/serializers.py +++ b/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 diff --git a/sapl/api/urls.py b/sapl/api/urls.py index a23984ba7..0d197efc9 100644 --- a/sapl/api/urls.py +++ b/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\d+)/(?P\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)) ] diff --git a/sapl/api/views.py b/sapl/api/views.py index e33807454..d0d1a875d 100644 --- a/sapl/api/views.py +++ b/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 já 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', ) diff --git a/sapl/base/apps.py b/sapl/base/apps.py index be67eee9e..9c0bf33b0 100644 --- a/sapl/base/apps.py +++ b/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") diff --git a/sapl/base/forms.py b/sapl/base/forms.py index 4fcfc4ef7..46357ee00 100644 --- a/sapl/base/forms.py +++ b/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'] diff --git a/sapl/base/migrations/0028_appconfig_proposicao_incorporacao_obrigatoria.py b/sapl/base/migrations/0028_appconfig_proposicao_incorporacao_obrigatoria.py new file mode 100644 index 000000000..131aa99a7 --- /dev/null +++ b/sapl/base/migrations/0028_appconfig_proposicao_incorporacao_obrigatoria.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-10-21 14:24 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0027_auto_20161011_1624'), + ] + + operations = [ + migrations.AddField( + model_name='appconfig', + name='proposicao_incorporacao_obrigatoria', + field=models.BooleanField(choices=[('O', 'Sempre Gerar Protocolo.'), ('C', 'Perguntar se é pra gerar protocolo ao incorporar.'), ('N', 'Nunca Protocolar ao incorporar uma proposição.')], default='O', verbose_name='Regra de incorporação e protocolo'), + ), + ] diff --git a/sapl/base/migrations/0029_auto_20161021_1445.py b/sapl/base/migrations/0029_auto_20161021_1445.py new file mode 100644 index 000000000..4f01852f1 --- /dev/null +++ b/sapl/base/migrations/0029_auto_20161021_1445.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-10-21 14:45 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0028_appconfig_proposicao_incorporacao_obrigatoria'), + ] + + operations = [ + migrations.AlterField( + model_name='appconfig', + name='proposicao_incorporacao_obrigatoria', + field=models.BooleanField(choices=[('O', 'Sempre Gerar Protocolo'), ('C', 'Perguntar se é pra gerar protocolo ao incorporar'), ('N', 'Nunca Protocolar ao incorporar uma proposição')], default='O', verbose_name='Regra de incorporação de proposições e protocolo'), + ), + ] diff --git a/sapl/base/migrations/0030_auto_20161021_2017.py b/sapl/base/migrations/0030_auto_20161021_2017.py new file mode 100644 index 000000000..1aa346a40 --- /dev/null +++ b/sapl/base/migrations/0030_auto_20161021_2017.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-10-21 20:17 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0029_auto_20161021_1445'), + ] + + operations = [ + migrations.AlterField( + model_name='appconfig', + name='proposicao_incorporacao_obrigatoria', + field=models.CharField(choices=[('O', 'Sempre Gerar Protocolo'), ('C', 'Perguntar se é pra gerar protocolo ao incorporar'), ('N', 'Nunca Protocolar ao incorporar uma proposição')], default='O', max_length=1, verbose_name='Regra de incorporação de proposições e protocolo'), + ), + ] diff --git a/sapl/base/models.py b/sapl/base/models.py index b1db9e0a6..ad7b7a5aa 100644 --- a/sapl/base/models.py +++ b/sapl/base/models.py @@ -7,11 +7,12 @@ from django.contrib.contenttypes.models import ContentType from django.core import exceptions from django.db import models, router from django.db.utils import DEFAULT_DB_ALIAS -from django.utils.translation import ugettext_lazy as _ from django.utils.translation import string_concat +from django.utils.translation import ugettext_lazy as _ from sapl.utils import UF, YES_NO_CHOICES, get_settings_auth_user_model + TIPO_DOCUMENTO_ADMINISTRATIVO = (('O', _('Ostensivo')), ('R', _('Restritivo'))) @@ -84,6 +85,13 @@ class ProblemaMigracao(models.Model): class AppConfig(models.Model): + + POLITICA_PROTOCOLO_CHOICES = ( + ('O', _('Sempre Gerar Protocolo')), + ('C', _('Perguntar se é pra gerar protocolo ao incorporar')), + ('N', _('Nunca Protocolar ao incorporar uma proposição')), + ) + documentos_administrativos = models.CharField( max_length=1, verbose_name=_('Ostensivo/Restritivo'), @@ -110,6 +118,10 @@ class AppConfig(models.Model): verbose_name=_('Usar Textos Articulados para Normas'), choices=YES_NO_CHOICES, default=True) + proposicao_incorporacao_obrigatoria = models.CharField( + verbose_name=_('Regra de incorporação de proposições e protocolo'), + max_length=1, choices=POLITICA_PROTOCOLO_CHOICES, default='O') + class Meta: verbose_name = _('Configurações da Aplicação') verbose_name_plural = _('Configurações da Aplicação') @@ -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") diff --git a/sapl/base/views.py b/sapl/base/views.py index 8f0b426aa..1edcb58f4 100644 --- a/sapl/base/views.py +++ b/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 diff --git a/sapl/compilacao/forms.py b/sapl/compilacao/forms.py index 1d7ff00da..b5b7cc09c 100644 --- a/sapl/compilacao/forms.py +++ b/sapl/compilacao/forms.py @@ -631,9 +631,9 @@ class DispositivoEdicaoBasicaForm(ModelForm): self.helper = FormHelper() if not editor_type: - label_cancel = _('Ir para o Editor Sequencial') + cancel_label = _('Ir para o Editor Sequencial') self.helper.layout = SaplFormLayout( - *layout, label_cancel=label_cancel) + *layout, cancel_label=cancel_label) elif editor_type == "get_form_base": getattr(self, "actions_" + editor_type)( @@ -644,11 +644,11 @@ class DispositivoEdicaoBasicaForm(ModelForm): def actions_get_form_base(self, layout, inst, texto_articulado_do_editor=None): - label_cancel = _('Fechar') + cancel_label = _('Fechar') more = [ HTML('%s' % - label_cancel), + cancel_label), ] btns_excluir = [] @@ -860,7 +860,7 @@ class DispositivoEdicaoVigenciaForm(ModelForm): self.helper = FormHelper() self.helper.layout = SaplFormLayout( *layout, - label_cancel=_('Ir para o Editor Sequencial')) + cancel_label=_('Ir para o Editor Sequencial')) super(DispositivoEdicaoVigenciaForm, self).__init__(*args, **kwargs) @@ -951,7 +951,7 @@ class DispositivoDefinidorVigenciaForm(Form): self.helper = FormHelper() self.helper.layout = SaplFormLayout( *layout, - label_cancel=_('Ir para o Editor Sequencial')) + cancel_label=_('Ir para o Editor Sequencial')) pk = kwargs.pop('pk') super(DispositivoDefinidorVigenciaForm, self).__init__(*args, **kwargs) @@ -1090,7 +1090,7 @@ class DispositivoEdicaoAlteracaoForm(ModelForm): self.helper = FormHelper() self.helper.layout = SaplFormLayout( *layout, - label_cancel=_('Ir para o Editor Sequencial')) + cancel_label=_('Ir para o Editor Sequencial')) super(DispositivoEdicaoAlteracaoForm, self).__init__(*args, **kwargs) diff --git a/sapl/crispy_layout_mixin.py b/sapl/crispy_layout_mixin.py index 7f7aca408..51b386d9c 100644 --- a/sapl/crispy_layout_mixin.py +++ b/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('%s' % label_cancel)]) - _fields = list(to_fieldsets(fields)) + [to_row([(buttons, 12)])] + def __init__(self, *fields, cancel_label=_('Cancelar'), + save_label=_('Salvar'), actions=None): + + buttons = actions + if not buttons: + buttons = form_actions(save_label=save_label, more=[ + HTML('%s' % cancel_label) + if cancel_label else None]) + + _fields = list(to_fieldsets(fields)) + if buttons: + _fields += [to_row([(buttons, 12)])] super(SaplFormLayout, self).__init__(*_fields) @@ -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') diff --git a/sapl/crud/base.py b/sapl/crud/base.py index 55366bcc5..9992d7fd9 100644 --- a/sapl/crud/base.py +++ b/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: diff --git a/sapl/materia/forms.py b/sapl/materia/forms.py index 2a7f5bb92..b454e8b3e 100644 --- a/sapl/materia/forms.py +++ b/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 já implementados, + TipoDocumento e TipoMateriaLegislativa, que são modelos utilizados + em DocumentoAcessorio e MateriaLegislativa geradas, + por sua vez a partir de uma Proposição. + Portanto para estas duas e para outras implementações que possam surgir + possuindo com matéria prima uma Proposição, dada sua estrutura, + deverá contar também com uma implementação particular aqui no código + abaixo. + """ + 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 diff --git a/sapl/materia/migrations/0054_auto_20161009_1222.py b/sapl/materia/migrations/0054_auto_20161009_1222.py index 286ce96c8..48a12f263 100644 --- a/sapl/materia/migrations/0054_auto_20161009_1222.py +++ b/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', diff --git a/sapl/materia/migrations/0056_remove_tipo_proposicao.py b/sapl/materia/migrations/0056_remove_tipo_proposicao.py new file mode 100644 index 000000000..42d212c57 --- /dev/null +++ b/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), ] diff --git a/sapl/materia/migrations/0057_auto_20161016_0156.py b/sapl/materia/migrations/0057_auto_20161016_0156.py new file mode 100644 index 000000000..31dad187e --- /dev/null +++ b/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'), + ), + ] diff --git a/sapl/materia/migrations/0058_auto_20161016_0329.py b/sapl/materia/migrations/0058_auto_20161016_0329.py new file mode 100644 index 000000000..caacc710c --- /dev/null +++ b/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'), + ), + ] diff --git a/sapl/materia/migrations/0059_auto_20161016_1333.py b/sapl/materia/migrations/0059_auto_20161016_1333.py new file mode 100644 index 000000000..8ec92f96f --- /dev/null +++ b/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')]), + ), + ] diff --git a/sapl/materia/migrations/0060_auto_20161017_0050.py b/sapl/materia/migrations/0060_auto_20161017_0050.py new file mode 100644 index 000000000..ec0cc4d8a --- /dev/null +++ b/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'), + ), + ] diff --git a/sapl/materia/migrations/0061_auto_20161017_1655.py b/sapl/materia/migrations/0061_auto_20161017_1655.py new file mode 100644 index 000000000..cbe8a0568 --- /dev/null +++ b/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'), + ), + ] diff --git a/sapl/materia/migrations/0062_auto_20161021_1424.py b/sapl/materia/migrations/0062_auto_20161021_1424.py new file mode 100644 index 000000000..c9d8985ea --- /dev/null +++ b/sapl/materia/migrations/0062_auto_20161021_1424.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-10-21 14:24 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('materia', '0061_auto_20161017_1655'), + ] + + operations = [ + migrations.AlterField( + model_name='proposicao', + name='autor', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='base.Autor'), + ), + migrations.AlterField( + model_name='proposicao', + name='materia', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='materia_vinculada', to='materia.MateriaLegislativa', verbose_name='Matéria Vinculada'), + ), + ] diff --git a/sapl/materia/migrations/0063_auto_20161021_1445.py b/sapl/materia/migrations/0063_auto_20161021_1445.py new file mode 100644 index 000000000..4a9a8a0dd --- /dev/null +++ b/sapl/materia/migrations/0063_auto_20161021_1445.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-10-21 14:45 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('materia', '0062_auto_20161021_1424'), + ] + + operations = [ + migrations.RemoveField( + model_name='proposicao', + name='materia', + ), + migrations.AddField( + model_name='proposicao', + name='materia_vinculada', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='proposicao_set', to='materia.MateriaLegislativa', verbose_name='Matéria Vinculada'), + ), + ] diff --git a/sapl/materia/migrations/0064_auto_20161022_1405.py b/sapl/materia/migrations/0064_auto_20161022_1405.py new file mode 100644 index 000000000..e7db0ecd7 --- /dev/null +++ b/sapl/materia/migrations/0064_auto_20161022_1405.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-10-22 14:05 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('materia', '0063_auto_20161021_1445'), + ] + + operations = [ + migrations.AddField( + model_name='proposicao', + name='content_type', + field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType', verbose_name='Tipo de Material Gerado'), + ), + migrations.AddField( + model_name='proposicao', + name='materia_de_vinculo', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='proposicao_set', to='materia.MateriaLegislativa', verbose_name='Matéria anexadora'), + ), + migrations.AddField( + model_name='proposicao', + name='object_id', + field=models.PositiveIntegerField(blank=True, default=None, null=True), + ), + migrations.RemoveField( + model_name='proposicao', + name='documento_gerado', + ), + migrations.RemoveField( + model_name='proposicao', + name='materia_gerada', + ), + migrations.RemoveField( + model_name='proposicao', + name='materia_vinculada', + ), + migrations.AlterUniqueTogether( + name='proposicao', + unique_together=set([('content_type', 'object_id')]), + ), + ] diff --git a/sapl/materia/migrations/0065_auto_20161022_1411.py b/sapl/materia/migrations/0065_auto_20161022_1411.py new file mode 100644 index 000000000..ce3a060df --- /dev/null +++ b/sapl/materia/migrations/0065_auto_20161022_1411.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-10-22 14:11 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('materia', '0064_auto_20161022_1405'), + ] + + operations = [ + migrations.AlterField( + model_name='proposicao', + name='content_type', + field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType', verbose_name='Tipo de Material Gerado'), + ), + ] diff --git a/sapl/materia/migrations/0066_proposicao_ano.py b/sapl/materia/migrations/0066_proposicao_ano.py new file mode 100644 index 000000000..d7152e987 --- /dev/null +++ b/sapl/materia/migrations/0066_proposicao_ano.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-10-22 23:03 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('materia', '0065_auto_20161022_1411'), + ] + + operations = [ + migrations.AddField( + model_name='proposicao', + name='ano', + field=models.PositiveSmallIntegerField(blank=True, choices=[(2016, 2016), (2015, 2015), (2014, 2014), (2013, 2013), (2012, 2012), (2011, 2011), (2010, 2010), (2009, 2009), (2008, 2008), (2007, 2007), (2006, 2006), (2005, 2005), (2004, 2004), (2003, 2003), (2002, 2002), (2001, 2001), (2000, 2000), (1999, 1999), (1998, 1998), (1997, 1997), (1996, 1996), (1995, 1995), (1994, 1994), (1993, 1993), (1992, 1992), (1991, 1991), (1990, 1990), (1989, 1989), (1988, 1988), (1987, 1987), (1986, 1986), (1985, 1985), (1984, 1984), (1983, 1983), (1982, 1982), (1981, 1981), (1980, 1980), (1979, 1979), (1978, 1978), (1977, 1977), (1976, 1976), (1975, 1975), (1974, 1974), (1973, 1973), (1972, 1972), (1971, 1971), (1970, 1970), (1969, 1969), (1968, 1968), (1967, 1967), (1966, 1966), (1965, 1965), (1964, 1964), (1963, 1963), (1962, 1962), (1961, 1961), (1960, 1960), (1959, 1959), (1958, 1958), (1957, 1957), (1956, 1956), (1955, 1955), (1954, 1954), (1953, 1953), (1952, 1952), (1951, 1951), (1950, 1950), (1949, 1949), (1948, 1948), (1947, 1947), (1946, 1946), (1945, 1945), (1944, 1944), (1943, 1943), (1942, 1942), (1941, 1941), (1940, 1940), (1939, 1939), (1938, 1938), (1937, 1937), (1936, 1936), (1935, 1935), (1934, 1934), (1933, 1933), (1932, 1932), (1931, 1931), (1930, 1930), (1929, 1929), (1928, 1928), (1927, 1927), (1926, 1926), (1925, 1925), (1924, 1924), (1923, 1923), (1922, 1922), (1921, 1921), (1920, 1920), (1919, 1919), (1918, 1918), (1917, 1917), (1916, 1916), (1915, 1915), (1914, 1914), (1913, 1913), (1912, 1912), (1911, 1911), (1910, 1910), (1909, 1909), (1908, 1908), (1907, 1907), (1906, 1906), (1905, 1905), (1904, 1904), (1903, 1903), (1902, 1902), (1901, 1901), (1900, 1900), (1899, 1899), (1898, 1898), (1897, 1897), (1896, 1896), (1895, 1895), (1894, 1894), (1893, 1893), (1892, 1892), (1891, 1891), (1890, 1890)], default=None, null=True, verbose_name='Ano'), + ), + ] diff --git a/sapl/materia/models.py b/sapl/materia/models.py index be92e9063..910228689 100644 --- a/sapl/materia/models.py +++ b/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 nº %(numero)s de %(ano)s') % { 'tipo': self.tipo, 'numero': self.numero, 'ano': self.ano} + def save(self, force_insert=False, force_update=False, using=None, + update_fields=None): + + if not self.pk and self.texto_original: + texto_original = self.texto_original + self.texto_original = None + models.Model.save(self, force_insert=force_insert, + force_update=force_update, + using=using, + update_fields=update_fields) + self.texto_original = texto_original + + return models.Model.save(self, force_insert=force_insert, + force_update=force_update, + using=using, + update_fields=update_fields) + class Autoria(models.Model): autor = models.ForeignKey(Autor, verbose_name=_('Autor')) @@ -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): diff --git a/sapl/materia/urls.py b/sapl/materia/urls.py index c78796569..c9e6a3270 100644 --- a/sapl/materia/urls.py +++ b/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\d+)', ConfirmarProposicao.as_view(), + url(r'^proposicao/confirmar/P(?P[0-9A-Fa-f]+)/' + '(?P\d+)', ConfirmarProposicao.as_view(), name='proposicao-confirmar'), url(r'^sistema/proposicao/tipo/', include(TipoProposicaoCrud.get_urls())), diff --git a/sapl/materia/views.py b/sapl/materia/views.py index 9d5d101ec..5773e8861 100644 --- a/sapl/materia/views.py +++ b/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: + já recebidas -> data_recebimento != None + não enviadas -> data_envio == None + """ + proposicao = Proposicao.objects.get(pk=self.kwargs['pk'], + data_envio__isnull=False, + data_recebimento__isnull=True) + self.object = None + # FIXME implementar hash para texto eletrônico + hasher = gerar_hash_arquivo( + proposicao.texto_original.path, + str(proposicao.pk)) if proposicao.texto_original else None + + if hasher == 'P%s/%s' % (self.kwargs['hash'], proposicao.pk): + self.object = proposicao + except: + raise Http404() + + if not self.object: + raise Http404() + + return self.object + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['subnav_template_name'] = '' + return context class 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 só 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 diff --git a/sapl/norma/models.py b/sapl/norma/models.py index 55f07ad16..f094df55f 100644 --- a/sapl/norma/models.py +++ b/sapl/norma/models.py @@ -6,7 +6,7 @@ from model_utils import Choices from sapl.compilacao.models import TextoArticulado from sapl.materia.models import MateriaLegislativa -from sapl.utils import RANGE_ANOS, YES_NO_CHOICES +from sapl.utils import RANGE_ANOS, YES_NO_CHOICES, texto_upload_path class AssuntoNorma(models.Model): @@ -55,14 +55,6 @@ class TipoNormaJuridica(models.Model): return self.descricao -def get_norma_media_path(instance, subpath, filename): - return './sapl/norma/%s/%s/%s' % (instance, subpath, filename) - - -def texto_upload_path(instance, filename): - return get_norma_media_path(instance, instance.ano, filename) - - class NormaJuridica(models.Model): ESFERA_FEDERACAO_CHOICES = Choices( ('E', 'estadual', _('Estadual')), @@ -124,6 +116,23 @@ class NormaJuridica(models.Model): 'numero': self.numero, 'data': defaultfilters.date(self.data, "d \d\e F \d\e Y")} + def save(self, force_insert=False, force_update=False, using=None, + update_fields=None): + + if not self.pk and self.texto_integral: + texto_integral = self.texto_integral + self.texto_integral = None + models.Model.save(self, force_insert=force_insert, + force_update=force_update, + using=using, + update_fields=update_fields) + self.texto_integral = texto_integral + + return models.Model.save(self, force_insert=force_insert, + force_update=force_update, + using=using, + update_fields=update_fields) + class AssuntoNormaRelationship(models.Model): assunto = models.ForeignKey(AssuntoNorma) diff --git a/sapl/protocoloadm/migrations/0003_auto_20161009_1222.py b/sapl/protocoloadm/migrations/0003_auto_20161009_1222.py index a083fdffa..4512ea548 100644 --- a/sapl/protocoloadm/migrations/0003_auto_20161009_1222.py +++ b/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'), ), ] diff --git a/sapl/protocoloadm/migrations/0004_auto_20161023_1444.py b/sapl/protocoloadm/migrations/0004_auto_20161023_1444.py new file mode 100644 index 000000000..3900a64ff --- /dev/null +++ b/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'), + ), + ] diff --git a/sapl/protocoloadm/models.py b/sapl/protocoloadm/models.py index 667a065a9..16fa8462d 100644 --- a/sapl/protocoloadm/models.py +++ b/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')) diff --git a/sapl/protocoloadm/urls.py b/sapl/protocoloadm/urls.py index 48a615f31..7b2a1ac69 100644 --- a/sapl/protocoloadm/urls.py +++ b/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\d+)/(?P\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\d+)/(?P\d+)/protocolo-mostrar$', + ProtocoloMostrarView.as_view(), name='protocolo_mostrar'), url(r'^protocoloadm/(?P\d+)/(?P\d+)/comprovante$', ComprovanteProtocoloView.as_view(), name='comprovante_protocolo'), url(r'^protocoloadm/(?P\d+)/(?P\d+)/criar-documento$', diff --git a/sapl/relatorios/views.py b/sapl/relatorios/views.py index 2900ee9a8..e0e64d691 100644 --- a/sapl/relatorios/views.py +++ b/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) diff --git a/sapl/settings.py b/sapl/settings.py index 7f39005b4..b0a7aaf76 100644 --- a/sapl/settings.py +++ b/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 = ( diff --git a/sapl/static/js/app.js b/sapl/static/js/app.js index 380b55c9a..9b7f2297d 100644 --- a/sapl/static/js/app.js +++ b/sapl/static/js/app.js @@ -104,7 +104,7 @@ function autorModal() { ''+val.text+''; 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); diff --git a/sapl/templates/base/layouts.yaml b/sapl/templates/base/layouts.yaml index 85be02341..be1901b0f 100644 --- a/sapl/templates/base/layouts.yaml +++ b/sapl/templates/base/layouts.yaml @@ -12,7 +12,11 @@ CasaLegislativa: AppConfig: {% trans 'Configurações da Aplicação' %}: - - documentos_administrativos sequencia_numeracao painel_aberto + - documentos_administrativos painel_aberto + + {% trans 'Proposições e Protocolo' %}: + - sequencia_numeracao proposicao_incorporacao_obrigatoria + {% trans 'Textos Articulados' %}: - texto_articulado_proposicao texto_articulado_materia texto_articulado_norma diff --git a/sapl/templates/compilacao/textoarticulado_detail.html b/sapl/templates/compilacao/textoarticulado_detail.html index 9c10aaf57..7acb4a0b1 100644 --- a/sapl/templates/compilacao/textoarticulado_detail.html +++ b/sapl/templates/compilacao/textoarticulado_detail.html @@ -74,7 +74,6 @@ - {% comment %}
@@ -83,7 +82,6 @@
- {% endcomment %} {% endblock detail_content %} {% endblock base_content %} diff --git a/sapl/templates/compilacao/textoarticulado_list.html b/sapl/templates/compilacao/textoarticulado_list.html index c6e1642f1..645df8081 100644 --- a/sapl/templates/compilacao/textoarticulado_list.html +++ b/sapl/templates/compilacao/textoarticulado_list.html @@ -32,7 +32,7 @@ {{ ta.tipo_ta }} {{ ta.numero }} {{ ta.ano }} - {{ ta.data }} + {{ ta.data|date:"D d M Y" }} {{ ta.ementa|safe }} {% endfor %} diff --git a/sapl/templates/crud/detail.html b/sapl/templates/crud/detail.html index ae8108cb2..8d824b0f2 100644 --- a/sapl/templates/crud/detail.html +++ b/sapl/templates/crud/detail.html @@ -5,29 +5,39 @@
{% block actions %} -
- {% if view.list_url %} - {% trans 'Listar' %} {{view.verbose_name_plural}} - {% endif %} - {% if view.search_url %} - {% trans 'Fazer Nova Pesquisa' %} - {% endif %} - {% if view.create_url %} - - {% blocktrans with verbose_name=view.verbose_name %} Adicionar {{ verbose_name }} {% endblocktrans %} - - {% endif %} -
- {% if view.update_url or view.delete_url %} -
- {% if view.update_url %} - {% trans 'Editar' %} + + {% block sub_actions %} + +
+ {% if view.list_url %} + {% trans 'Listar' %} {{view.verbose_name_plural}} + {% endif %} + {% if view.search_url %} + {% trans 'Fazer Nova Pesquisa' %} {% endif %} - {% if view.delete_url %} - {% trans 'Excluir' %} + {% if view.create_url %} + + {% blocktrans with verbose_name=view.verbose_name %} Adicionar {{ verbose_name }} {% endblocktrans %} + {% endif %}
- {% endif %} + {% endblock sub_actions %} + +
+ {% block editions %} + {% if view.update_url or view.delete_url %} +
+ {% if view.update_url %} + {% trans 'Editar' %} + {% endif %} + {% if view.delete_url %} + {% trans 'Excluir' %} + {% endif %} +
+ {% endif %} + {% endblock %} +
+ {% endblock actions %}
@@ -41,7 +51,7 @@

{{ column.verbose_name }}

- + {% comment %}TODO Transformar os links em URLs diretamente no CRUD{% endcomment %} {% if column.text|url %} {% else %} @@ -77,7 +87,7 @@ {% if href %} {{ value }} - {% elif value != 'core.Cep.None' %} + {% elif 'None' not in value %} {{ value|safe }} {% endif %} diff --git a/sapl/templates/crud/detail_detail.html b/sapl/templates/crud/detail_detail.html index dde6abbd6..219e287df 100644 --- a/sapl/templates/crud/detail_detail.html +++ b/sapl/templates/crud/detail_detail.html @@ -80,7 +80,7 @@ {% if href %} {{ value }} - {% elif valu != 'core.Cep.None' %} + {% elif 'None' in value %} {{ value|safe }} {% endif %} diff --git a/sapl/templates/materia/autor_form.html b/sapl/templates/materia/autor_form.html deleted file mode 100644 index fb5810653..000000000 --- a/sapl/templates/materia/autor_form.html +++ /dev/null @@ -1,6 +0,0 @@ -{% extends "base.html" %} -{% load i18n crispy_forms_tags %} - -{% block base_content %} - {% crispy form helper %} -{% endblock %} diff --git a/sapl/templates/materia/confirmar_proposicao.html b/sapl/templates/materia/confirmar_proposicao.html index f81ad26dc..99f9647ff 100644 --- a/sapl/templates/materia/confirmar_proposicao.html +++ b/sapl/templates/materia/confirmar_proposicao.html @@ -1,32 +1,21 @@ -{% extends "base.html" %} -{% load i18n crispy_forms_tags %} +{% extends "materia/proposicao_form.html" %} +{% load i18n crispy_forms_tags common_tags %} {% block base_content %} - -
- Confirmar recebimento de Proposição - - - - - -
Tipo: {{proposicao.tipo}}
Autor: {{proposicao.autor}}
Descrição: {{proposicao.descricao}}
Data de Envio: {{proposicao.data_envio|date:'d/m/Y H:i:s'}}
+
+ {% block actions %}{{block.super}} +
+ {% if object.texto_articulado.exists %} + {% trans "Texto Eletrônico da Proposição" %} + {% endif %} + {% if object.texto_original %} + {% trans "Texto Original da Proposição" %} + {% endif %} +
+ {% endblock actions%} +
+ +{{block.super}} -
- {% csrf_token %} -
- -               - -
-
-
{% endblock %} diff --git a/sapl/templates/materia/layouts.yaml b/sapl/templates/materia/layouts.yaml index a2c35be9d..a5070ef86 100644 --- a/sapl/templates/materia/layouts.yaml +++ b/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 diff --git a/sapl/templates/materia/materialegislativa_form.html b/sapl/templates/materia/materialegislativa_form.html index b2bf75e8a..335350406 100644 --- a/sapl/templates/materia/materialegislativa_form.html +++ b/sapl/templates/materia/materialegislativa_form.html @@ -1,3 +1,4 @@ + {% extends "crud/form.html" %} {% load i18n %} {% load crispy_forms_tags %} diff --git a/sapl/templates/materia/prop_devolvidas_list.html b/sapl/templates/materia/prop_devolvidas_list.html index 04089432e..153c7d778 100644 --- a/sapl/templates/materia/prop_devolvidas_list.html +++ b/sapl/templates/materia/prop_devolvidas_list.html @@ -1,8 +1,6 @@ {% extends "base.html" %} {% load i18n %} -{% block sections_nav %} {% include 'materia/subnav_prop.html'%} {% endblock sections_nav %} - {% block base_content %}
Proposições Não Incorporadas diff --git a/sapl/templates/materia/prop_pendentes_list.html b/sapl/templates/materia/prop_pendentes_list.html index 825b1fad9..e07ba0ee7 100644 --- a/sapl/templates/materia/prop_pendentes_list.html +++ b/sapl/templates/materia/prop_pendentes_list.html @@ -1,8 +1,5 @@ {% extends "base.html" %} {% load i18n %} - -{% block sections_nav %} {% include 'materia/subnav_prop.html'%} {% endblock sections_nav %} - {% block base_content %}
Proposições Não Recebidas diff --git a/sapl/templates/materia/prop_recebidas_list.html b/sapl/templates/materia/prop_recebidas_list.html index 08ccfe473..4d4ff5ddb 100644 --- a/sapl/templates/materia/prop_recebidas_list.html +++ b/sapl/templates/materia/prop_recebidas_list.html @@ -1,8 +1,5 @@ {% extends "base.html" %} {% load i18n %} - -{% block sections_nav %} {% include 'materia/subnav_prop.html'%} {% endblock sections_nav %} - {% block base_content %}
Proposições Incorporadas @@ -27,10 +24,11 @@ {{ prop.descricao }} {{ prop.autor }} - {% if prop.materia_gerada %} - {{ prop.materia_gerada.tipo.sigla }} {{ prop.materia_gerada.numero }}/{{ prop.materia_gerada.ano }} - {% elif prop.documento_gerado %} - {{ prop.documento_gerado.materia.tipo.sigla }} {{ prop.documento_gerado.materia.numero }}/{{ prop.documento_gerado.materia.ano }} + {{ MateriaLegislativa.Meta}} + {% if prop.content_type.model == 'materialegislativa' %} + {{ prop.conteudo_gerado_related.tipo.sigla }} {{ prop.conteudo_gerado_related.numero }}/{{ prop.conteudo_gerado_related.ano }} + {% elif prop.content_type.model == 'documentoacessorio' %} + {{ prop.conteudo_gerado_related.materia.tipo.sigla }} {{ prop.conteudo_gerado_related.materia.numero }}/{{ prop.conteudo_gerado_related.materia.ano }} {% endif %} diff --git a/sapl/templates/materia/proposicao_detail.html b/sapl/templates/materia/proposicao_detail.html index 9a2057609..dd6f68046 100644 --- a/sapl/templates/materia/proposicao_detail.html +++ b/sapl/templates/materia/proposicao_detail.html @@ -1,31 +1,127 @@ {% extends "crud/detail.html" %} -{% load i18n %} -{% load common_tags %} - -{% block actions %} -
- {% if proposicao.data_envio %} - {% if perms|get_change_perm:view and not object.data_recebimento %} - {% trans 'Editar Proposição' %} - {% endif %} +{% load i18n common_tags %} - {% if perms|get_delete_perm:view and not object.data_recebimento %} - {% trans 'Retornar Proposição Enviada' %} - {% endif %} +{% block sub_actions %}{{block.super}} +
+ {% if object.texto_articulado.exists %} + {% trans "Texto Eletrônico" %} + {% endif %} + {% if object.texto_original %} + {% trans "Texto Original" %} + {% endif %} +
+{% endblock sub_actions%} + +{% block editions %} + + {% if object.data_envio %} + {% block editions_actions_return %} +
+ {% trans "Recibo de Envio" %} + {% if not object.data_recebimento %} + {% trans 'Retornar Proposição Enviada' %} + {% endif %} +
+ {% endblock %} + + {% else %} + + {% block editions_actions_send %} + + + + {% endblock %} + {% endif %} + +{% endblock editions %} - {% else %} - {% if perms|get_change_perm:view and not object.data_recebimento %} - {% trans 'Enviar/Editar Proposição' %} - {% endif %} - {% if perms|get_delete_perm:view and not object.data_recebimento %} - {% trans 'Excluir Proposição' %} +{% block detail_content %} + + +

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

+ +
+ +
+
+

{%field_verbose_name object 'tipo'%}

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

{%field_verbose_name object 'data_envio'%}

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

{%field_verbose_name object 'data_recebimento'%}

+
+
{{object.data_recebimento}}
+
+
+
+ {% elif object.data_envio %} +
+ +
+ {% endif %} {% endif %} - {% endif %}
-{% endblock actions %} -{% block extra_msg %} - {% if proposicao.data_envio and not proposicao.data_recebimento %} -

[Imprimir Recibo]

+ +
+
+
+

{%field_verbose_name object 'descricao'%}

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

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

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

{{msg}}

-{% endblock %} diff --git a/sapl/templates/materia/subnav_prop.html b/sapl/templates/materia/subnav_prop.html deleted file mode 100644 index 0cf82c77e..000000000 --- a/sapl/templates/materia/subnav_prop.html +++ /dev/null @@ -1,6 +0,0 @@ - diff --git a/sapl/templates/materia/subnav_prop.yaml b/sapl/templates/materia/subnav_prop.yaml new file mode 100644 index 000000000..facfe876f --- /dev/null +++ b/sapl/templates/materia/subnav_prop.yaml @@ -0,0 +1,9 @@ +{% load i18n common_tags %} +- title: {% trans 'Receber Proposição' %} + url: receber-proposicao +- title: {% trans 'Proposições Não Recebidas' %} + url: proposicao-pendente +- title: {% trans 'Proposições Não Incorporadas' %} + url: proposicao-devolvida +- title: {% trans 'Proposições Incorporadas' %} + url: proposicao-recebida diff --git a/sapl/templates/materia/tipoproposicao_form.html b/sapl/templates/materia/tipoproposicao_form.html new file mode 100644 index 000000000..5168cb29a --- /dev/null +++ b/sapl/templates/materia/tipoproposicao_form.html @@ -0,0 +1,37 @@ +{% extends "crud/form.html" %} +{% load i18n %} +aaa +{% block extra_js %} + + + +{% endblock %} diff --git a/sapl/templates/sessao/presenca_ordemdia.html b/sapl/templates/sessao/presenca_ordemdia.html index d5563ecfe..1b66b53b1 100644 --- a/sapl/templates/sessao/presenca_ordemdia.html +++ b/sapl/templates/sessao/presenca_ordemdia.html @@ -10,21 +10,25 @@
{% csrf_token %} -
-
Presença
-
Parlamentar
-
- -
-
Marcar/Desmarcar Todos
+
+
+ +
+
+
{% for parlamentar, check in view.get_presencas_ordem %} -
-
-
-
+
+ +
{% endfor %} +

@@ -51,11 +55,11 @@ {% block extra_js %} {% endblock %} diff --git a/sapl/utils.py b/sapl/utils.py index b4e6450fa..573dffb46 100644 --- a/sapl/utils.py +++ b/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')])) +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, + [] + ), + ] + """ + 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 já acomparem um arquivo, haverá dois saves, + um para armazenar toda a informação e recuperar o pk, e outro logo em + seguida para armazenar o arquivo. + """ + + filename = re.sub('[^a-zA-Z0-9]', '-', filename).strip('-').lower() + filename = re.sub('[-]+', '-', filename) + path = './sapl/%(model_name)s/%(pk)s/%(filename)s' % { + 'model_name': instance._meta.model_name, + 'pk': instance.pk, + 'filename': filename} + + return path