diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 9c8faaa2d..9ddd7a255 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -13,6 +13,7 @@ django-floppyforms==1.6.2 django-model-utils==2.5 django-sass-processor==0.4.6 djangorestframework +drfdocs easy-thumbnails==2.3 git+git://github.com/interlegis/trml2pdf.git libsass==0.11.1 diff --git a/sapl/api/__init__.py b/sapl/api/__init__.py new file mode 100644 index 000000000..9580155c5 --- /dev/null +++ b/sapl/api/__init__.py @@ -0,0 +1 @@ +default_app_config = 'sapl.api.apps.AppConfig' diff --git a/sapl/api/admin.py b/sapl/api/admin.py new file mode 100644 index 000000000..e69de29bb diff --git a/sapl/api/apps.py b/sapl/api/apps.py new file mode 100644 index 000000000..ced7e511b --- /dev/null +++ b/sapl/api/apps.py @@ -0,0 +1,8 @@ +from django import apps +from django.utils.translation import ugettext_lazy as _ + + +class AppConfig(apps.AppConfig): + name = 'sapl.api' + label = 'api' + verbose_name = _('API Rest') diff --git a/sapl/api/forms.py b/sapl/api/forms.py new file mode 100644 index 000000000..5ac75f00f --- /dev/null +++ b/sapl/api/forms.py @@ -0,0 +1,57 @@ +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 + + +class AutorChoiceFilterSet(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() + for qtext in query: + if not qtext: + continue + q_fs = Q(nome__icontains=qtext) + + order_by = [] + + for gr in autores_models_generic_relations(): + model = gr[0] + sgr = gr[1] + for item in sgr: + if item.related_model != Autor: + continue + flag_order_by = True + for field in item.fields_search: + if flag_order_by: + flag_order_by = False + order_by.append('%s__%s' % ( + item.related_query_name(), + field[0]) + ) + q_fs = q_fs | Q(**{'%s__%s%s' % ( + item.related_query_name(), + field[0], + field[1]): qtext}) + + q = q & q_fs + + if q: + queryset = queryset.filter(q).order_by(*order_by) + + return queryset diff --git a/sapl/api/migrations/__init__.py b/sapl/api/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/sapl/api/pagination.py b/sapl/api/pagination.py new file mode 100644 index 000000000..75941c1d3 --- /dev/null +++ b/sapl/api/pagination.py @@ -0,0 +1,34 @@ +from django.core.paginator import EmptyPage +from django.utils.encoding import force_text +from rest_framework import pagination +from rest_framework.response import Response + + +class StandardPagination(pagination.PageNumberPagination): + page_size = 10 + page_size_query_param = 'page_size' + max_page_size = 50 + + def get_paginated_response(self, data): + try: + previous_page_number = self.page.previous_page_number() + except EmptyPage: + previous_page_number = None + + try: + next_page_number = self.page.next_page_number() + except EmptyPage: + next_page_number = None + + return Response({ + 'pagination': { + 'previous_page': previous_page_number, + 'next_page': next_page_number, + 'start_index': self.page.start_index(), + 'end_index': self.page.end_index(), + 'total_entries': self.page.paginator.count, + 'total_pages': self.page.paginator.num_pages, + 'page': self.page.number, + }, + 'models': data, + }) diff --git a/sapl/api/permissions.py b/sapl/api/permissions.py new file mode 100644 index 000000000..1149e8196 --- /dev/null +++ b/sapl/api/permissions.py @@ -0,0 +1,17 @@ +from rest_framework.permissions import DjangoModelPermissions + + +class DjangoModelPermissions(DjangoModelPermissions): + + perms_map = { + 'GET': ['%(app_label)s.list_%(model_name)s', + '%(app_label)s.detail_%(model_name)s'], + 'OPTIONS': ['%(app_label)s.list_%(model_name)s', + '%(app_label)s.detail_%(model_name)s'], + 'HEAD': ['%(app_label)s.list_%(model_name)s', + '%(app_label)s.detail_%(model_name)s'], + 'POST': ['%(app_label)s.list_%(model_name)s'], + '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 new file mode 100644 index 000000000..1dc502450 --- /dev/null +++ b/sapl/api/serializers.py @@ -0,0 +1,43 @@ +from django.contrib.contenttypes.fields import GenericRel +from rest_framework import serializers + +from sapl.base.models import Autor +from sapl.utils import SaplGenericRelation + + +class ChoiceSerializer(serializers.Serializer): + value = serializers.SerializerMethodField() + text = serializers.SerializerMethodField() + + def get_text(self, obj): + return obj[1] + + def get_value(self, obj): + return obj[0] + + +class AutorChoiceSerializer(ChoiceSerializer): + + def get_text(self, obj): + return obj.nome + + def get_value(self, obj): + return obj.id + + class Meta: + model = Autor + fields = ['id', 'nome'] + + +class AutorObjectRelatedField(serializers.RelatedField): + + def to_representation(self, value): + return str(value) + + +class AutorSerializer(serializers.ModelSerializer): + autor_related = AutorObjectRelatedField(read_only=True) + + class Meta: + model = Autor + fields = ['id', 'tipo', 'nome', 'object_id', 'autor_related', 'user'] diff --git a/sapl/api/urls.py b/sapl/api/urls.py new file mode 100644 index 000000000..d87a5d48f --- /dev/null +++ b/sapl/api/urls.py @@ -0,0 +1,30 @@ +from django.conf import settings +from django.conf.urls import url, include + +from sapl.api.views import AutorListView + +from .apps import AppConfig + + +app_name = AppConfig.name + + +# router = DefaultRouter() + +# urlpatterns += router.urls + + +urlpatterns_api = [ + # url(r'^$', api_root), + url(r'^autor', + AutorListView.as_view(), + name='autor_list'), +] + +if settings.DEBUG: + urlpatterns_api += [ + url(r'^docs', include('rest_framework_docs.urls')), ] + +urlpatterns = [ + url(r'^api/', include(urlpatterns_api)) +] diff --git a/sapl/api/views.py b/sapl/api/views.py new file mode 100644 index 000000000..4d5ee0ca8 --- /dev/null +++ b/sapl/api/views.py @@ -0,0 +1,169 @@ + +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 IsAuthenticated, AllowAny + +from sapl.api.forms import AutorChoiceFilterSet +from sapl.api.serializers import ChoiceSerializer, AutorSerializer,\ + AutorChoiceSerializer +from sapl.base.models import Autor, TipoAutor +from sapl.utils import SaplGenericRelation, sapl_logger + + +class AutorListView(ListAPIView): + """ + Listagem de Autores com filtro para autores já cadastrados + e/ou possíveis autores. + + - tr - tipo do resultado + Prepera Lista de Autores para 3 cenários distintos + + - default = 1 + + = 1 -> para (value, text) usados geralmente + em combobox, radiobox, checkbox, etc com pesquisa básica + de Autores feita pelo django-filter + -> processo usado nas pesquisas, o mais usado. + + = 2 -> para (value, text) usados geralmente + em combobox, radiobox, checkbox, etc com pesquisa básica + de Autores mas feito para Possíveis Autores armazenados + segundo o ContentType associado ao Tipo de Autor via + relacionamento genérico. + Busca feita sem django-filter processada no get_queryset + -> processo no cadastro de autores para seleção e busca + dos possíveis autores + + = 3 -> Devolve instancias da classe Autor filtradas pelo + django-filter + + - tipo - chave primária do Tipo de Autor a ser filtrado + + - q - busca textual no nome do Autor ou em fields_search + declarados no field SaplGenericRelation das GenericFks + A busca textual acontece via django-filter com a + variável `tr` igual 1 ou 3. Em caso contrário, + o django-filter é desativado e a busca é feita + no model do ContentType associado ao tipo. + + Outros campos + """ + + TR_AUTOR_CHOICE_SERIALIZER = 1 + TR_CHOICE_SERIALIZER = 2 + TR_AUTOR_SERIALIZER = 3 + + # FIXME aplicar permissão correta de usuário + permission_classes = (AllowAny,) + serializer_class = AutorSerializer + queryset = Autor.objects.all() + model = Autor + + filter_class = AutorChoiceFilterSet + filter_backends = (DjangoFilterBackend, ) + serializer_class = AutorChoiceSerializer + + @property + def tr(self): + try: + tr = int(self.request.GET.get + ('tr', AutorListView.TR_AUTOR_CHOICE_SERIALIZER)) + + assert tr in ( + AutorListView.TR_AUTOR_CHOICE_SERIALIZER, + AutorListView.TR_CHOICE_SERIALIZER, + AutorListView.TR_AUTOR_SERIALIZER), sapl_logger.info( + _("Tipo do Resultado a ser fornecido não existe!")) + except: + return AutorListView.TR_AUTOR_CHOICE_SERIALIZER + else: + return tr + + def get(self, request, *args, **kwargs): + """ + desativa o django-filter se a busca for por possiveis autores + parametro tr = TR_CHOICE_SERIALIZER + """ + + if self.tr == AutorListView.TR_CHOICE_SERIALIZER: + self.filter_class = None + self.filter_backends = [] + self.serializer_class = ChoiceSerializer + + elif self.tr == AutorListView.TR_AUTOR_SERIALIZER: + self.serializer_class = AutorSerializer + self.permission_classes = (IsAuthenticated,) + + return ListAPIView.get(self, request, *args, **kwargs) + + def get_queryset(self): + queryset = ListAPIView.get_queryset(self) + + if self.filter_backends: + return queryset + + params = {'content_type__isnull': False} + + tipo = '' + try: + tipo = int(self.request.GET.get('tipo', '')) + if tipo: + params['id'] = tipo + except: + pass + + tipos = TipoAutor.objects.filter(**params) + + if not tipos.exists() and tipo: + raise Http404() + + r = [] + for tipo in tipos: + q = self.request.GET.get('q', '').strip() + + model_class = tipo.content_type.model_class() + + fields = list(filter( + lambda field: isinstance(field, SaplGenericRelation) and + field.related_model == Autor, + model_class._meta.get_fields(include_hidden=True))) + + """ + fields - é um array de SaplGenericRelation que deve possuir o + atributo fields_search. Verifique na documentação da classe + a estrutura de fields_search. + """ + + assert len(fields) >= 1, (_( + 'Não foi encontrado em %(model)s um atributo do tipo ' + 'SaplGenericRelation que use o model %(model_autor)s') % { + 'model': model_class._meta.verbose_name, + 'model_autor': Autor._meta.verbose_name}) + + qs = model_class.objects.all() + + q_filter = Q() + if q: + for item in fields: + if item.related_model != Autor: + continue + q_fs = Q() + for field in item.fields_search: + q_fs = q_fs | Q(**{'%s%s' % ( + field[0], + field[1]): q}) + q_filter = q_filter & q_fs + + qs = qs.filter(q_filter).distinct( + fields[0].fields_search[0][0]) + + qs = qs.order_by(fields[0].fields_search[0][0]).values_list( + 'id', fields[0].fields_search[0][0]) + r += list(qs) + + if tipos.count() > 1: + r.sort(key=lambda x: x[1].upper()) + return r diff --git a/sapl/base/forms.py b/sapl/base/forms.py index 952db3b35..8033c37f6 100644 --- a/sapl/base/forms.py +++ b/sapl/base/forms.py @@ -1,23 +1,425 @@ -import django_filters +from crispy_forms.bootstrap import FieldWithButtons, InlineRadios, StrictButton from crispy_forms.helper import FormHelper -from crispy_forms.layout import HTML, Button, Fieldset, Layout +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 from django.contrib.auth.forms import AuthenticationForm +from django.contrib.auth.models import Group +from django.contrib.auth.password_validation import validate_password +from django.contrib.contenttypes.fields import GenericRel +from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError -from django.db import models +from django.db import models, transaction from django.forms import ModelForm -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ugettext_lazy as _, string_concat +import django_filters -from sapl.crispy_layout_mixin import form_actions, to_row +from sapl.base.models import Autor, TipoAutor +from sapl.crispy_layout_mixin import (SaplFormLayout, form_actions, to_column, + to_row) 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, autor_label, autor_modal) + RangeWidgetOverride, autor_label, autor_modal, + SaplGenericRelation) from .models import AppConfig, CasaLegislativa +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')), + ('D', _('Retirar Perfil de Autor e desativar Usuário que está sendo' + ' desvinculado')), + ('X', _('Excluir Usuário')), +] + + +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( + queryset=ContentType.objects.all(), + label=TipoAutor._meta.get_field('content_type').verbose_name, + required=False) + + class Meta: + model = TipoAutor + fields = ['descricao', + 'content_type'] + + def __init__(self, *args, **kwargs): + + 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) + + 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, + label=_('Senha'), + required=False, + widget=forms.PasswordInput()) + + senha_confirma = forms.CharField( + max_length=20, + label=_('Confirmar Senha'), + required=False, + widget=forms.PasswordInput()) + + email = forms.EmailField( + required=False, + label=_('Email')) + + confirma_email = forms.EmailField( + required=False, + label=_('Confirmar Email')) + + username = forms.CharField(label=get_user_model()._meta.get_field( + 'username').verbose_name.capitalize(), + required=False, + max_length=50) + + q = forms.CharField( + max_length=50, required=False, + label='Pesquise o nome do Autor com o ' + 'tipo Selecionado e marque o escolhido.') + + autor_related = ChoiceWithoutValidationField(label='', + required=False, + widget=forms.RadioSelect()) + + action_user = forms.ChoiceField( + label=_('Usuário com acesso ao Sistema para este Autor'), + choices=ACTION_CREATE_USERS_AUTOR_CHOICE, + widget=forms.RadioSelect()) + + status_user = forms.ChoiceField( + label=_('Bloqueio do Usuário Existente'), + choices=STATUS_USER_CHOICE, + widget=forms.RadioSelect(), + required=False, + help_text=_('Se vc está trocando ou removendo o usuário deste Autor, ' + 'como o Sistema deve proceder com o usuário que está sendo' + ' desvinculado?')) + + class Meta: + model = Autor + fields = ['tipo', + 'nome', + 'cargo', + 'autor_related', + 'q', + 'action_user', + 'username'] + + def __init__(self, *args, **kwargs): + + autor_related = Div( + FieldWithButtons( + Field('q', + placeholder=_('Pesquisar por possíveis autores para ' + 'o Tipo de Autor selecionado.')), + StrictButton( + _('Filtrar'), css_class='btn-default btn-filtrar-autor', + type='button')), + + + css_class='hidden', + data_action='create', + data_application='AutorSearch', + data_field='autor_related') + + autor_select = Row(to_column(('tipo', 3)), + Div(to_column(('nome', 5)), + to_column(('cargo', 4)), css_class="div_nome_cargo"), + to_column((autor_related, 9)), + to_column((Div( + Field('autor_related'), + css_class='radiogroup-autor-related hidden'), + 12))) + + 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)), + to_column(('confirma_email', 3)), + css_class='new_user_fields hidden') + + row4 = Row(to_column((Div(InlineRadios('status_user'), + css_class='radiogroup-status hidden'), 12))) + + controle_acesso = Fieldset( + _('Controle de Acesso do Autor'), + row2, row3, row4 + ) + + self.helper = FormHelper() + self.helper.layout = SaplFormLayout(autor_select, controle_acesso) + + super(AutorForm, self).__init__(*args, **kwargs) + + self.fields['action_user'].initial = 'N' + + if self.instance.pk: + if self.instance.autor_related: + self.fields['autor_related'].choices = [ + (self.instance.autor_related.pk, + self.instance.autor_related)] + self.fields['q'].initial = self.instance.nome + + if self.instance.user: + self.fields['username'].initial = self.instance.user.username + self.fields['action_user'].initial = 'A' + self.fields['status_user'].initial = 'R' + self.fields['username'].label = string_concat( + self.fields['username'].label, + ' (', self.instance.user.username, ')') + self.fields['status_user'].label = string_concat( + self.fields['status_user'].label, + ' (', self.instance.user.username, ')') + self.fields['username'].widget.attrs.update({ + 'data': self.instance.user.username + if self.instance.user else ''}) + + self.fields['status_user'].widget.attrs.update({ + 'data': self.instance.user.username + if self.instance.user else ''}) + + def valida_igualdade(self, texto1, texto2, msg): + if texto1 != texto2: + raise ValidationError(msg) + return True + + def clean(self): + User = get_user_model() + cd = self.cleaned_data + + if 'action_user' not in cd or not cd['action_user']: + raise ValidationError(_('Informe se o Autor terá usuário ' + 'vinculado para acesso ao Sistema.')) + + if self.instance.pk and self.instance.user_id: + if self.instance.user.username != cd['username']: + if 'status_user' not in cd or not cd['status_user']: + raise ValidationError( + _('Foi trocado ou removido o usuário deste Autor, ' + 'mas não foi informado como se deve proceder com o ' + 'usuário que está sendo desvinculado?')) + + qs_user = User.objects.all() + qs_autor = Autor.objects.all() + + if self.instance.pk: + qs_autor = qs_autor.exclude(pk=self.instance.pk) + if self.instance.user: + qs_user = qs_user.exclude(pk=self.instance.user.pk) + + if cd['action_user'] == 'C': + if User.objects.filter(username=cd['username']).exists(): + raise ValidationError( + _('Já existe usuário com o username "%s". ' + 'Para utilizar esse username você deve selecionar ' + '"Associar um usuário existente".') % cd['username']) + + if ('senha' not in cd or 'senha_confirma' not in cd or + not cd['senha'] or not cd['senha_confirma']): + raise ValidationError(_( + 'A senha e sua confirmação devem ser informadas.')) + msg = _('As senhas não conferem.') + self.valida_igualdade(cd['senha'], cd['senha_confirma'], msg) + + try: + validate_password(self.cleaned_data['senha']) + except ValidationError as error: + raise ValidationError(error) + + if ('email' not in cd or 'confirma_email' not in cd or + not cd['email'] or not cd['confirma_email']): + raise ValidationError(_( + 'O email e sua confirmação devem ser informados.')) + msg = _('Os emails não conferem.') + self.valida_igualdade(cd['email'], cd['confirma_email'], msg) + + if qs_user.filter(email=cd['email']).exists(): + raise ValidationError(_('Este email já foi cadastrado.')) + + if qs_autor.filter(user__email=cd['email']).exists(): + raise ValidationError( + _('Já existe um Autor com este email.')) + + elif cd['action_user'] == 'A': + if not User.objects.filter(username=cd['username']).exists(): + raise ValidationError( + _('Não existe usuário com username "%s". ' + 'Para utilizar esse username você deve selecionar ' + '"Criar novo Usuário".') % cd['username']) + + if cd['action_user'] != 'N': + + if 'username' not in cd or not cd['username']: + raise ValidationError(_('O username deve ser informado.')) + + if qs_autor.filter(user__username=cd['username']).exists(): + raise ValidationError( + _('Já existe um Autor para este usuário.')) + + """ + 'if' não é necessário por ser campo obrigatório e o framework já + mostrar a mensagem de obrigatório junto ao campo. mas foi colocado + ainda assim para renderizar um message.danger no topo do form. + """ + if 'tipo' not in cd or not cd['tipo']: + raise ValidationError( + _('O Tipo do Autor deve ser selecionado.')) + + tipo = cd['tipo'] + + if not tipo.content_type: + if 'nome' not in cd or not cd['nome']: + raise ValidationError( + _('O Nome do Autor deve ser informado.')) + else: + if 'autor_related' not in cd or not cd['autor_related']: + raise ValidationError( + _('Um registro de %s deve ser escolhido para ser ' + 'vinculado ao cadastro de Autor') % tipo.descricao) + + if not tipo.content_type.model_class().objects.filter( + pk=cd['autor_related']).exists(): + raise ValidationError( + _('O Registro definido (%s-%s) não está na base de %s.' + ) % (cd['autor_related'], cd['q'], tipo.descricao)) + + if qs_autor.filter(object_id=cd['autor_related']).exists(): + autor = qs_autor.filter(object_id=cd['autor_related']).first() + raise ValidationError( + _('Já existe um autor Cadastrado para %s' + ) % autor.autor_related) + + return self.cleaned_data + + @transaction.atomic + def save(self, commit=False): + autor = super(AutorForm, self).save(commit) + + user_old = autor.user if autor.user_id else None + + u = None + if self.cleaned_data['action_user'] == 'A': + u = get_user_model().objects.get( + username=self.cleaned_data['username']) + if not u.is_active: + u.is_active = settings.DEBUG + u.save() + elif self.cleaned_data['action_user'] == 'C': + u = get_user_model().objects.create( + username=self.cleaned_data['username'], + email=self.cleaned_data['email']) + u.set_password(self.cleaned_data['senha']) + # Define usuário como ativo em ambiente de desenvolvimento + # pode logar sem a necessidade de passar pela validação de email + # troque par False para testar o envio de email em desenvolvimento + u.is_active = settings.DEBUG + u.save() + autor.user = u + + if not autor.tipo.content_type: + autor.content_type = None + autor.object_id = None + autor.autor_related = None + else: + autor.autor_related = autor.tipo.content_type.model_class( + ).objects.get(pk=self.cleaned_data['autor_related']) + autor.nome = str(autor.autor_related) + + autor.save() + + # FIXME melhorar captura de grupo de Autor, levando em conta, + # no mínimo, a tradução. + grupo = Group.objects.filter(name='Autor')[0] + if self.cleaned_data['action_user'] != 'N': + autor.user.groups.add(grupo) + if user_old and user_old != autor.user: + user_old.groups.remove(grupo) + + else: + if 'status_user' in self.cleaned_data and user_old: + if self.cleaned_data['status_user'] == 'X': + user_old.delete() + + elif self.cleaned_data['status_user'] == 'D': + user_old.groups.remove(grupo) + user_old.is_active = False + user_old.save() + + elif self.cleaned_data['status_user'] == 'R': + user_old.groups.remove(grupo) + elif user_old: + user_old.groups.remove(grupo) + + return autor + + class RelatorioAtasFilterSet(django_filters.FilterSet): filter_overrides = {models.DateField: { diff --git a/sapl/base/migrations/0022_auto_20161009_1222.py b/sapl/base/migrations/0022_auto_20161009_1222.py new file mode 100644 index 000000000..37794c428 --- /dev/null +++ b/sapl/base/migrations/0022_auto_20161009_1222.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-10-09 15:22 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('contenttypes', '0002_remove_content_type_name'), + ('base', '0021_auto_20161006_1019'), + ] + + operations = [ + migrations.CreateModel( + name='Autor', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('object_id', models.PositiveIntegerField(blank=True, default=None, null=True)), + ('nome', models.CharField(blank=True, max_length=50, verbose_name='Autor')), + ('cargo', models.CharField(blank=True, max_length=50)), + ('content_type', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), + ], + options={ + 'verbose_name': 'Autor', + 'verbose_name_plural': 'Autores', + }, + ), + migrations.CreateModel( + name='TipoAutor', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('descricao', models.CharField(max_length=50, verbose_name='Descrição')), + ('content_type', models.OneToOneField(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType', verbose_name='Modelo do Tipo de Autor')), + ], + options={ + 'verbose_name': 'Tipo de Autor', + 'verbose_name_plural': 'Tipos de Autor', + }, + ), + migrations.AddField( + model_name='autor', + name='tipo', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='base.TipoAutor', verbose_name='Tipo'), + ), + migrations.AddField( + model_name='autor', + name='user', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/sapl/base/migrations/0023_auto_20161009_1852.py b/sapl/base/migrations/0023_auto_20161009_1852.py new file mode 100644 index 000000000..79baef066 --- /dev/null +++ b/sapl/base/migrations/0023_auto_20161009_1852.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-10-09 21:52 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0022_auto_20161009_1222'), + ] + + operations = [ + migrations.AlterField( + model_name='tipoautor', + name='content_type', + field=models.OneToOneField(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType', verbose_name='Modelagem no SAPL'), + ), + migrations.AlterUniqueTogether( + name='autor', + unique_together=set([('content_type', 'object_id')]), + ), + ] diff --git a/sapl/base/migrations/0024_auto_20161010_1002.py b/sapl/base/migrations/0024_auto_20161010_1002.py new file mode 100644 index 000000000..0de18cad7 --- /dev/null +++ b/sapl/base/migrations/0024_auto_20161010_1002.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-10-10 13:02 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0023_auto_20161009_1852'), + ] + + operations = [ + migrations.AlterField( + model_name='autor', + name='nome', + field=models.CharField(blank=True, max_length=50, verbose_name='Nome do Autor'), + ), + migrations.AlterField( + model_name='autor', + name='tipo', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='base.TipoAutor', verbose_name='Tipo do Autor'), + ), + ] diff --git a/sapl/base/migrations/0025_tipoautor_cria_usuario.py b/sapl/base/migrations/0025_tipoautor_cria_usuario.py new file mode 100644 index 000000000..ba36e092c --- /dev/null +++ b/sapl/base/migrations/0025_tipoautor_cria_usuario.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-10-11 14:38 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0024_auto_20161010_1002'), + ] + + operations = [ + migrations.AddField( + model_name='tipoautor', + name='cria_usuario', + field=models.BooleanField(choices=[(True, 'Sim'), (False, 'Não')], default=False, help_text='Criação de Usuários víncula e libera o acesso de Autores ao sistema. Vincular um Autor a um tipo que esta opção está marcada como "Não", o Autor não terá username associado.', verbose_name='Criação de Usuários'), + ), + ] diff --git a/sapl/base/migrations/0026_remove_tipoautor_cria_usuario.py b/sapl/base/migrations/0026_remove_tipoautor_cria_usuario.py new file mode 100644 index 000000000..948427836 --- /dev/null +++ b/sapl/base/migrations/0026_remove_tipoautor_cria_usuario.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-10-11 18:08 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0025_tipoautor_cria_usuario'), + ] + + operations = [ + migrations.RemoveField( + model_name='tipoautor', + name='cria_usuario', + ), + ] diff --git a/sapl/base/migrations/0027_auto_20161011_1624.py b/sapl/base/migrations/0027_auto_20161011_1624.py new file mode 100644 index 000000000..ff6eae09d --- /dev/null +++ b/sapl/base/migrations/0027_auto_20161011_1624.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-10-11 19:24 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0026_remove_tipoautor_cria_usuario'), + ] + + operations = [ + migrations.AlterField( + model_name='autor', + name='user', + field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/sapl/base/models.py b/sapl/base/models.py index 7872f2c21..b1db9e0a6 100644 --- a/sapl/base/models.py +++ b/sapl/base/models.py @@ -10,7 +10,7 @@ from django.db.utils import DEFAULT_DB_ALIAS from django.utils.translation import ugettext_lazy as _ from django.utils.translation import string_concat -from sapl.utils import UF, YES_NO_CHOICES +from sapl.utils import UF, YES_NO_CHOICES, get_settings_auth_user_model TIPO_DOCUMENTO_ADMINISTRATIVO = (('O', _('Ostensivo')), ('R', _('Restritivo'))) @@ -132,6 +132,67 @@ class AppConfig(models.Model): 'id': self.id} +class TipoAutor(models.Model): + descricao = models.CharField(max_length=50, verbose_name=_('Descrição')) + + content_type = models.OneToOneField( + ContentType, + null=True, default=None, + verbose_name=_('Modelagem no SAPL')) + + class Meta: + verbose_name = _('Tipo de Autor') + verbose_name_plural = _('Tipos de Autor') + + def __str__(self): + return self.descricao + + +class Autor(models.Model): + + user = models.OneToOneField(get_settings_auth_user_model(), + on_delete=models.SET_NULL, + null=True) + + tipo = models.ForeignKey(TipoAutor, verbose_name=_('Tipo do Autor')) + + content_type = models.ForeignKey( + ContentType, + blank=True, null=True, default=None) + object_id = models.PositiveIntegerField( + blank=True, null=True, default=None) + autor_related = GenericForeignKey('content_type', 'object_id') + + nome = models.CharField( + max_length=50, blank=True, verbose_name=_('Nome do Autor')) + + cargo = models.CharField(max_length=50, blank=True) + + class Meta: + verbose_name = _('Autor') + verbose_name_plural = _('Autores') + unique_together = (('content_type', 'object_id'), ) + + def __str__(self): + + if self.autor_related: + return str(self.autor_related) + else: + if str(self.cargo): + return _('%(nome)s - %(cargo)s') % { + 'nome': self.nome, 'cargo': self.cargo} + else: + return str(self.nome) + """if str(self.tipo) == 'Parlamentar' and self.parlamentar: + return self.parlamentar.nome_parlamentar + elif str(self.tipo) == 'Comissao' and self.comissao: + return str(self.comissao) + elif str(self.tipo) == 'Partido' and self.partido: + return str(self.partido) + else: + """ + + def create_proxy_permissions( app_config, verbosity=2, interactive=True, using=DEFAULT_DB_ALIAS, **kwargs): diff --git a/sapl/base/urls.py b/sapl/base/urls.py index cf97c33b4..e06580cfa 100644 --- a/sapl/base/urls.py +++ b/sapl/base/urls.py @@ -3,6 +3,8 @@ from django.contrib.auth import views from django.contrib.auth.decorators import permission_required from django.views.generic.base import TemplateView +from sapl.base.views import AutorCrud, TipoAutorCrud + from .apps import AppConfig from .forms import LoginForm from .views import (AppConfigCrud, CasaLegislativaCrud, HelpView, @@ -16,19 +18,19 @@ app_name = AppConfig.name urlpatterns = [ + url(r'^sistema/autor/tipo/', include(TipoAutorCrud.get_urls())), + url(r'^sistema/autor/', include(AutorCrud.get_urls())), + url(r'^sistema/ajuda/', TemplateView.as_view(template_name='ajuda.html')), - url(r'^sistema/ajuda/(?P\w+)$', HelpView.as_view(), name='help_topic'), - url(r'^sistema/ajuda/', TemplateView.as_view(template_name='ajuda/index.html'), + url(r'^sistema/ajuda/(?P\w+)$', + HelpView.as_view(), name='help_topic'), + url(r'^sistema/ajuda/', + TemplateView.as_view(template_name='ajuda/index.html'), name='help_base'), url(r'^sistema/casa-legislativa/', include(CasaLegislativaCrud.get_urls()), name="casa_legislativa"), url(r'^sistema/app-config/', include(AppConfigCrud.get_urls())), - url(r'^login/$', views.login, { - 'template_name': 'base/login.html', 'authentication_form': LoginForm}, - name='login'), - url(r'^logout/$', views.logout, {'next_page': '/login'}, name='logout'), - # TODO mover estas telas para a app 'relatorios' url(r'^sistema/relatorios/$', TemplateView.as_view( template_name='base/relatorios_list.html')), @@ -50,8 +52,13 @@ urlpatterns = [ RelatorioAtasView.as_view(), name='atas'), + # todos os sublink s de sistema devem vir acima deste url(r'^sistema/', permission_required('base.view_tabelas_auxiliares') (TemplateView.as_view(template_name='sistema.html'))), + url(r'^login/$', views.login, { + 'template_name': 'base/login.html', 'authentication_form': LoginForm}, + name='login'), + url(r'^logout/$', views.logout, {'next_page': '/login'}, name='logout'), ] diff --git a/sapl/base/views.py b/sapl/base/views.py index b7d6378b8..89aaec991 100644 --- a/sapl/base/views.py +++ b/sapl/base/views.py @@ -1,12 +1,20 @@ +from django.conf import settings from django.contrib.auth.mixins import PermissionRequiredMixin +from django.contrib.auth.models import Group +from django.contrib.auth.tokens import default_token_generator +from django.core.mail import send_mail from django.core.urlresolvers import reverse from django.db.models import Count, Q from django.http import HttpResponseRedirect +from django.utils.encoding import force_bytes +from django.utils.http import 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 +from sapl.base.models import Autor, TipoAutor from sapl.crud.base import CrudAux from sapl.materia.models import MateriaLegislativa, TipoMateriaLegislativa from sapl.parlamentares.models import Parlamentar @@ -26,6 +34,120 @@ def get_casalegislativa(): return CasaLegislativa.objects.first() +class TipoAutorCrud(CrudAux): + model = TipoAutor + help_path = 'tipo-autor' + + class BaseMixin(CrudAux.BaseMixin): + list_field_names = ['descricao', 'content_type'] + form_class = TipoAutorForm + + +class AutorCrud(CrudAux): + model = Autor + help_path = 'autor' + + class BaseMixin(CrudAux.BaseMixin): + list_field_names = ['tipo', 'nome', 'user__username'] + + class DeleteView(CrudAux.DeleteView): + + def delete(self, *args, **kwargs): + self.object = self.get_object() + + # FIXME melhorar captura de grupo de Autor, levando em conta trad + grupo = Group.objects.filter(name='Autor')[0] + self.object.user.groups.remove(grupo) + + return CrudAux.DeleteView.delete(self, *args, **kwargs) + + class UpdateView(CrudAux.UpdateView): + layout_key = None + form_class = AutorForm + + def form_valid(self, form): + # devido a implement do form o form_valid do Crud deve ser pulado + return super(CrudAux.UpdateView, self).form_valid(form) + + def get_success_url(self): + + # FIXME try except - testar envio de emails + + pk_autor = self.object.id + try: + kwargs = {} + user = self.object.user + + if user.is_active: + return reverse('sapl.base:autor_detail', + kwargs={'pk': pk_autor}) + + kwargs['token'] = default_token_generator.make_token(user) + kwargs['uidb64'] = urlsafe_base64_encode(force_bytes(user.pk)) + assunto = "SAPL - Confirmação de Conta" + full_url = self.request.get_raw_uri() + url_base = full_url[:full_url.find('sistema') - 1] + + mensagem = ( + "Este e-mail foi utilizado para fazer cadastro no " + + "SAPL com o perfil de Autor. Agora você pode " + + "criar/editar/enviar Proposições.\n" + + "Seu nome de usuário é: " + + self.request.POST['username'] + "\n" + "Caso você não tenha feito este cadastro, por favor " + + "ignore esta mensagem. Caso tenha, clique " + + "no link abaixo\n" + url_base + + reverse('sapl.materia:confirmar_email', kwargs=kwargs)) + remetente = [settings.EMAIL_SEND_USER] + destinatario = [user.email] + send_mail(assunto, mensagem, remetente, destinatario, + fail_silently=False) + except: + pass + return reverse('sapl.base:autor_detail', + kwargs={'pk': pk_autor}) + + class CreateView(CrudAux.CreateView): + 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 get_success_url(self): + pk_autor = self.object.id + try: + # FIXME try except - testar envio de emails + kwargs = {} + user = self.object.user + kwargs['token'] = default_token_generator.make_token(user) + kwargs['uidb64'] = urlsafe_base64_encode(force_bytes(user.pk)) + assunto = "SAPL - Confirmação de Conta" + full_url = self.request.get_raw_uri() + url_base = full_url[:full_url.find('sistema') - 1] + + mensagem = ( + "Este e-mail foi utilizado para fazer cadastro no " + + "SAPL com o perfil de Autor. Agora você pode " + + "criar/editar/enviar Proposições.\n" + + "Seu nome de usuário é: " + + self.request.POST['username'] + "\n" + "Caso você não tenha feito este cadastro, por favor " + + "ignore esta mensagem. Caso tenha, clique " + + "no link abaixo\n" + url_base + + reverse('sapl.materia:confirmar_email', kwargs=kwargs)) + remetente = settings.EMAIL_SEND_USER + destinatario = [user.email] + send_mail(assunto, mensagem, remetente, destinatario, + fail_silently=False) + except: + pass + + return reverse('sapl.base:autor_detail', + kwargs={'pk': pk_autor}) + + class RelatorioAtasView(FilterView): model = SessaoPlenaria filterset_class = RelatorioAtasFilterSet diff --git a/sapl/comissoes/models.py b/sapl/comissoes/models.py index 2036c76c0..441eacba6 100644 --- a/sapl/comissoes/models.py +++ b/sapl/comissoes/models.py @@ -3,8 +3,9 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ from model_utils import Choices +from sapl.base.models import Autor from sapl.parlamentares.models import Parlamentar -from sapl.utils import YES_NO_CHOICES +from sapl.utils import YES_NO_CHOICES, SaplGenericRelation class TipoComissao(models.Model): @@ -79,12 +80,19 @@ class Comissao(models.Model): choices=YES_NO_CHOICES, verbose_name=_('Comissão Ativa?')) + autor = SaplGenericRelation(Autor, + related_query_name='comissao_set', + fields_search=( + ('nome', '__icontains'), + ('sigla', '__icontains') + )) + class Meta: verbose_name = _('Comissão') verbose_name_plural = _('Comissões') def __str__(self): - return self.nome + return self.sigla + ' - ' + self.nome class Periodo(models.Model): # PeriodoCompComissao diff --git a/sapl/compilacao/views.py b/sapl/compilacao/views.py index c125b3688..ccafcb6c2 100644 --- a/sapl/compilacao/views.py +++ b/sapl/compilacao/views.py @@ -1,7 +1,7 @@ -import logging -import sys from collections import OrderedDict from datetime import datetime, timedelta +import logging +import sys from braces.views import FormMessagesMixin from django import forms @@ -20,8 +20,8 @@ from django.shortcuts import get_object_or_404, redirect from django.utils.dateparse import parse_date from django.utils.decorators import method_decorator from django.utils.encoding import force_text -from django.utils.translation import ugettext_lazy as _ from django.utils.translation import string_concat +from django.utils.translation import ugettext_lazy as _ from django.views.generic.base import TemplateView from django.views.generic.detail import DetailView from django.views.generic.edit import (CreateView, DeleteView, FormView, @@ -47,6 +47,8 @@ from sapl.compilacao.models import (Dispositivo, Nota, from sapl.compilacao.utils import (DISPOSITIVO_SELECT_RELATED, DISPOSITIVO_SELECT_RELATED_EDIT) from sapl.crud.base import Crud, CrudListView, make_pagination +from sapl.settings import BASE_DIR + TipoNotaCrud = Crud.build(TipoNota, 'tipo_nota') TipoVideCrud = Crud.build(TipoVide, 'tipo_vide') @@ -55,7 +57,7 @@ VeiculoPublicacaoCrud = Crud.build(VeiculoPublicacao, 'veiculo_publicacao') TipoDispositivoCrud = Crud.build( TipoDispositivo, 'tipo_dispositivo') -logger = logging.getLogger(__name__) +logger = logging.getLogger(BASE_DIR.name) def get_integrations_view_names(): diff --git a/sapl/crispy_layout_mixin.py b/sapl/crispy_layout_mixin.py index e0184e0f0..7f7aca408 100644 --- a/sapl/crispy_layout_mixin.py +++ b/sapl/crispy_layout_mixin.py @@ -138,8 +138,9 @@ class CrispyLayoutFormMixin: # simply return None if there is no get_form on super pass else: - form.helper = FormHelper() - form.helper.layout = SaplFormLayout(*self.get_layout()) + if self.layout_key: + form.helper = FormHelper() + form.helper.layout = SaplFormLayout(*self.get_layout()) return form @property diff --git a/sapl/crud/base.py b/sapl/crud/base.py index 1077c55a3..694898115 100644 --- a/sapl/crud/base.py +++ b/sapl/crud/base.py @@ -10,6 +10,7 @@ from django.conf.urls import url 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.utils.decorators import classonlymethod from django.utils.encoding import force_text @@ -21,10 +22,11 @@ from django.views.generic.base import ContextMixin from django.views.generic.list import MultipleObjectMixin from sapl.crispy_layout_mixin import CrispyLayoutFormMixin, get_field_display +from sapl.settings import BASE_DIR from sapl.utils import normalize -logger = logging.getLogger(__name__) +logger = logging.getLogger(BASE_DIR.name) ACTION_LIST, ACTION_CREATE, ACTION_DETAIL, ACTION_UPDATE, ACTION_DELETE = \ 'list', 'create', 'detail', 'update', 'delete' @@ -882,9 +884,9 @@ class CrudAux(Crud): """ Checa permissão para ver qualquer dado de tabela auxiliar a permissão base.view_tabelas_auxiliares está definada class Meta - do model sapl.base.models.AppConfig que, naturalmente é um arquivo - de configuração geral e só pode ser acessado através das Tabelas - Auxiliares... Com isso o script de geração de perfis acaba que por + do model sapl.base.models.AppConfig que, naturalmente é um arquivo + de configuração geral e só pode ser acessado através das Tabelas + Auxiliares... Com isso o script de geração de perfis acaba que por criar essa permissão apenas para o perfil Operador Geral. """ permission_required = ('base.view_tabelas_auxiliares',) @@ -895,8 +897,8 @@ class CrudAux(Crud): def __init__(self, **kwargs): super().__init__(**kwargs) """ - Mantem as permissões individuais geradas pelo Crud através do - Modelo e adiciona a obrigatoriedade de permissão para view + Mantem as permissões individuais geradas pelo Crud através do + Modelo e adiciona a obrigatoriedade de permissão para view tabelas auxiliares. """ self.permission_required = self.permission_required + \ @@ -1027,10 +1029,17 @@ class MasterDetailCrud(Crud): parent_model = None if '__' in obj.parent_field: fields = obj.parent_field.split('__') - parent_model = self.model + parent_model = pm = self.model for field in fields: - parent_model = getattr( - parent_model, field).field.related_model + pm = getattr(pm, field) + if isinstance(pm.field, ForeignKey): + parent_model = getattr( + parent_model, field).field.related_model + else: + parent_model = getattr( + parent_model, field).rel.related_model + pm = parent_model + else: parent_model = getattr( self.model, obj.parent_field).field.related_model diff --git a/sapl/materia/forms.py b/sapl/materia/forms.py index 7af65be63..b41549018 100644 --- a/sapl/materia/forms.py +++ b/sapl/materia/forms.py @@ -1,18 +1,16 @@ from datetime import datetime -import django_filters from crispy_forms.helper import FormHelper from crispy_forms.layout import HTML, Button, Column, Fieldset, Layout from django import forms -from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group, User -from django.contrib.auth.password_validation import validate_password from django.core.exceptions import ObjectDoesNotExist, ValidationError -from django.db import models, transaction +from django.db import models from django.db.models import Max from django.forms import ModelForm 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.norma.models import (LegislacaoCitada, NormaJuridica, @@ -22,7 +20,7 @@ from sapl.settings import MAX_DOC_UPLOAD_SIZE from sapl.utils import (RANGE_ANOS, RangeWidgetOverride, autor_label, autor_modal) -from .models import (AcompanhamentoMateria, Anexada, Autor, Autoria, +from .models import (AcompanhamentoMateria, Anexada, Autoria, DespachoInicial, DocumentoAcessorio, MateriaLegislativa, Numeracao, Proposicao, Relatoria, TipoMateriaLegislativa, Tramitacao, UnidadeTramitacao) @@ -508,7 +506,7 @@ class MateriaLegislativaFilterSet(django_filters.FilterSet): 'data_apresentacao', 'data_publicacao', 'autoria__autor__tipo', - 'autoria__autor__partido', + # 'autoria__autor__partido', 'relatoria__parlamentar_id', 'local_origem_externa', 'tramitacao__unidade_tramitacao_destino', @@ -544,7 +542,7 @@ class MateriaLegislativaFilterSet(django_filters.FilterSet): self.filters['tipo'].label = 'Tipo de Matéria' self.filters['autoria__autor__tipo'].label = 'Tipo de Autor' - self.filters['autoria__autor__partido'].label = 'Partido do Autor' + # self.filters['autoria__autor__partido'].label = 'Partido do Autor' self.filters['relatoria__parlamentar_id'].label = 'Relatoria' row1 = to_row( @@ -566,7 +564,8 @@ class MateriaLegislativaFilterSet(django_filters.FilterSet): css_class='btn btn-primary btn-sm'), 10)]) row5 = to_row( [('autoria__autor__tipo', 6), - ('autoria__autor__partido', 6)]) + # ('autoria__autor__partido', 6) + ]) row6 = to_row( [('relatoria__parlamentar_id', 6), ('local_origem_externa', 6)]) @@ -666,122 +665,6 @@ class AutoriaForm(ModelForm): return self.cleaned_data -class AutorForm(ModelForm): - senha = forms.CharField( - max_length=20, - label=_('Senha'), - required=True, - widget=forms.PasswordInput()) - - senha_confirma = forms.CharField( - max_length=20, - label=_('Confirmar Senha'), - required=True, - widget=forms.PasswordInput()) - - confirma_email = forms.EmailField( - required=True, - label=_('Confirmar Email')) - - username = forms.CharField( - required=True, - max_length=50 - ) - - class Meta: - model = Autor - fields = ['username', - 'senha', - 'email', - 'nome', - 'tipo', - 'cargo'] - widgets = {'nome': forms.HiddenInput()} - - def valida_igualdade(self, texto1, texto2, msg): - if texto1 != texto2: - raise ValidationError(msg) - return True - - def valida_email_existente(self): - return get_user_model().objects.filter( - email=self.cleaned_data['email']).exists() - - def clean(self): - - if 'username' not in self.cleaned_data: - raise ValidationError(_('Favor informar o username')) - - if ('senha' not in self.cleaned_data or - 'senha_confirma' not in self.cleaned_data): - raise ValidationError(_('Favor informar as senhas')) - - msg = _('As senhas não conferem.') - self.valida_igualdade( - self.cleaned_data['senha'], - self.cleaned_data['senha_confirma'], - msg) - - if ('email' not in self.cleaned_data or - 'confirma_email' not in self.cleaned_data): - raise ValidationError(_('Favor informar endereços de email')) - - msg = _('Os emails não conferem.') - self.valida_igualdade( - self.cleaned_data['email'], - self.cleaned_data['confirma_email'], - msg) - - email_existente = self.valida_email_existente() - - if (Autor.objects.filter( - username=self.cleaned_data['username']).exists()): - raise ValidationError(_('Já existe um autor para este usuário')) - - if email_existente: - msg = _('Este email já foi cadastrado.') - raise ValidationError(msg) - - try: - validate_password(self.cleaned_data['senha']) - except ValidationError as error: - raise ValidationError(error) - - try: - User.objects.get( - username=self.cleaned_data['username'], - email=self.cleaned_data['email']) - except ObjectDoesNotExist: - msg = _('Este nome de usuario não está cadastrado. ' + - 'Por favor, cadastre-o no Administrador do ' + - 'Sistema antes de adicioná-lo como Autor') - raise ValidationError(msg) - - return self.cleaned_data - - @transaction.atomic - def save(self, commit=False): - - autor = super(AutorForm, self).save(commit) - - u = User.objects.get( - username=autor.username, - email=autor.email) - - u.set_password(self.cleaned_data['senha']) - u.is_active = False - u.save() - - autor.user = u - - autor.save() - - grupo = Group.objects.filter(name='Autor')[0] - u.groups.add(grupo) - - return autor - - class AcessorioEmLoteFilterSet(django_filters.FilterSet): filter_overrides = {models.DateField: { diff --git a/sapl/materia/migrations/0054_auto_20161009_1222.py b/sapl/materia/migrations/0054_auto_20161009_1222.py new file mode 100644 index 000000000..286ce96c8 --- /dev/null +++ b/sapl/materia/migrations/0054_auto_20161009_1222.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-10-09 15:22 +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'), + ('materia', '0053_auto_20161004_1854'), + ] + + operations = [ + migrations.RemoveField( + model_name='autor', + name='comissao', + ), + migrations.RemoveField( + model_name='autor', + name='parlamentar', + ), + migrations.RemoveField( + model_name='autor', + name='partido', + ), + migrations.RemoveField( + model_name='autor', + name='tipo', + ), + migrations.RemoveField( + model_name='autor', + name='user', + ), + migrations.AlterField( + model_name='autoria', + 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'), + ), + migrations.DeleteModel( + name='Autor', + ), + migrations.DeleteModel( + name='TipoAutor', + ), + ] diff --git a/sapl/materia/migrations/0055_auto_20161009_1418.py b/sapl/materia/migrations/0055_auto_20161009_1418.py new file mode 100644 index 000000000..8168ab4e4 --- /dev/null +++ b/sapl/materia/migrations/0055_auto_20161009_1418.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-10-09 17:18 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0022_auto_20161009_1222'), + ('materia', '0054_auto_20161009_1222'), + ] + + operations = [ + migrations.AddField( + model_name='materialegislativa', + name='autores', + field=models.ManyToManyField(through='materia.Autoria', to='base.Autor'), + ), + migrations.AlterField( + model_name='autoria', + name='autor', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='base.Autor', verbose_name='Autor'), + ), + migrations.AlterField( + model_name='autoria', + name='materia', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='materia.MateriaLegislativa', verbose_name='Matéria Legislativa'), + ), + ] diff --git a/sapl/materia/migrations/0056_merge.py b/sapl/materia/migrations/0056_merge.py new file mode 100644 index 000000000..895977296 --- /dev/null +++ b/sapl/materia/migrations/0056_merge.py @@ -0,0 +1,16 @@ +# -*- 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 + + +class Migration(migrations.Migration): + + dependencies = [ + ('materia', '0055_auto_20161009_1418'), + ('materia', '0054_auto_20161011_0904'), + ] + + operations = [ + ] diff --git a/sapl/materia/models.py b/sapl/materia/models.py index baedf7248..e326b7698 100644 --- a/sapl/materia/models.py +++ b/sapl/materia/models.py @@ -3,8 +3,9 @@ from django.db import models 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.parlamentares.models import Parlamentar, Partido +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) @@ -143,6 +144,12 @@ class MateriaLegislativa(models.Model): verbose_name=_('Texto Original (PDF)'), validators=[restringe_tipos_de_arquivo_txt]) + autores = models.ManyToManyField( + Autor, + through='Autoria', + through_fields=('materia', 'autor'), + symmetrical=False,) + class Meta: verbose_name = _('Matéria Legislativa') verbose_name_plural = _('Matérias Legislativas') @@ -153,6 +160,22 @@ class MateriaLegislativa(models.Model): 'tipo': self.tipo, 'numero': self.numero, 'ano': self.ano} +class Autoria(models.Model): + autor = models.ForeignKey(Autor, verbose_name=_('Autor')) + materia = models.ForeignKey( + MateriaLegislativa, verbose_name=_('Matéria Legislativa')) + primeiro_autor = models.BooleanField(verbose_name=_('Primeiro Autor'), + choices=YES_NO_CHOICES) + + class Meta: + verbose_name = _('Autoria') + verbose_name_plural = _('Autorias') + + def __str__(self): + return _('%(autor)s - %(materia)s') % { + 'autor': self.autor, 'materia': self.materia} + + class AcompanhamentoMateria(models.Model): usuario = models.CharField(max_length=50) materia = models.ForeignKey(MateriaLegislativa) @@ -204,68 +227,6 @@ class AssuntoMateria(models.Model): return self.assunto -class TipoAutor(models.Model): - descricao = models.CharField(max_length=50, verbose_name=_('Descrição')) - - class Meta: - verbose_name = _('Tipo de Autor') - verbose_name_plural = _('Tipos de Autor') - - def __str__(self): - return self.descricao - - -class Autor(models.Model): - user = models.ForeignKey( - get_settings_auth_user_model(), blank=True, null=True) - partido = models.ForeignKey(Partido, blank=True, null=True) - comissao = models.ForeignKey(Comissao, blank=True, null=True) - parlamentar = models.ForeignKey(Parlamentar, blank=True, null=True) - tipo = models.ForeignKey(TipoAutor, verbose_name=_('Tipo')) - nome = models.CharField( - max_length=50, blank=True, verbose_name=_('Autor')) - cargo = models.CharField(max_length=50, blank=True) - username = models.CharField( - max_length=50, - blank=True, - verbose_name=_('Nome de Usuário')) - email = models.EmailField( - verbose_name=_('Email')) - - class Meta: - verbose_name = _('Autor') - verbose_name_plural = _('Autores') - - def __str__(self): - if str(self.tipo) == 'Parlamentar' and self.parlamentar: - return self.parlamentar.nome_parlamentar - elif str(self.tipo) == 'Comissao' and self.comissao: - return str(self.comissao) - elif str(self.tipo) == 'Partido' and self.partido: - return str(self.partido) - else: - if str(self.cargo): - return _('%(nome)s - %(cargo)s') % { - 'nome': self.nome, 'cargo': self.cargo} - else: - return str(self.nome) - - -class Autoria(models.Model): - autor = models.ForeignKey(Autor, verbose_name=_('Autor')) - materia = models.ForeignKey(MateriaLegislativa) - primeiro_autor = models.BooleanField(verbose_name=_('Primeiro Autor'), - choices=YES_NO_CHOICES) - - class Meta: - verbose_name = _('Autoria') - verbose_name_plural = _('Autorias') - - def __str__(self): - return _('%(autor)s - %(materia)s') % { - 'autor': self.autor, 'materia': self.materia} - - class DespachoInicial(models.Model): # TODO M2M? materia = models.ForeignKey(MateriaLegislativa) diff --git a/sapl/materia/tests/test_materia.py b/sapl/materia/tests/test_materia.py index c7ca7d721..ed6ae1ccd 100644 --- a/sapl/materia/tests/test_materia.py +++ b/sapl/materia/tests/test_materia.py @@ -1,14 +1,15 @@ -import pytest from django.contrib.auth import get_user_model from django.core.files.uploadedfile import SimpleUploadedFile from django.core.urlresolvers import reverse from model_mommy import mommy +import pytest +from sapl.base.models import TipoAutor, Autor from sapl.comissoes.models import Comissao, TipoComissao -from sapl.materia.models import (Anexada, Autor, Autoria, DespachoInicial, +from sapl.materia.models import (Anexada, Autoria, DespachoInicial, DocumentoAcessorio, MateriaLegislativa, Numeracao, Proposicao, RegimeTramitacao, - StatusTramitacao, TipoAutor, TipoDocumento, + StatusTramitacao, TipoDocumento, TipoMateriaLegislativa, TipoProposicao, Tramitacao, UnidadeTramitacao) from sapl.norma.models import (LegislacaoCitada, NormaJuridica, diff --git a/sapl/materia/urls.py b/sapl/materia/urls.py index 5f943be9d..a6f85016a 100644 --- a/sapl/materia/urls.py +++ b/sapl/materia/urls.py @@ -3,7 +3,7 @@ from django.conf.urls import include, url from sapl.materia.views import (AcompanhamentoConfirmarView, AcompanhamentoExcluirView, AcompanhamentoMateriaView, AnexadaCrud, - AutorCrud, AutoriaCrud, ConfirmarEmailView, + AutoriaCrud, ConfirmarEmailView, ConfirmarProposicao, DespachoInicialCrud, DocumentoAcessorioCrud, DocumentoAcessorioEmLoteView, @@ -15,11 +15,11 @@ from sapl.materia.views import (AcompanhamentoConfirmarView, ProposicaoRecebida, ProposicaoTaView, ReceberProposicao, ReciboProposicaoView, RegimeTramitacaoCrud, RelatoriaCrud, - StatusTramitacaoCrud, TipoAutorCrud, - TipoDocumentoCrud, TipoFimRelatoriaCrud, - TipoMateriaCrud, TipoProposicaoCrud, - TramitacaoCrud, TramitacaoEmLoteView, - UnidadeTramitacaoCrud, recuperar_materia) + StatusTramitacaoCrud, TipoDocumentoCrud, + TipoFimRelatoriaCrud, TipoMateriaCrud, + TipoProposicaoCrud, TramitacaoCrud, + TramitacaoEmLoteView, UnidadeTramitacaoCrud, + recuperar_materia) from .apps import AppConfig @@ -78,6 +78,8 @@ urlpatterns_proposicao = [ name='proposicao-devolvida'), url(r'^proposicao/confirmar/(?P\d+)', ConfirmarProposicao.as_view(), name='proposicao-confirmar'), + url(r'^sistema/proposicao/tipo/', + include(TipoProposicaoCrud.get_urls())), url(r'^proposicao/(?P[0-9]+)/ta$', ProposicaoTaView.as_view(), name='proposicao_ta'), @@ -89,7 +91,6 @@ urlpatterns_sistema = [ url(r'^sistema/materia/tipo/', include(TipoMateriaCrud.get_urls())), url(r'^sistema/materia/regime-tramitacao/', include(RegimeTramitacaoCrud.get_urls())), - url(r'^sistema/materia/tipo-autor/', include(TipoAutorCrud.get_urls())), url(r'^sistema/materia/tipo-documento/', include(TipoDocumentoCrud.get_urls())), url(r'^sistema/materia/tipo-fim-relatoria/', @@ -97,7 +98,6 @@ urlpatterns_sistema = [ url(r'^sistema/materia/unidade-tramitacao/', include(UnidadeTramitacaoCrud.get_urls())), url(r'^sistema/materia/origem/', include(OrigemCrud.get_urls())), - url(r'^sistema/materia/autor/', include(AutorCrud.get_urls())), url(r'^sistema/materia/status-tramitacao/', include(StatusTramitacaoCrud.get_urls())), url(r'^sistema/materia/orgao/', include(OrgaoCrud.get_urls())), diff --git a/sapl/materia/views.py b/sapl/materia/views.py index 083b3ede1..e1604a74c 100644 --- a/sapl/materia/views.py +++ b/sapl/materia/views.py @@ -3,12 +3,10 @@ from random import choice from string import ascii_letters, digits from crispy_forms.helper import FormHelper -from crispy_forms.layout import HTML, Button -from django.conf import settings +from crispy_forms.layout import HTML from django.contrib import messages from django.contrib.auth import get_user_model from django.contrib.auth.mixins import PermissionRequiredMixin -from django.contrib.auth.tokens import default_token_generator from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist from django.core.mail import send_mail from django.core.urlresolvers import reverse @@ -17,16 +15,15 @@ from django.http import JsonResponse from django.http.response import HttpResponseRedirect from django.shortcuts import redirect from django.template import Context, loader -from django.utils.encoding import force_bytes -from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode +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_filters.views import FilterView -from sapl.base.models import AppConfig, CasaLegislativa +from sapl.base.models import AppConfig, Autor, CasaLegislativa, TipoAutor from sapl.compilacao.views import IntegracaoTaView -from sapl.crispy_layout_mixin import SaplFormLayout, form_actions, to_row +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, @@ -37,23 +34,24 @@ 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, permissoes_autor, permissoes_materia, - permissoes_protocoloadm, permission_required_for_app) + permissoes_protocoloadm, permission_required_for_app, + montar_row_autor) + from .forms import (AcessorioEmLoteFilterSet, AcompanhamentoMateriaForm, - AutorForm, ConfirmarProposicaoForm, DocumentoAcessorioForm, + ConfirmarProposicaoForm, DocumentoAcessorioForm, MateriaLegislativaFilterSet, PrimeiraTramitacaoEmLoteFilterSet, ProposicaoForm, ReceberProposicaoForm, TramitacaoEmLoteFilterSet, filtra_tramitacao_destino, filtra_tramitacao_destino_and_status, filtra_tramitacao_status) -from .models import (AcompanhamentoMateria, Anexada, Autor, Autoria, - DespachoInicial, DocumentoAcessorio, MateriaLegislativa, - Numeracao, Orgao, Origem, Proposicao, RegimeTramitacao, - Relatoria, StatusTramitacao, TipoAutor, TipoDocumento, - TipoFimRelatoria, TipoMateriaLegislativa, TipoProposicao, - Tramitacao, UnidadeTramitacao) - +from .models import (AcompanhamentoMateria, Anexada, Autoria, DespachoInicial, + DocumentoAcessorio, MateriaLegislativa, Numeracao, Orgao, + Origem, Proposicao, RegimeTramitacao, Relatoria, + StatusTramitacao, TipoDocumento, TipoFimRelatoria, + TipoMateriaLegislativa, TipoProposicao, Tramitacao, + UnidadeTramitacao) OrigemCrud = Crud.build(Origem, '') @@ -69,20 +67,17 @@ TipoDocumentoCrud = CrudAux.build( TipoFimRelatoriaCrud = CrudAux.build( TipoFimRelatoria, 'fim_relatoria') -TipoAutorCrud = CrudAux.build( - TipoAutor, 'regime_tramitacao') - class MateriaTaView(IntegracaoTaView): model = MateriaLegislativa model_type_foreignkey = TipoMateriaLegislativa - """ - Para manter a app compilacao isolada das outras aplicações, - este get foi implementado para tratar uma prerrogativa externa - de usuário. - """ def get(self, request, *args, **kwargs): + """ + Para manter a app compilacao isolada das outras aplicações, + este get foi implementado para tratar uma prerrogativa externa + de usuário. + """ if AppConfig.attr('texto_articulado_materia'): return IntegracaoTaView.get(self, request, *args, **kwargs) else: @@ -118,89 +113,6 @@ def recuperar_materia(request): return response -def montar_helper_autor(self): - autor_row = montar_row_autor('nome') - self.helper = FormHelper() - self.helper.layout = SaplFormLayout(*self.get_layout()) - - # Adiciona o novo campo 'autor' e mecanismo de busca - self.helper.layout[0][0].append(HTML(autor_label)) - self.helper.layout[0][0].append(HTML(autor_modal)) - self.helper.layout[0][1] = autor_row - - # Adiciona espaço entre o novo campo e os botões - # self.helper.layout[0][4][1].append(HTML('

')) - - # Remove botões que estão fora do form - self.helper.layout[1].pop() - - # Adiciona novos botões dentro do form - self.helper.layout[0][4][0].insert(2, form_actions(more=[ - HTML('Cancelar')])) - - -class AutorCrud(CrudAux): - model = Autor - help_path = 'autor' - - class BaseMixin(CrudAux.BaseMixin): - list_field_names = ['tipo', 'nome'] - - class UpdateView(CrudAux.UpdateView): - layout_key = 'AutorCreate' - - def __init__(self, *args, **kwargs): - montar_helper_autor(self) - super(UpdateView, self).__init__(*args, **kwargs) - - def get_context_data(self, **kwargs): - context = super(UpdateView, self).get_context_data(**kwargs) - context['helper'] = self.helper - return context - - class CreateView(CrudAux.CreateView): - form_class = AutorForm - layout_key = 'AutorCreate' - - def __init__(self, *args, **kwargs): - montar_helper_autor(self) - super(CreateView, self).__init__(*args, **kwargs) - - def get_context_data(self, **kwargs): - context = super(CreateView, self).get_context_data(**kwargs) - context['helper'] = self.helper - return context - - def get_success_url(self): - pk_autor = Autor.objects.get( - email=self.request.POST.get('email')).id - kwargs = {} - user = get_user_model().objects.get( - email=self.request.POST.get('email')) - kwargs['token'] = default_token_generator.make_token(user) - kwargs['uidb64'] = urlsafe_base64_encode(force_bytes(user.pk)) - assunto = "SAPL - Confirmação de Conta" - full_url = self.request.get_raw_uri() - url_base = full_url[:full_url.find('sistema') - 1] - - mensagem = ("Este e-mail foi utilizado para fazer cadastro no " + - "SAPL com o perfil de Autor. Agora você pode " + - "criar/editar/enviar Proposições.\n" + - "Seu nome de usuário é: " + - self.request.POST['username'] + "\n" - "Caso você não tenha feito este cadastro, por favor " + - "ignore esta mensagem. Caso tenha, clique " + - "no link abaixo\n" + url_base + - reverse('sapl.materia:confirmar_email', kwargs=kwargs)) - remetente = settings.EMAIL_SEND_USER - destinatario = [self.request.POST.get('email')] - send_mail(assunto, mensagem, remetente, destinatario, - fail_silently=False) - return reverse('sapl.materia:autor_detail', - kwargs={'pk': pk_autor}) - - class ConfirmarEmailView(TemplateView): template_name = "confirma_email.html" @@ -398,6 +310,12 @@ class ConfirmarProposicao(PermissionRequiredMixin, CreateView): 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 = '' @@ -661,19 +579,6 @@ class TramitacaoCrud(MasterDetailCrud): return HttpResponseRedirect(url) -def montar_row_autor(name): - autor_row = to_row( - [(name, 0), - (Button('pesquisar', - 'Pesquisar Autor', - css_class='btn btn-primary btn-sm'), 2), - (Button('limpar', - 'Limpar Autor', - css_class='btn btn-primary btn-sm'), 10)]) - - return autor_row - - def montar_helper_documento_acessorio(self): autor_row = montar_row_autor('autor') self.helper = FormHelper() diff --git a/sapl/parlamentares/models.py b/sapl/parlamentares/models.py index cb6e8b42d..2703b2416 100644 --- a/sapl/parlamentares/models.py +++ b/sapl/parlamentares/models.py @@ -4,8 +4,9 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ from model_utils import Choices +from sapl.base.models import Autor from sapl.utils import (INDICADOR_AFASTAMENTO, UF, YES_NO_CHOICES, - intervalos_tem_intersecao, + SaplGenericRelation, intervalos_tem_intersecao, restringe_tipos_de_arquivo_img) @@ -262,6 +263,18 @@ class Parlamentar(models.Model): verbose_name=_('Fotografia'), validators=[restringe_tipos_de_arquivo_img]) + # campo conceitual de reversão genérica para o model Autor que dá a + # o meio possível de localização de tipos de autores. + autor = SaplGenericRelation( + Autor, + related_query_name='parlamentar_set', + fields_search=( + # na primeira posição dever ser campo simples sem __ + ('nome_completo', '__icontains'), + ('nome_parlamentar', '__icontains'), + ('filiacao__partido__sigla', '__icontains'), + )) + class Meta: verbose_name = _('Parlamentar') verbose_name_plural = _('Parlamentares') @@ -447,6 +460,18 @@ class Frente(models.Model): blank=True, null=True, verbose_name=_('Data Dissolução')) descricao = models.TextField(blank=True, verbose_name=_('Descrição')) + # campo conceitual de reversão genérica para o model Autor que dá a + # o meio possível de localização de tipos de autores. + autor = SaplGenericRelation( + Autor, + related_query_name='frente_set', + fields_search=( + ('nome', '__icontains'), + ('descricao', '__icontains'), + ('parlamentares__filiacao__partido__sigla', '__icontains'), + ('parlamentares__filiacao__partido__nome', '__icontains'), + )) + class Meta: verbose_name = _('Frente') verbose_name_plural = _('Frentes') diff --git a/sapl/parlamentares/views.py b/sapl/parlamentares/views.py index cf64d0bde..d31e4557d 100644 --- a/sapl/parlamentares/views.py +++ b/sapl/parlamentares/views.py @@ -59,14 +59,13 @@ class RelatoriaParlamentarCrud(CrudBaseForListAndDetailExternalAppView): class ProposicaoParlamentarCrud(CrudBaseForListAndDetailExternalAppView): model = Proposicao list_field_names = ['tipo', 'descricao'] - parent_field = 'autor__parlamentar' + parent_field = 'autor__parlamentar_set' namespace = AppConfig.name class ListView(CrudBaseForListAndDetailExternalAppView.ListView): def get_queryset(self): return super().get_queryset().filter( - autor__parlamentar_id=self.kwargs['pk'], data_envio__isnull=False) diff --git a/sapl/protocoloadm/forms.py b/sapl/protocoloadm/forms.py index 20e77b782..7eae4909d 100644 --- a/sapl/protocoloadm/forms.py +++ b/sapl/protocoloadm/forms.py @@ -1,6 +1,5 @@ from datetime import datetime -import django_filters from crispy_forms.bootstrap import InlineRadios from crispy_forms.helper import FormHelper from crispy_forms.layout import HTML, Button, Fieldset, Layout, Submit @@ -9,9 +8,11 @@ from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.db import models from django.forms import ModelForm from django.utils.translation import ugettext_lazy as _ +import django_filters +from sapl.base.models import Autor from sapl.crispy_layout_mixin import form_actions, to_row -from sapl.materia.models import Autor, UnidadeTramitacao +from sapl.materia.models import UnidadeTramitacao from sapl.utils import (RANGE_ANOS, RangeWidgetOverride, autor_label, autor_modal) @@ -19,6 +20,7 @@ from .models import (DocumentoAcessorioAdministrativo, DocumentoAdministrativo, Protocolo, TipoDocumentoAdministrativo, TramitacaoAdministrativo) + TIPOS_PROTOCOLO = [('0', 'Enviado'), ('1', 'Recebido'), ('', 'Ambos')] NATUREZA_PROCESSO = [('', 'Ambos'), @@ -421,6 +423,7 @@ class ProtocoloMateriaForm(ModelForm): super(ProtocoloMateriaForm, self).__init__( *args, **kwargs) + self.fields['tipo_protocolo'].inline_class = True class DocumentoAcessorioAdministrativoForm(ModelForm): diff --git a/sapl/protocoloadm/migrations/0003_auto_20161009_1222.py b/sapl/protocoloadm/migrations/0003_auto_20161009_1222.py new file mode 100644 index 000000000..a083fdffa --- /dev/null +++ b/sapl/protocoloadm/migrations/0003_auto_20161009_1222.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-10-09 15:22 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('protocoloadm', '0002_delete_tipoinstituicao'), + ] + + operations = [ + migrations.AlterField( + model_name='documentoadministrativo', + name='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'), + ), + ] diff --git a/sapl/protocoloadm/models.py b/sapl/protocoloadm/models.py index 6c65dfb6f..5f713202b 100644 --- a/sapl/protocoloadm/models.py +++ b/sapl/protocoloadm/models.py @@ -4,7 +4,8 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ from model_utils import Choices -from sapl.materia.models import (Autor, TipoMateriaLegislativa, +from sapl.base.models import Autor +from sapl.materia.models import (TipoMateriaLegislativa, UnidadeTramitacao) from sapl.utils import RANGE_ANOS, YES_NO_CHOICES diff --git a/sapl/protocoloadm/urls.py b/sapl/protocoloadm/urls.py index d84e735f8..48a615f31 100644 --- a/sapl/protocoloadm/urls.py +++ b/sapl/protocoloadm/urls.py @@ -17,7 +17,7 @@ from sapl.protocoloadm.views import (AnularProtocoloAdmView, ProtocoloPesquisaView, StatusTramitacaoAdministrativoCrud, TipoDocumentoAdministrativoCrud, - TramitacaoAdmCrud, pesquisa_autores) + TramitacaoAdmCrud) from .apps import AppConfig @@ -70,11 +70,11 @@ urlpatterns_sistema = [ url(r'^sistema/status-tramitacao-adm/', include(StatusTramitacaoAdministrativoCrud.get_urls())), - # FIXME: Usado para pesquisar autor + # FIXME: Usado para pesquisar autor- SOLUÇÃO-foi transformado em api/autor # Melhor forma de fazer? # Deve mudar de app? - url(r'^protocoloadm/pesquisar-autor', - pesquisa_autores, name='pesquisar_autor'), + # url(r'^protocoloadm/pesquisar-autor', + # pesquisa_autores, name='pesquisar_autor'), ] urlpatterns = (urlpatterns_documento_administrativo + diff --git a/sapl/protocoloadm/views.py b/sapl/protocoloadm/views.py index c1c258690..30a9606fa 100644 --- a/sapl/protocoloadm/views.py +++ b/sapl/protocoloadm/views.py @@ -1,5 +1,5 @@ -import json from datetime import date, datetime +import json from braces.views import FormValidMessageMixin from django.contrib import messages @@ -15,7 +15,7 @@ from django.views.generic.base import TemplateView from django_filters.views import FilterView from sapl.base.apps import AppConfig as AppsAppConfig -from sapl.base.models import AppConfig +from sapl.base.models import AppConfig, Autor from sapl.crud.base import Crud, CrudAux, MasterDetailCrud, make_pagination from sapl.materia.models import TipoMateriaLegislativa from sapl.utils import (create_barcode, get_client_ip, permissoes_adm, @@ -26,11 +26,12 @@ from .forms import (AnularProcoloAdmForm, DocumentoAcessorioAdministrativoForm, DocumentoAdministrativoForm, ProtocoloDocumentForm, ProtocoloFilterSet, ProtocoloMateriaForm, TramitacaoAdmEditForm, TramitacaoAdmForm) -from .models import (Autor, DocumentoAcessorioAdministrativo, +from .models import (DocumentoAcessorioAdministrativo, DocumentoAdministrativo, Protocolo, StatusTramitacaoAdministrativo, TipoDocumentoAdministrativo, TramitacaoAdministrativo) + TipoDocumentoAdministrativoCrud = CrudAux.build( TipoDocumentoAdministrativo, '') @@ -584,6 +585,7 @@ class TramitacaoAdmCrud(MasterDetailCrud): pass +""" def get_nome_autor(request): nome_autor = '' if request.method == 'GET': @@ -597,9 +599,9 @@ def get_nome_autor(request): except ObjectDoesNotExist: pass return HttpResponse("{\"nome\":\"" + nome_autor + "\"}", - content_type="application/json; charset=utf-8") - + content_type="application/json; charset=utf-8")""" +""" def pesquisa_autores(request): q = '' if request.method == 'GET': @@ -611,6 +613,8 @@ def pesquisa_autores(request): Q(comissao__nome__icontains=q) ) + autor = Autor.objects.filter(nome__icontains=q) + autores = [] for a in autor: @@ -630,3 +634,4 @@ def pesquisa_autores(request): sort_keys=True, ensure_ascii=False), content_type="application/json; charset=utf-8") +""" \ No newline at end of file diff --git a/sapl/relatorios/views.py b/sapl/relatorios/views.py index 5c1d26d8c..6c3184b42 100644 --- a/sapl/relatorios/views.py +++ b/sapl/relatorios/views.py @@ -4,9 +4,9 @@ from bs4 import BeautifulSoup from django.http import Http404, HttpResponse from django.utils.translation import ugettext_lazy as _ -from sapl.base.models import CasaLegislativa +from sapl.base.models import CasaLegislativa, Autor from sapl.comissoes.models import Comissao -from sapl.materia.models import (Autor, Autoria, MateriaLegislativa, Numeracao, +from sapl.materia.models import (Autoria, MateriaLegislativa, Numeracao, Tramitacao, UnidadeTramitacao) from sapl.parlamentares.models import (CargoMesa, ComposicaoMesa, Filiacao, Parlamentar) @@ -102,12 +102,8 @@ def get_materias(mats): dic['txt_ementa'] = materia.ementa autores = Autoria.objects.filter(materia=materia) - dic['nom_autor'] = " " - for autoria in autores: - if autoria.autor.parlamentar: - dic['nom_autor'] = autoria.autor.parlamentar.nome_completo - elif autoria.autor.comissao: - dic['nom_autor'] = autoria.autor.comissao.nome + dic['nom_autor'] = ', '.join( + [str(autoria.autor) for autoria in autores]) des_status = '' txt_tramitacao = '' diff --git a/sapl/sessao/forms.py b/sapl/sessao/forms.py index fa5694777..f213e1b94 100644 --- a/sapl/sessao/forms.py +++ b/sapl/sessao/forms.py @@ -212,7 +212,7 @@ class AdicionarVariasMateriasFilterSet(MateriaLegislativaFilterSet): 'data_apresentacao', 'data_publicacao', 'autoria__autor__tipo', - 'autoria__autor__partido', + # 'autoria__autor__partido', 'relatoria__parlamentar_id', 'local_origem_externa', 'em_tramitacao', @@ -231,7 +231,7 @@ class AdicionarVariasMateriasFilterSet(MateriaLegislativaFilterSet): self.filters['tipo'].label = 'Tipo de Matéria' self.filters['autoria__autor__tipo'].label = 'Tipo de Autor' - self.filters['autoria__autor__partido'].label = 'Partido do Autor' + # self.filters['autoria__autor__partido'].label = 'Partido do Autor' self.filters['relatoria__parlamentar_id'].label = 'Relatoria' row1 = to_row( @@ -253,7 +253,8 @@ class AdicionarVariasMateriasFilterSet(MateriaLegislativaFilterSet): css_class='btn btn-primary btn-sm'), 10)]) row5 = to_row( [('autoria__autor__tipo', 6), - ('autoria__autor__partido', 6)]) + # ('autoria__autor__partido', 6) + ]) row6 = to_row( [('relatoria__parlamentar_id', 6), ('local_origem_externa', 6)]) diff --git a/sapl/sessao/models.py b/sapl/sessao/models.py index 614f3ec57..dc375fff7 100644 --- a/sapl/sessao/models.py +++ b/sapl/sessao/models.py @@ -2,10 +2,12 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ from model_utils import Choices +from sapl.base.models import Autor from sapl.materia.models import MateriaLegislativa from sapl.parlamentares.models import (CargoMesa, Legislatura, Parlamentar, Partido, SessaoLegislativa) -from sapl.utils import YES_NO_CHOICES, restringe_tipos_de_arquivo_txt +from sapl.utils import YES_NO_CHOICES, restringe_tipos_de_arquivo_txt,\ + SaplGenericRelation class CargoBancada(models.Model): @@ -38,6 +40,16 @@ class Bancada(models.Model): verbose_name=_('Data Extinção')) descricao = models.TextField(blank=True, verbose_name=_('Descrição')) + # campo conceitual de reversão genérica para o model Autor que dá a + # o meio possível de localização de tipos de autores. + autor = SaplGenericRelation(Autor, related_query_name='bancada_set', + fields_search=( + ('nome', '__icontains'), + ('descricao', '__icontains'), + ('partido__sigla', '__icontains'), + ('partido__nome', '__icontains'), + )) + class Meta: verbose_name = _('Bancada') verbose_name_plural = _('Bancadas') @@ -341,6 +353,17 @@ class Bloco(models.Model): blank=True, null=True, verbose_name=_('Data Dissolução')) descricao = models.TextField(blank=True, verbose_name=_('Descrição')) + # campo conceitual de reversão genérica para o model Autor que dá a + # o meio possível de localização de tipos de autores. + autor = SaplGenericRelation(Autor, + related_query_name='bloco_set', + fields_search=( + ('nome', '__icontains'), + ('descricao', '__icontains'), + ('partidos__sigla', '__icontains'), + ('partidos__nome', '__icontains'), + )) + class Meta: verbose_name = _('Bloco') verbose_name_plural = _('Blocos') diff --git a/sapl/sessao/urls.py b/sapl/sessao/urls.py index f24c31ef0..fd06ceb7f 100644 --- a/sapl/sessao/urls.py +++ b/sapl/sessao/urls.py @@ -11,10 +11,9 @@ from sapl.sessao.views import (AdicionarVariasMateriasExpediente, PesquisarPautaSessaoView, PesquisarSessaoPlenariaView, PresencaOrdemDiaView, PresencaView, ResumoView, - SessaoCrud, - TipoExpedienteCrud, TipoResultadoVotacaoCrud, - TipoSessaoCrud, VotacaoEditView, - VotacaoExpedienteEditView, + SessaoCrud, TipoExpedienteCrud, + TipoResultadoVotacaoCrud, TipoSessaoCrud, + VotacaoEditView, VotacaoExpedienteEditView, VotacaoExpedienteView, VotacaoNominalEditView, VotacaoNominalExpedienteEditView, VotacaoNominalExpedienteView, @@ -30,6 +29,7 @@ from .apps import AppConfig app_name = AppConfig.name + urlpatterns = [ url(r'^sessao/', include(SessaoCrud.get_urls() + OradorCrud.get_urls() + OradorExpedienteCrud.get_urls() + diff --git a/sapl/settings.py b/sapl/settings.py index 1cb071eb5..f3030d255 100644 --- a/sapl/settings.py +++ b/sapl/settings.py @@ -13,6 +13,9 @@ Quick-start development settings - unsuitable for production See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ """ +import logging +import sys + from decouple import config from dj_database_url import parse as db_url from unipath import Path @@ -20,13 +23,13 @@ from unipath import Path from .temp_suppress_crispy_form_warnings import \ SUPRESS_CRISPY_FORM_WARNINGS_LOGGING + BASE_DIR = Path(__file__).ancestor(1) PROJECT_DIR = Path(__file__).ancestor(2) # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = config('SECRET_KEY', default='') - # SECURITY WARNING: don't run with debug turned on in production! DEBUG = config('DEBUG', default=False, cast=bool) @@ -35,6 +38,9 @@ ALLOWED_HOSTS = ['*'] LOGIN_REDIRECT_URL = '/' LOGIN_URL = '/login/?next=' +EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' + + # SAPL business apps in dependency order SAPL_APPS = ( 'sapl.base', @@ -48,6 +54,7 @@ SAPL_APPS = ( 'sapl.painel', 'sapl.protocoloadm', 'sapl.compilacao', + 'sapl.api' ) INSTALLED_APPS = ( @@ -69,10 +76,11 @@ INSTALLED_APPS = ( 'sass_processor', 'rest_framework', + ) + SAPL_APPS if DEBUG: - INSTALLED_APPS += ('debug_toolbar',) + INSTALLED_APPS += ('debug_toolbar', 'rest_framework_docs',) MIDDLEWARE_CLASSES = ( 'django.contrib.sessions.middleware.SessionMiddleware', @@ -86,6 +94,32 @@ MIDDLEWARE_CLASSES = ( 'django.middleware.security.SecurityMiddleware', ) + +REST_FRAMEWORK = { + "DEFAULT_RENDERER_CLASSES": ( + "rest_framework.renderers.JSONRenderer", + #"rest_framework.renderers.BrowsableAPIRenderer", + ), + "DEFAULT_PARSER_CLASSES": ( + "rest_framework.parsers.JSONParser", + ), + "DEFAULT_PERMISSION_CLASSES": ( + "rest_framework.permissions.IsAuthenticated", + ), + "DEFAULT_PERMISSION_CLASSES": ( + "sapl.api.permissions.DjangoModelPermissions", + ), + "DEFAULT_AUTHENTICATION_CLASSES": ( + "rest_framework.authentication.SessionAuthentication", + ), + "DEFAULT_PAGINATION_CLASS": "sapl.api.pagination.StandardPagination", + "DEFAULT_FILTER_BACKENDS": ( + "rest_framework.filters.SearchFilter", + "rest_framework.filters.DjangoFilterBackend", + ), +} + + ROOT_URLCONF = 'sapl.urls' TEMPLATES = [ @@ -193,10 +227,41 @@ SASS_PROCESSOR_INCLUDE_DIRS = (BOWER_COMPONENTS_ROOT.child( 'bower_components', 'bootstrap-sass', 'assets', 'stylesheets'), ) +# suprime texto de ajuda default do django-filter +FILTERS_HELP_TEXT_FILTER = False + + # FIXME update cripy-forms and remove this # hack to suppress many annoying warnings from crispy_forms # see sapl.temp_suppress_crispy_form_warnings LOGGING = SUPRESS_CRISPY_FORM_WARNINGS_LOGGING -# suprime texto de ajuda default do django-filter -FILTERS_HELP_TEXT_FILTER = False + +LOGGING_CONSOLE = config('LOGGING_CONSOLE', default=False, cast=bool) +if DEBUG and LOGGING_CONSOLE: + # Descomentar linha abaixo fará com que logs aparecam, inclusive SQL + # LOGGING['handlers']['console']['level'] = 'DEBUG' + LOGGING['loggers']['django']['level'] = 'DEBUG' + LOGGING.update({ + 'formatters': { + 'verbose': { + 'format': '%(levelname)s %(asctime)s %(pathname)s ' + '%(funcName)s %(message)s' + }, + 'simple': { + 'format': '%(levelname)s %(message)s' + }, + }, + }) + LOGGING['handlers']['console']['formatter'] = 'verbose' + LOGGING['loggers'][BASE_DIR.name] = { + 'handlers': ['console'], + 'level': 'DEBUG', + } + + +def excepthook(*args): + logging.getLogger(BASE_DIR.name).error( + 'Uncaught exception:', exc_info=args) + +sys.excepthook = excepthook diff --git a/sapl/static/js/app.js b/sapl/static/js/app.js index 8cb30f44e..15e744fd6 100644 --- a/sapl/static/js/app.js +++ b/sapl/static/js/app.js @@ -90,28 +90,25 @@ function autorModal() { $("#pesquisar").click(function() { var query = $("#q").val() - $.get("/protocoloadm/pesquisar-autor?q="+ query, function( - data, status){ - $("#div-resultado").children().remove(); + $.get("/api/autor?q=" + query, function(data, status) { + $("#div-resultado").children().remove(); + if (data.pagination.total_entries == 0) { + $("#selecionar").attr("hidden", "hidden"); + $("#div-resultado").html( + "Nenhum resultado"); + return; + } - if (data.length == 0) { - $("#selecionar").attr("hidden", "hidden"); - $("#div-resultado").html( - "Nenhum resultado"); - return; - } + var select = $( + ''); + data.models.forEach(function(item, index) { + select.append($("