diff --git a/sapl/api/forms.py b/sapl/api/forms.py index 5ac75f00f..60c4a0465 100644 --- a/sapl/api/forms.py +++ b/sapl/api/forms.py @@ -1,25 +1,15 @@ -from django.contrib.contenttypes.fields import GenericRel 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 SaplGenericRelation +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): - query = value.split(' ') if query: q = Q() @@ -30,11 +20,11 @@ class AutorChoiceFilterSet(FilterSet): order_by = [] - for gr in autores_models_generic_relations(): + 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: @@ -55,3 +45,14 @@ 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', ] diff --git a/sapl/api/serializers.py b/sapl/api/serializers.py index 1dc502450..6add3cc58 100644 --- a/sapl/api/serializers.py +++ b/sapl/api/serializers.py @@ -16,10 +16,16 @@ 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 AutorChoiceSerializer(ModelChoiceSerializer): def get_value(self, obj): return obj.id diff --git a/sapl/api/urls.py b/sapl/api/urls.py index d547bb2fb..2bcac27ff 100644 --- a/sapl/api/urls.py +++ b/sapl/api/urls.py @@ -2,7 +2,7 @@ from django.conf import settings from django.conf.urls import url, include -from sapl.api.views import AutorListView +from sapl.api.views import AutorListView, ModelChoiceView from .apps import AppConfig @@ -16,9 +16,12 @@ app_name = AppConfig.name 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+)$', + ModelChoiceView.as_view(), name='model_list'), + + ] if settings.DEBUG: diff --git a/sapl/api/views.py b/sapl/api/views.py index 419248c0f..d49f3090b 100644 --- a/sapl/api/views.py +++ b/sapl/api/views.py @@ -1,4 +1,5 @@ +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 _ @@ -10,11 +11,28 @@ from rest_framework.viewsets import ModelViewSet from sapl.api.forms import AutorChoiceFilterSet from sapl.api.serializers import ChoiceSerializer, AutorSerializer,\ - AutorChoiceSerializer + AutorChoiceSerializer, ModelChoiceSerializer from sapl.base.models import Autor, TipoAutor from sapl.utils import SaplGenericRelation, sapl_logger +class ModelChoiceView(ListAPIView): + + # FIXME aplicar permissão correta de usuário + permission_classes = (AllowAny,) + serializer_class = ModelChoiceSerializer + + def get_queryset(self): + + try: + ct = ContentType.objects.get_for_id(self.kwargs['content_type']) + + except: + raise Http404 + + return ct.model_class().objects.all() + + class AutorListView(ListAPIView): """ Listagem de Autores com filtro para autores já cadastrados @@ -60,7 +78,6 @@ class AutorListView(ListAPIView): # FIXME aplicar permissão correta de usuário permission_classes = (AllowAny,) - serializer_class = AutorSerializer queryset = Autor.objects.all() model = Autor diff --git a/sapl/base/forms.py b/sapl/base/forms.py index ccbc7d496..34cabc628 100644 --- a/sapl/base/forms.py +++ b/sapl/base/forms.py @@ -27,7 +27,8 @@ from sapl.sessao.models import SessaoPlenaria from sapl.settings import MAX_IMAGE_UPLOAD_SIZE from sapl.utils import (RANGE_ANOS, ImageThumbnailFileInput, RangeWidgetOverride, autor_label, autor_modal, - SaplGenericRelation) + SaplGenericRelation, models_with_gr_for_model, + ChoiceWithoutValidationField) from .models import AppConfig, CasaLegislativa @@ -55,31 +56,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( @@ -96,32 +72,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, diff --git a/sapl/materia/forms.py b/sapl/materia/forms.py index 8eae1e6a2..73b14b299 100644 --- a/sapl/materia/forms.py +++ b/sapl/materia/forms.py @@ -2,23 +2,29 @@ from datetime import datetime import django_filters 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,\ + Div, Field from django import forms +from django.contrib.contenttypes.fields import GenericRel +from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist, ValidationError -from django.db import models +from django.db import models, transaction from django.db.models import Max from django.forms import ModelForm from django.utils.translation import ugettext_lazy as _ 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 from sapl.norma.models import (LegislacaoCitada, NormaJuridica, TipoNormaJuridica) from sapl.parlamentares.models import Parlamentar 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) from .models import (AcompanhamentoMateria, Anexada, Autoria, DespachoInicial, DocumentoAcessorio, MateriaLegislativa, @@ -767,3 +773,80 @@ 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=True, + widget=forms.RadioSelect()) + + tipo_conteudo_related = forms.IntegerField( + widget=forms.HiddenInput()) + + 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 = Row(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 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..f1754635c --- /dev/null +++ b/sapl/materia/migrations/0057_auto_20161016_0156.py @@ -0,0 +1,38 @@ +# -*- 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 + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('materia', '0056_merge'), + ] + + operations = [ + 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/models.py b/sapl/materia/models.py index e326b7698..82381bc2f 100644 --- a/sapl/materia/models.py +++ b/sapl/materia/models.py @@ -1,4 +1,6 @@ from django.contrib.auth.models import Group +from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.models import ContentType from django.db import models from django.utils.translation import ugettext_lazy as _ from model_utils import Choices @@ -8,7 +10,8 @@ from sapl.comissoes.models import Comissao from sapl.parlamentares.models import Parlamentar from sapl.utils import (RANGE_ANOS, YES_NO_CHOICES, get_settings_auth_user_model, - restringe_tipos_de_arquivo_txt) + restringe_tipos_de_arquivo_txt, SaplGenericRelation, + SaplGenericForeignKey) EM_TRAMITACAO = [(1, 'Sim'), @@ -23,6 +26,38 @@ def grupo_autor(): return grupo.id +class TipoProposicao(models.Model): + descricao = models.CharField(max_length=50, verbose_name=_('Descrição')) + + 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') + + 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 ')) @@ -31,6 +66,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') @@ -246,6 +289,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') @@ -398,32 +448,6 @@ 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) tipo = models.ForeignKey(TipoProposicao, verbose_name=_('Tipo')) diff --git a/sapl/materia/views.py b/sapl/materia/views.py index c1411f078..5fd8349f6 100644 --- a/sapl/materia/views.py +++ b/sapl/materia/views.py @@ -29,7 +29,8 @@ from sapl.crud.base import (ACTION_CREATE, ACTION_DELETE, ACTION_DETAIL, Crud, CrudAux, CrudDetailView, MasterDetailCrud, make_pagination) from sapl.materia import apps -from sapl.materia.forms import AnexadaForm, LegislacaoCitadaForm +from sapl.materia.forms import AnexadaForm, LegislacaoCitadaForm,\ + TipoProposicaoForm 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, @@ -125,11 +126,26 @@ class ConfirmarEmailView(TemplateView): OrgaoCrud = CrudAux.build(Orgao, 'orgao') -TipoProposicaoCrud = CrudAux.build(TipoProposicao, 'tipo_proposicao') StatusTramitacaoCrud = CrudAux.build(StatusTramitacao, 'status_tramitacao') UnidadeTramitacaoCrud = CrudAux.build(UnidadeTramitacao, 'unidade_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) 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/layouts.yaml b/sapl/templates/materia/layouts.yaml index 47cb7f212..bfdff08d8 100644 --- a/sapl/templates/materia/layouts.yaml +++ b/sapl/templates/materia/layouts.yaml @@ -74,9 +74,10 @@ Relatoria: TipoProposicao: {% trans 'Tipo Proposição' %}: - - descricao - - materia_ou_documento tipo_documento - - modelo + - descricao conteudo + - tipo_conteudo_related + + ProposicaoCreate: {% trans 'Proposição' %}: diff --git a/sapl/templates/materia/tipoproposicao_form.html b/sapl/templates/materia/tipoproposicao_form.html new file mode 100644 index 000000000..333e1a185 --- /dev/null +++ b/sapl/templates/materia/tipoproposicao_form.html @@ -0,0 +1,36 @@ +{% extends "crud/form.html" %} +{% load i18n %} +aaa +{% block extra_js %} + + + +{% endblock %} diff --git a/sapl/utils.py b/sapl/utils.py index 74d4f6fd5..efe3cabb8 100644 --- a/sapl/utils.py +++ b/sapl/utils.py @@ -13,13 +13,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 -from sapl.crispy_layout_mixin import SaplFormLayout, form_actions, to_row import magic + +from sapl.crispy_layout_mixin import SaplFormLayout, form_actions, to_row from sapl.settings import BASE_DIR @@ -95,6 +97,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 @@ -462,3 +471,52 @@ 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) + ))