From f6065c3e54e5d51ea62d80f0fafc0bec7bd19871 Mon Sep 17 00:00:00 2001 From: LeandroRoberto Date: Mon, 10 Oct 2016 08:39:54 -0300 Subject: [PATCH 01/16] Ref Autor, TipoAutor, cria app api DRF MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Autor e TipoAutor migrados para app base. - Foram refatorados para GR - Generic Relations - Em TipoAutor: passou se a apontar também para um ContentType que é usado para contextualização de dados da GR em Autor. - A captura da combo de ContentTypes é feita através do apontamento reverso nos models que se queira disponibilizar conceitualmente como Autor - Em Autor: neste commit, o form de create está em desenvolvimento, com o buscador de possiveis autores baseados na seleção do usuário de TipoAutor que, se não possui ContentType, abre o campo nome para insersão, se possui ContentType, abre caixa de busca com atualização jquery de radiobox's para o usuário selecionar um possível autor. - api rest: para a busca funcionar e como objetivo de futuras implementações em DRF, a app api foi criada, anotada nas configurações gerais de sapl.urls com o prefixo /api. - na api foi criada a uma ListAPIView para pesquisa de possiveis autores baseados no tipo autor enviado, url /api/autor/possiveis/?P[0-9]*)$ que sem pk devolve a lista de TipoAutor e, com pk, devolve a lista dos registros ligados ao ContentType, filtrados pelo parametro q --- sapl/api/__init__.py | 1 + sapl/api/admin.py | 0 sapl/api/apps.py | 8 + sapl/api/forms.py | 0 sapl/api/migrations/__init__.py | 0 sapl/api/serializers.py | 25 ++ sapl/api/urls.py | 20 ++ sapl/api/views.py | 36 +++ sapl/base/forms.py | 214 +++++++++++++++++- .../migrations/0022_auto_20161009_1222.py | 55 +++++ .../migrations/0023_auto_20161009_1852.py | 25 ++ sapl/base/models.py | 64 +++++- sapl/base/urls.py | 30 ++- sapl/base/views.py | 120 ++++++++++ sapl/comissoes/models.py | 4 + sapl/crispy_layout_mixin.py | 7 +- sapl/materia/forms.py | 125 +--------- .../migrations/0054_auto_20161009_1222.py | 53 +++++ .../migrations/0055_auto_20161009_1418.py | 32 +++ sapl/materia/models.py | 85 ++----- sapl/materia/urls.py | 8 +- sapl/materia/views.py | 118 +--------- sapl/parlamentares/models.py | 4 + .../migrations/0003_auto_20161009_1222.py | 26 +++ sapl/rest_pagination.py | 33 +++ sapl/sessao/forms.py | 9 +- sapl/sessao/urls.py | 1 + sapl/settings.py | 24 ++ sapl/static/styles/app.scss | 1 + sapl/templates/base/autor_form.html | 61 +++++ sapl/templates/base/layouts.yaml | 13 ++ sapl/templates/base/tipoautor_form.html | 20 ++ sapl/templates/materia/layouts.yaml | 17 -- sapl/templates/sistema.html | 7 +- sapl/urls.py | 3 + 35 files changed, 913 insertions(+), 336 deletions(-) create mode 100644 sapl/api/__init__.py create mode 100644 sapl/api/admin.py create mode 100644 sapl/api/apps.py create mode 100644 sapl/api/forms.py create mode 100644 sapl/api/migrations/__init__.py create mode 100644 sapl/api/serializers.py create mode 100644 sapl/api/urls.py create mode 100644 sapl/api/views.py create mode 100644 sapl/base/migrations/0022_auto_20161009_1222.py create mode 100644 sapl/base/migrations/0023_auto_20161009_1852.py create mode 100644 sapl/materia/migrations/0054_auto_20161009_1222.py create mode 100644 sapl/materia/migrations/0055_auto_20161009_1418.py create mode 100644 sapl/protocoloadm/migrations/0003_auto_20161009_1222.py create mode 100644 sapl/rest_pagination.py create mode 100644 sapl/templates/base/autor_form.html create mode 100644 sapl/templates/base/tipoautor_form.html 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..e69de29bb 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/serializers.py b/sapl/api/serializers.py new file mode 100644 index 000000000..d61643d21 --- /dev/null +++ b/sapl/api/serializers.py @@ -0,0 +1,25 @@ +from rest_framework import serializers + +from sapl.comissoes.models import Comissao +from sapl.parlamentares.models import Parlamentar + + +class ChoiceSerializer(serializers.Serializer): + pk = serializers.IntegerField() + display = serializers.SerializerMethodField() + + def get_display(self, obj): + return str(obj) +""" + +class ModelChoiceParlamentarSerializer(ModelChoiceSerializer): + + class Meta: + model = Parlamentar + + +class ModelChoiceComissaoSerializer(ModelChoiceSerializer): + + class Meta: + model = Comissao +""" \ No newline at end of file diff --git a/sapl/api/urls.py b/sapl/api/urls.py new file mode 100644 index 000000000..a3a130da9 --- /dev/null +++ b/sapl/api/urls.py @@ -0,0 +1,20 @@ +from django.conf.urls import include, url +from rest_framework.routers import DefaultRouter + +from sapl.api.views import TipoAutorContentOfModelContentTypeView + +from .apps import AppConfig + + +app_name = AppConfig.name + + +# router = DefaultRouter() + +urlpatterns = [ + url(r'^autor/possiveis/(?P[0-9]*)$', + TipoAutorContentOfModelContentTypeView.as_view(), + name='autores_possiveis_pelo_tipo'), +] + +# urlpatterns += router.urls diff --git a/sapl/api/views.py b/sapl/api/views.py new file mode 100644 index 000000000..08181d8a0 --- /dev/null +++ b/sapl/api/views.py @@ -0,0 +1,36 @@ +from django.http import Http404 +from rest_framework import mixins, viewsets +from rest_framework.generics import ListAPIView, GenericAPIView,\ + get_object_or_404 +from rest_framework.permissions import IsAuthenticated, AllowAny +from rest_framework.views import APIView +from rest_framework.viewsets import ModelViewSet + +from sapl.api.serializers import ChoiceSerializer +from sapl.base.models import Autor, TipoAutor + + +class TipoAutorContentOfModelContentTypeView(ListAPIView): + serializer_class = ChoiceSerializer + permission_classes = (AllowAny,) + queryset = TipoAutor.objects.all() + model = TipoAutor + pagination_class = None + + def get_queryset(self): + queryset = ModelViewSet.get_queryset(self) + if not self.kwargs['pk']: + return queryset + + obj = get_object_or_404(queryset, pk=self.kwargs['pk']) + + if not obj.content_type: + raise Http404 + + q = self.request.GET.get('q', None) + + if not q: + return [] + else: + return obj.content_type.model_class().objects.filter( + nome__icontains=q)[:10] diff --git a/sapl/base/forms.py b/sapl/base/forms.py index 952db3b35..93ae87290 100644 --- a/sapl/base/forms.py +++ b/sapl/base/forms.py @@ -1,14 +1,23 @@ -import django_filters +from crispy_forms.bootstrap import FieldWithButtons, StrictButton from crispy_forms.helper import FormHelper -from crispy_forms.layout import HTML, Button, Fieldset, Layout +from crispy_forms.layout import HTML, Button, Fieldset, Layout, Field, Div, Row +from crispy_forms.templatetags.crispy_forms_field import css_class from django import forms +from django.contrib.auth import get_user_model from django.contrib.auth.forms import AuthenticationForm -from django.core.exceptions import ValidationError -from django.db import models -from django.forms import ModelForm +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, ObjectDoesNotExist +from django.db import models, transaction +from django.forms import ModelForm, widgets from django.utils.translation import ugettext_lazy as _ +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 form_actions, to_row, SaplFormLayout,\ + to_column from sapl.materia.models import MateriaLegislativa from sapl.sessao.models import SessaoPlenaria from sapl.settings import MAX_IMAGE_UPLOAD_SIZE @@ -18,6 +27,199 @@ from sapl.utils import (RANGE_ANOS, ImageThumbnailFileInput, from .models import AppConfig, CasaLegislativa +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 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 + )""" + + q = forms.CharField( + max_length=50, required=False, + label='Pesquise o nome do Autor com o tipo Selecionado') + content_object = forms.ChoiceField(label='', + required=False, + widget=forms.RadioSelect()) + + class Meta: + model = Autor + fields = ['tipo', + 'nome', + 'content_object', + 'q'] + + def __init__(self, *args, **kwargs): + + content_object = 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')), + + + + + Field('content_object'), + css_class='hidden', + data_action='create', + data_application='AutorSearch', + data_field='content_object') + + row1 = to_row([ + ('tipo', 4), + ('nome', 8), + (content_object, 8), + + ]) + + self.helper = FormHelper() + self.helper.layout = SaplFormLayout(row1) + + super(AutorForm, self).__init__(*args, **kwargs) + + self.fields['content_object'].choices = [('1', 'teste')] + if self.instance and self.instance.content_object: + self.fields['content_object'].choices = [ + (self.instance.content_object.pk, + self.instance.content_object)] + + 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 clea(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: + get_user_model().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 sav(self, commit=False): + + autor = super(AutorForm, self).save(commit) + + u = get_user_model().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 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/models.py b/sapl/base/models.py index 7872f2c21..67896fef9 100644 --- a/sapl/base/models.py +++ b/sapl/base/models.py @@ -7,10 +7,11 @@ from django.contrib.contenttypes.models import ContentType from django.core import exceptions from django.db import models, router from django.db.utils import DEFAULT_DB_ALIAS -from django.utils.translation import ugettext_lazy as _ from django.utils.translation import string_concat +from django.utils.translation import ugettext_lazy as _ + +from sapl.utils import UF, YES_NO_CHOICES, get_settings_auth_user_model -from sapl.utils import UF, YES_NO_CHOICES TIPO_DOCUMENTO_ADMINISTRATIVO = (('O', _('Ostensivo')), ('R', _('Restritivo'))) @@ -132,6 +133,65 @@ 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()) + + 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) + content_object = 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.content_object: + return str(self.content_object) + 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..1e822dcb0 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, @@ -12,23 +14,26 @@ from .views import (AppConfigCrud, CasaLegislativaCrud, HelpView, RelatorioMateriasTramitacaoView, RelatorioPresencaSessaoView) + app_name = AppConfig.name urlpatterns = [ +<<<<<<< 6123d2617726dd220c02c2bb3b3c27ed4b136df1 +======= + url(r'^sistema/autor/tipo/', include(TipoAutorCrud.get_urls())), + url(r'^sistema/autor/', include(AutorCrud.get_urls())), + +>>>>>>> Ref Autor, TipoAutor, cria app api DRF 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/(?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 +55,21 @@ urlpatterns = [ RelatorioAtasView.as_view(), name='atas'), +<<<<<<< 6123d2617726dd220c02c2bb3b3c27ed4b136df1 +======= + + # todos os sublink s de sistema devem vir acima deste +>>>>>>> Ref Autor, TipoAutor, cria app api DRF url(r'^sistema/', permission_required('base.view_tabelas_auxiliares') (TemplateView.as_view(template_name='sistema.html'))), +<<<<<<< 6123d2617726dd220c02c2bb3b3c27ed4b136df1 +======= + 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'), + +>>>>>>> Ref Autor, TipoAutor, cria app api DRF ] diff --git a/sapl/base/views.py b/sapl/base/views.py index b7d6378b8..ac3e3ef56 100644 --- a/sapl/base/views.py +++ b/sapl/base/views.py @@ -1,16 +1,28 @@ +from crispy_forms.helper import FormHelper +from crispy_forms.layout import HTML, Button +from django.conf import settings +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.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 TipoAutor, Autor +from sapl.crispy_layout_mixin import to_row, SaplFormLayout, form_actions from sapl.crud.base import CrudAux from sapl.materia.models import MateriaLegislativa, TipoMateriaLegislativa from sapl.parlamentares.models import Parlamentar from sapl.sessao.models import OrdemDia, SessaoPlenaria +from sapl.utils import autor_label, autor_modal from .forms import (CasaLegislativaForm, ConfiguracoesAppForm, RelatorioAtasFilterSet, @@ -26,6 +38,114 @@ def get_casalegislativa(): return CasaLegislativa.objects.first() +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_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 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 UpdateView(CrudAux.UpdateView): + layout_key = None + form_class = AutorForm + + def __init__(self, *args, **kwargs): + # montar_helper_autor(self) + super(CrudAux.UpdateView, self).__init__(*args, **kwargs) + + def get_context_data(self, **kwargs): + context = super( + CrudAux.UpdateView, self).get_context_data(**kwargs) + #context['helper'] = self.helper + return context + + class CreateView(CrudAux.CreateView): + form_class = AutorForm + layout_key = None + + """def __init__(self, *args, **kwargs): + montar_helper_autor(self) + super(CrudAux.CreateView, self).__init__(*args, **kwargs)""" + + """def get_context_data(self, **kwargs): + context = super( + CrudAux.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.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..9919857c1 100644 --- a/sapl/comissoes/models.py +++ b/sapl/comissoes/models.py @@ -1,8 +1,10 @@ +from django.contrib.contenttypes.fields import GenericRelation 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 @@ -79,6 +81,8 @@ class Comissao(models.Model): choices=YES_NO_CHOICES, verbose_name=_('Comissão Ativa?')) + autor = GenericRelation(Autor) + class Meta: verbose_name = _('Comissão') verbose_name_plural = _('Comissões') diff --git a/sapl/crispy_layout_mixin.py b/sapl/crispy_layout_mixin.py index e0184e0f0..33e6d2608 100644 --- a/sapl/crispy_layout_mixin.py +++ b/sapl/crispy_layout_mixin.py @@ -1,12 +1,12 @@ from math import ceil -import rtyaml from crispy_forms.bootstrap import FormActions from crispy_forms.helper import FormHelper from crispy_forms.layout import HTML, Div, Fieldset, Layout, Submit from django import template from django.utils import formats from django.utils.translation import ugettext as _ +import rtyaml def heads_and_tails(list_of_lists): @@ -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/materia/forms.py b/sapl/materia/forms.py index 7af65be63..5e434007d 100644 --- a/sapl/materia/forms.py +++ b/sapl/materia/forms.py @@ -1,6 +1,5 @@ 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 @@ -12,6 +11,7 @@ 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 _ +import django_filters from sapl.comissoes.models import Comissao from sapl.crispy_layout_mixin import form_actions, to_row @@ -508,7 +508,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 +544,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 +566,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 +667,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/models.py b/sapl/materia/models.py index baedf7248..5a14e7ced 100644 --- a/sapl/materia/models.py +++ b/sapl/materia/models.py @@ -3,6 +3,7 @@ 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.utils import (RANGE_ANOS, YES_NO_CHOICES, @@ -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/urls.py b/sapl/materia/urls.py index 5f943be9d..dc18e47ef 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,7 +15,7 @@ from sapl.materia.views import (AcompanhamentoConfirmarView, ProposicaoRecebida, ProposicaoTaView, ReceberProposicao, ReciboProposicaoView, RegimeTramitacaoCrud, RelatoriaCrud, - StatusTramitacaoCrud, TipoAutorCrud, + StatusTramitacaoCrud, TipoDocumentoCrud, TipoFimRelatoriaCrud, TipoMateriaCrud, TipoProposicaoCrud, TramitacaoCrud, TramitacaoEmLoteView, @@ -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..71b166c12 100644 --- a/sapl/materia/views.py +++ b/sapl/materia/views.py @@ -24,7 +24,8 @@ 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, CasaLegislativa, Autor, TipoAutor +from sapl.base.views import montar_row_autor from sapl.compilacao.views import IntegracaoTaView from sapl.crispy_layout_mixin import SaplFormLayout, form_actions, to_row from sapl.crud.base import (ACTION_CREATE, ACTION_DELETE, ACTION_DETAIL, @@ -40,17 +41,17 @@ from sapl.utils import (TURNO_TRAMITACAO_CHOICES, YES_NO_CHOICES, autor_label, permissoes_protocoloadm, permission_required_for_app) 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, +from .models import (AcompanhamentoMateria, Anexada, Autoria, DespachoInicial, DocumentoAcessorio, MateriaLegislativa, Numeracao, Orgao, Origem, Proposicao, RegimeTramitacao, - Relatoria, StatusTramitacao, TipoAutor, TipoDocumento, + Relatoria, StatusTramitacao, TipoDocumento, TipoFimRelatoria, TipoMateriaLegislativa, TipoProposicao, Tramitacao, UnidadeTramitacao) @@ -69,20 +70,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 +116,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" @@ -661,19 +576,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..2629eac6c 100644 --- a/sapl/parlamentares/models.py +++ b/sapl/parlamentares/models.py @@ -1,9 +1,11 @@ from datetime import datetime +from django.contrib.contenttypes.fields import GenericRelation 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, restringe_tipos_de_arquivo_img) @@ -262,6 +264,8 @@ class Parlamentar(models.Model): verbose_name=_('Fotografia'), validators=[restringe_tipos_de_arquivo_img]) + autor = GenericRelation(Autor) + class Meta: verbose_name = _('Parlamentar') verbose_name_plural = _('Parlamentares') 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/rest_pagination.py b/sapl/rest_pagination.py new file mode 100644 index 000000000..bb7096a55 --- /dev/null +++ b/sapl/rest_pagination.py @@ -0,0 +1,33 @@ +from django.core.paginator import EmptyPage +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/sessao/forms.py b/sapl/sessao/forms.py index fa5694777..d629c608c 100644 --- a/sapl/sessao/forms.py +++ b/sapl/sessao/forms.py @@ -1,12 +1,12 @@ from datetime import datetime -import django_filters from crispy_forms.helper import FormHelper from crispy_forms.layout import HTML, Button, Fieldset, Layout from django import forms from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.forms import ModelForm from django.utils.translation import ugettext_lazy as _ +import django_filters from sapl.crispy_layout_mixin import form_actions, to_row from sapl.materia.forms import MateriaLegislativaFilterSet @@ -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/urls.py b/sapl/sessao/urls.py index f24c31ef0..672a95f0c 100644 --- a/sapl/sessao/urls.py +++ b/sapl/sessao/urls.py @@ -30,6 +30,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 0bf9e231c..ed27a931f 100644 --- a/sapl/settings.py +++ b/sapl/settings.py @@ -48,6 +48,7 @@ SAPL_APPS = ( 'sapl.painel', 'sapl.protocoloadm', 'sapl.compilacao', + 'sapl.api' ) INSTALLED_APPS = ( @@ -86,6 +87,29 @@ 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_AUTHENTICATION_CLASSES": ( + "rest_framework.authentication.SessionAuthentication", + ), + "DEFAULT_PAGINATION_CLASS": "sapl.rest_pagination.StandardPagination", + "DEFAULT_FILTER_BACKENDS": ( + "rest_framework.filters.SearchFilter", + "rest_framework.filters.DjangoFilterBackend", + ), +} + + ROOT_URLCONF = 'sapl.urls' TEMPLATES = [ diff --git a/sapl/static/styles/app.scss b/sapl/static/styles/app.scss index beb9f8bf5..535bf912e 100644 --- a/sapl/static/styles/app.scss +++ b/sapl/static/styles/app.scss @@ -77,6 +77,7 @@ h6, .h6 { } } + // #### CRUD DETAIL ######################################## p.control-label { font-weight: bold; diff --git a/sapl/templates/base/autor_form.html b/sapl/templates/base/autor_form.html new file mode 100644 index 000000000..dec8b20a3 --- /dev/null +++ b/sapl/templates/base/autor_form.html @@ -0,0 +1,61 @@ +{% extends "crud/form.html" %} +{% load i18n %} +{% block extra_js %} + + + +{% endblock %} diff --git a/sapl/templates/base/layouts.yaml b/sapl/templates/base/layouts.yaml index b657d4b79..c63135ee6 100644 --- a/sapl/templates/base/layouts.yaml +++ b/sapl/templates/base/layouts.yaml @@ -15,3 +15,16 @@ AppConfig: - documentos_administrativos sequencia_numeracao painel_aberto {% trans 'Textos Articulados' %}: - texto_articulado_proposicao texto_articulado_materia texto_articulado_norma + +TipoAutor: + {% trans 'Tipo Autor' %}: + - content_type descricao:7 + +Autor: + {% trans 'Autor' %}: + - tipo:3 nome + - username:6 cargo + +AutorCreate: + {% trans 'Cadastro de Usuários Autores' %}: + - tipo:3 search_autor diff --git a/sapl/templates/base/tipoautor_form.html b/sapl/templates/base/tipoautor_form.html new file mode 100644 index 000000000..01f771246 --- /dev/null +++ b/sapl/templates/base/tipoautor_form.html @@ -0,0 +1,20 @@ +{% extends "crud/form.html" %} +{% load i18n %} +{% block extra_js %} + + + +{% endblock %} diff --git a/sapl/templates/materia/layouts.yaml b/sapl/templates/materia/layouts.yaml index a5d20cfbf..47cb7f212 100644 --- a/sapl/templates/materia/layouts.yaml +++ b/sapl/templates/materia/layouts.yaml @@ -47,23 +47,6 @@ AnexadaDetail: - materia_anexada - data_anexacao data_desanexacao -TipoAutor: - {% trans 'Tipo Autor' %}: - - descricao - -Autor: - {% trans 'Autor' %}: - - tipo:3 nome - - username:6 cargo - -AutorCreate: - Autor: - - tipo - - nome - - username:4 senha:4 senha_confirma:4 - - email:6 confirma_email:6 - - cargo - Autoria: {% trans 'Autoria' %}: - autor primeiro_autor diff --git a/sapl/templates/sistema.html b/sapl/templates/sistema.html index 0b80f85ad..9db02b417 100644 --- a/sapl/templates/sistema.html +++ b/sapl/templates/sistema.html @@ -3,10 +3,12 @@ {% block base_content %} -

Configuração Inicial

+

Configuração Gerais


@@ -49,7 +51,6 @@

Módulo Proposições


@@ -57,12 +58,10 @@ diff --git a/sapl/urls.py b/sapl/urls.py index 750cce94d..84f92363d 100644 --- a/sapl/urls.py +++ b/sapl/urls.py @@ -31,6 +31,7 @@ import sapl.parlamentares.urls import sapl.protocoloadm.urls import sapl.relatorios.urls import sapl.sessao.urls +import sapl.api.urls urlpatterns = [ url(r'^$', TemplateView.as_view(template_name='index.html')), @@ -50,6 +51,8 @@ urlpatterns = [ # must come at the end # so that base /sistema/ url doesn't capture its children url(r'', include(sapl.base.urls)), + + url(r'^api/', include(sapl.api.urls)), ] From 0e71660c7db3bd6745e3850eefbfbbb2b8bc99fd Mon Sep 17 00:00:00 2001 From: LeandroRoberto Date: Mon, 10 Oct 2016 13:22:28 -0300 Subject: [PATCH 02/16] Ajusta front para busca por possiveis autores MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ajusta front e implementa SaplGenericRelation, uma customização que adiciona o atributo fields_search que possibilita passar para qualquer implementação de busca quais são os campos de busca padrão do do GenericRelation --- sapl/api/views.py | 26 ++++++++++--- sapl/base/forms.py | 33 +++++++--------- .../migrations/0024_auto_20161010_1002.py | 26 +++++++++++++ sapl/base/models.py | 6 +-- sapl/comissoes/models.py | 4 +- sapl/parlamentares/models.py | 6 ++- sapl/templates/base/autor_form.html | 39 +++++++++++++------ sapl/utils.py | 10 +++++ 8 files changed, 108 insertions(+), 42 deletions(-) create mode 100644 sapl/base/migrations/0024_auto_20161010_1002.py diff --git a/sapl/api/views.py b/sapl/api/views.py index 08181d8a0..a8058e5c6 100644 --- a/sapl/api/views.py +++ b/sapl/api/views.py @@ -1,3 +1,4 @@ +from django.db.models import Q from django.http import Http404 from rest_framework import mixins, viewsets from rest_framework.generics import ListAPIView, GenericAPIView,\ @@ -8,6 +9,7 @@ from rest_framework.viewsets import ModelViewSet from sapl.api.serializers import ChoiceSerializer from sapl.base.models import Autor, TipoAutor +from sapl.utils import SaplGenericRelation class TipoAutorContentOfModelContentTypeView(ListAPIView): @@ -27,10 +29,24 @@ class TipoAutorContentOfModelContentTypeView(ListAPIView): if not obj.content_type: raise Http404 - q = self.request.GET.get('q', None) + q = self.request.GET.get('q', '').strip() - if not q: - return [] + model_class = obj.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))) + + assert len(fields) == 1 + + fields_search = fields[0].fields_search + + if q: + q_filter = Q() + for fs in fields_search: + q_filter |= Q(**{'%s__icontains' % fs: q}) + + return model_class.objects.filter(q_filter)[:10] else: - return obj.content_type.model_class().objects.filter( - nome__icontains=q)[:10] + return model_class.objects.all()[:10] diff --git a/sapl/base/forms.py b/sapl/base/forms.py index 93ae87290..bfc85b7b6 100644 --- a/sapl/base/forms.py +++ b/sapl/base/forms.py @@ -85,21 +85,22 @@ class AutorForm(ModelForm): q = forms.CharField( max_length=50, required=False, - label='Pesquise o nome do Autor com o tipo Selecionado') - content_object = forms.ChoiceField(label='', - required=False, - widget=forms.RadioSelect()) + label='Pesquise o nome do Autor com o ' + 'tipo Selecionado e marque o escolhido.') + autor_related = forms.ChoiceField(label='', + required=False, + widget=forms.RadioSelect()) class Meta: model = Autor fields = ['tipo', 'nome', - 'content_object', + 'autor_related', 'q'] def __init__(self, *args, **kwargs): - content_object = Div( + autor_related = Div( FieldWithButtons( Field('q', @@ -108,20 +109,16 @@ class AutorForm(ModelForm): StrictButton( _('Filtrar'), css_class='btn-default btn-filtrar-autor', type='button')), - - - - - Field('content_object'), + Field('autor_related'), css_class='hidden', data_action='create', data_application='AutorSearch', - data_field='content_object') + data_field='autor_related') row1 = to_row([ ('tipo', 4), ('nome', 8), - (content_object, 8), + (autor_related, 8), ]) @@ -130,11 +127,11 @@ class AutorForm(ModelForm): super(AutorForm, self).__init__(*args, **kwargs) - self.fields['content_object'].choices = [('1', 'teste')] - if self.instance and self.instance.content_object: - self.fields['content_object'].choices = [ - (self.instance.content_object.pk, - self.instance.content_object)] + self.fields['autor_related'].choices = [] + if self.instance and self.instance.autor_related: + self.fields['autor_related'].choices = [ + (self.instance.autor_related.pk, + self.instance.autor_related)] def valida_igualdade(self, texto1, texto2, msg): if texto1 != texto2: 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/models.py b/sapl/base/models.py index 67896fef9..fecf7f3d2 100644 --- a/sapl/base/models.py +++ b/sapl/base/models.py @@ -160,7 +160,7 @@ class Autor(models.Model): blank=True, null=True, default=None) object_id = models.PositiveIntegerField( blank=True, null=True, default=None) - content_object = GenericForeignKey('content_type', 'object_id') + autor_related = GenericForeignKey('content_type', 'object_id') nome = models.CharField( max_length=50, blank=True, verbose_name=_('Nome do Autor')) @@ -174,8 +174,8 @@ class Autor(models.Model): def __str__(self): - if self.content_object: - return str(self.content_object) + if self.autor_related: + return str(self.autor_related) else: if str(self.cargo): return _('%(nome)s - %(cargo)s') % { diff --git a/sapl/comissoes/models.py b/sapl/comissoes/models.py index 9919857c1..c0c3ec4d3 100644 --- a/sapl/comissoes/models.py +++ b/sapl/comissoes/models.py @@ -6,7 +6,7 @@ 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): @@ -81,7 +81,7 @@ class Comissao(models.Model): choices=YES_NO_CHOICES, verbose_name=_('Comissão Ativa?')) - autor = GenericRelation(Autor) + autor = SaplGenericRelation(Autor, fields_search=('nome', 'sigla')) class Meta: verbose_name = _('Comissão') diff --git a/sapl/parlamentares/models.py b/sapl/parlamentares/models.py index 2629eac6c..ef7afeb7c 100644 --- a/sapl/parlamentares/models.py +++ b/sapl/parlamentares/models.py @@ -8,7 +8,7 @@ from model_utils import Choices from sapl.base.models import Autor from sapl.utils import (INDICADOR_AFASTAMENTO, UF, YES_NO_CHOICES, intervalos_tem_intersecao, - restringe_tipos_de_arquivo_img) + restringe_tipos_de_arquivo_img, SaplGenericRelation) class Legislatura(models.Model): @@ -264,7 +264,9 @@ class Parlamentar(models.Model): verbose_name=_('Fotografia'), validators=[restringe_tipos_de_arquivo_img]) - autor = GenericRelation(Autor) + # campo conceitual de reversão genérica para o model Autor + autor = SaplGenericRelation(Autor, fields_search=('nome_completo', + 'nome_parlamentar')) class Meta: verbose_name = _('Parlamentar') diff --git a/sapl/templates/base/autor_form.html b/sapl/templates/base/autor_form.html index dec8b20a3..710747cc8 100644 --- a/sapl/templates/base/autor_form.html +++ b/sapl/templates/base/autor_form.html @@ -7,10 +7,10 @@ $(document).ready(function(){ var active = function(str) { if (str == 'nome') { - $('#id_q').val(''); + $('#id_nome, #id_q').val(''); $('#div_id_nome').removeClass('hidden'); $("[data-application='AutorSearch']").addClass('hidden'); - $("#div_id_content_object .controls").html(''); + $("#div_id_autor_related .controls").html(''); } else { $('#id_nome').val(''); @@ -18,7 +18,7 @@ $(document).ready(function(){ $("[data-application='AutorSearch']").removeClass('hidden'); } } - var update_search = function(pk) { + var update_search = function(pk, atualizar=true) { var q = $('#id_q').val(); var url = '{% url 'sapl.api:autores_possiveis_pelo_tipo' 0 %}' url = url.replace('0', pk); @@ -28,15 +28,29 @@ $(document).ready(function(){ 'format' : 'json', } $.get(url, formData).done(function(data) { - var radios = $("#div_id_content_object .controls").html(''); - active('pesquisa'); - data.forEach(function (val, index) { - var html_radio = '
'; - radios.append(html_radio); + active('pesquisa'); + if (atualizar) { + var radios = $("#div_id_autor_related .controls").html(''); + data.forEach(function (val, index) { + var html_radio = '
'; + radios.append(html_radio); + }); + if (data.length > 1) { + $('input[name=autor_related]').change(function(event){ + $('#id_q').val(event.target.parentElement.textContent); + //$('input[name=autor_related]:not(:checked)').closest('.radio').remove(); + }); + } + else { + $('input[name=autor_related]').prop('checked', 'checked') + } + } + else{ + $('#id_nome, #id_q').val(''); + } + }).fail(function(data) { + active('nome'); }); - }).fail(function(data) { - active('nome'); - }); } $('#id_tipo').change(function(event) { @@ -46,7 +60,8 @@ $(document).ready(function(){ } else { var pk = this[event.target.selectedIndex].value; - update_search(pk); + $('input[name=autor_related]').closest('.radio').remove(); + update_search(pk, false) } }); $('.btn-filtrar-autor').click(function(event) { diff --git a/sapl/utils.py b/sapl/utils.py index d4e7bed3f..3b5cbd36f 100644 --- a/sapl/utils.py +++ b/sapl/utils.py @@ -10,6 +10,7 @@ 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.models import ContentType from django.core.exceptions import PermissionDenied, ValidationError from django.utils.translation import ugettext_lazy as _ @@ -55,6 +56,15 @@ autor_modal = ''' ''' +class SaplGenericRelation(GenericRelation): + + def __init__(self, to, fields_search=(), **kwargs): + + assert fields_search + self.fields_search = fields_search + super().__init__(to, **kwargs) + + class ImageThumbnailFileInput(ClearableFileInput): template_name = 'floppyforms/image_thumbnail.html' From 50a2f52af2e5f954a43cbc708aa06823ffa8af6d Mon Sep 17 00:00:00 2001 From: LeandroRoberto Date: Tue, 11 Oct 2016 16:36:43 -0300 Subject: [PATCH 03/16] =?UTF-8?q?Conc=20refatora=C3=A7=C3=A3o=20no=20Cada?= =?UTF-8?q?=20de=20Autor=20e=20Tipos=20de=20Autor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sapl/api/serializers.py | 16 - sapl/api/urls.py | 4 +- sapl/api/views.py | 10 +- sapl/base/forms.py | 305 ++++++++++++------ .../migrations/0025_tipoautor_cria_usuario.py | 20 ++ .../0026_remove_tipoautor_cria_usuario.py | 19 ++ .../migrations/0027_auto_20161011_1624.py | 22 ++ sapl/base/models.py | 7 +- sapl/base/urls.py | 15 +- sapl/base/views.py | 169 +++++----- sapl/comissoes/models.py | 1 - sapl/crispy_layout_mixin.py | 2 +- sapl/crud/base.py | 13 +- sapl/materia/forms.py | 9 +- sapl/materia/migrations/0056_merge.py | 16 + sapl/materia/models.py | 2 +- sapl/materia/urls.py | 10 +- sapl/materia/views.py | 30 +- sapl/parlamentares/models.py | 5 +- sapl/protocoloadm/forms.py | 1 + sapl/sessao/forms.py | 8 +- sapl/sessao/urls.py | 7 +- sapl/settings.py | 10 +- sapl/static/styles/app.scss | 37 ++- sapl/templates/base.html | 2 + sapl/templates/base/autor_form.html | 28 +- sapl/templates/base/layouts.yaml | 4 +- .../layout/checkboxselectmultiple.html | 14 + .../bootstrap3/layout/radioselect.html | 19 ++ sapl/test_urls.py | 6 +- sapl/urls.py | 2 +- sapl/utils.py | 41 ++- 32 files changed, 566 insertions(+), 288 deletions(-) create mode 100644 sapl/base/migrations/0025_tipoautor_cria_usuario.py create mode 100644 sapl/base/migrations/0026_remove_tipoautor_cria_usuario.py create mode 100644 sapl/base/migrations/0027_auto_20161011_1624.py create mode 100644 sapl/materia/migrations/0056_merge.py create mode 100644 sapl/templates/bootstrap3/layout/checkboxselectmultiple.html create mode 100644 sapl/templates/bootstrap3/layout/radioselect.html diff --git a/sapl/api/serializers.py b/sapl/api/serializers.py index d61643d21..0cbade069 100644 --- a/sapl/api/serializers.py +++ b/sapl/api/serializers.py @@ -1,8 +1,5 @@ from rest_framework import serializers -from sapl.comissoes.models import Comissao -from sapl.parlamentares.models import Parlamentar - class ChoiceSerializer(serializers.Serializer): pk = serializers.IntegerField() @@ -10,16 +7,3 @@ class ChoiceSerializer(serializers.Serializer): def get_display(self, obj): return str(obj) -""" - -class ModelChoiceParlamentarSerializer(ModelChoiceSerializer): - - class Meta: - model = Parlamentar - - -class ModelChoiceComissaoSerializer(ModelChoiceSerializer): - - class Meta: - model = Comissao -""" \ No newline at end of file diff --git a/sapl/api/urls.py b/sapl/api/urls.py index a3a130da9..2d6753308 100644 --- a/sapl/api/urls.py +++ b/sapl/api/urls.py @@ -1,11 +1,9 @@ -from django.conf.urls import include, url -from rest_framework.routers import DefaultRouter +from django.conf.urls import url from sapl.api.views import TipoAutorContentOfModelContentTypeView from .apps import AppConfig - app_name = AppConfig.name diff --git a/sapl/api/views.py b/sapl/api/views.py index a8058e5c6..def65a217 100644 --- a/sapl/api/views.py +++ b/sapl/api/views.py @@ -1,10 +1,7 @@ from django.db.models import Q from django.http import Http404 -from rest_framework import mixins, viewsets -from rest_framework.generics import ListAPIView, GenericAPIView,\ - get_object_or_404 -from rest_framework.permissions import IsAuthenticated, AllowAny -from rest_framework.views import APIView +from rest_framework.generics import ListAPIView, get_object_or_404 +from rest_framework.permissions import IsAuthenticated from rest_framework.viewsets import ModelViewSet from sapl.api.serializers import ChoiceSerializer @@ -14,7 +11,8 @@ from sapl.utils import SaplGenericRelation class TipoAutorContentOfModelContentTypeView(ListAPIView): serializer_class = ChoiceSerializer - permission_classes = (AllowAny,) + # FIXME aplicar permissão correta de usuário + permission_classes = (IsAuthenticated,) queryset = TipoAutor.objects.all() model = TipoAutor pagination_class = None diff --git a/sapl/base/forms.py b/sapl/base/forms.py index bfc85b7b6..baaa3059d 100644 --- a/sapl/base/forms.py +++ b/sapl/base/forms.py @@ -1,23 +1,23 @@ -from crispy_forms.bootstrap import FieldWithButtons, StrictButton +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, Field, Div, Row -from crispy_forms.templatetags.crispy_forms_field import css_class +from crispy_forms.layout import HTML, Button, Div, Field, Fieldset, Layout, Row 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, ObjectDoesNotExist +from django.core.exceptions import ValidationError from django.db import models, transaction -from django.forms import ModelForm, widgets +from django.forms import ModelForm from django.utils.translation import ugettext_lazy as _ -import django_filters from sapl.base.models import Autor, TipoAutor -from sapl.crispy_layout_mixin import form_actions, to_row, SaplFormLayout,\ - to_column +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 @@ -26,6 +26,12 @@ from sapl.utils import (RANGE_ANOS, ImageThumbnailFileInput, 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')), +] + class TipoAutorForm(ModelForm): @@ -37,7 +43,7 @@ class TipoAutorForm(ModelForm): class Meta: model = TipoAutor fields = ['descricao', - 'content_type', ] + 'content_type'] def __init__(self, *args, **kwargs): @@ -61,47 +67,65 @@ class TipoAutorForm(ModelForm): (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( + senha = forms.CharField( max_length=20, label=_('Senha'), - required=True, + required=False, widget=forms.PasswordInput()) senha_confirma = forms.CharField( max_length=20, label=_('Confirmar Senha'), - required=True, + required=False, widget=forms.PasswordInput()) + email = forms.EmailField( + required=False, + label=_('Email')) + confirma_email = forms.EmailField( - required=True, + required=False, label=_('Confirmar Email')) username = forms.CharField( - required=True, - max_length=50 - )""" + 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 = forms.ChoiceField(label='', - required=False, - widget=forms.RadioSelect()) + + autor_related = ChoiceWithoutValidationField(label='', + required=False, + widget=forms.RadioSelect()) + + action_user = forms.ChoiceField( + label=_('Usuário de acesso ao Sistema para este Autor'), + choices=ACTION_CREATE_USERS_AUTOR_CHOICE, + widget=forms.RadioSelect()) class Meta: model = Autor fields = ['tipo', 'nome', 'autor_related', - 'q'] + 'q', + 'action_user', + 'username'] def __init__(self, *args, **kwargs): autor_related = Div( - FieldWithButtons( Field('q', placeholder=_('Pesquisar por possíveis autores para ' @@ -109,110 +133,195 @@ class AutorForm(ModelForm): StrictButton( _('Filtrar'), css_class='btn-default btn-filtrar-autor', type='button')), + + Field('autor_related'), css_class='hidden', data_action='create', data_application='AutorSearch', data_field='autor_related') - row1 = to_row([ - ('tipo', 4), - ('nome', 8), - (autor_related, 8), - - ]) + autor_select = Row(to_column(('tipo', 4)), + to_column(('nome', 8)), + to_column((autor_related, 8))) + + row2 = Row(to_column((InlineRadios('action_user'), 8)), + to_column(('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') + + controle_acesso = Fieldset( + _('Controle de Acesso do Autor'), + row2, row3 + ) self.helper = FormHelper() - self.helper.layout = SaplFormLayout(row1) + self.helper.layout = SaplFormLayout(autor_select, controle_acesso) super(AutorForm, self).__init__(*args, **kwargs) - self.fields['autor_related'].choices = [] - if self.instance and self.instance.autor_related: - self.fields['autor_related'].choices = [ - (self.instance.autor_related.pk, - self.instance.autor_related)] + self.fields['action_user'].initial = 'N' + self.fields['action_user'].inline_class = True + + 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' 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 clea(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: - get_user_model().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) + def clean(self): + User = get_user_model() + cd = self.cleaned_data + + if 'username' not in cd: + raise ValidationError(_('O username deve ser informado.')) + + if 'action_user' not in cd: + raise ValidationError(_('Informe se o Autor terá usuário ' + 'para acesso ao Sistema.')) + + 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 usá-lo 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 usá-lo você deve selecionar ' + '"Criar novo Usuário".') % cd['username']) + + if cd['action_user'] != 'N': + 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 sav(self, commit=False): - + def save(self, commit=False): + print('aqui') autor = super(AutorForm, self).save(commit) - u = get_user_model().objects.get( - username=autor.username, - email=autor.email) + user_old = autor.user if autor.user_id else None - u.set_password(self.cleaned_data['senha']) - u.is_active = False - u.save() - - autor.user = u + if self.cleaned_data['action_user'] == 'A': + u = get_user_model().objects.get( + username=self.cleaned_data['username']) + autor.user = u + 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 + 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() - grupo = Group.objects.filter(name='Autor')[0] - u.groups.add(grupo) + if self.cleaned_data['action_user'] != 'N': + # FIXME melhorar captura de grupo de Autor, levando em conta + # tradução + grupo = Group.objects.filter(name='Autor')[0] + autor.user.groups.add(grupo) + + if user_old and user_old != autor.user: + user_old.groups.remove(grupo) return 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 fecf7f3d2..b1db9e0a6 100644 --- a/sapl/base/models.py +++ b/sapl/base/models.py @@ -7,12 +7,11 @@ from django.contrib.contenttypes.models import ContentType from django.core import exceptions from django.db import models, router from django.db.utils import DEFAULT_DB_ALIAS -from django.utils.translation import string_concat from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import string_concat from sapl.utils import UF, YES_NO_CHOICES, get_settings_auth_user_model - TIPO_DOCUMENTO_ADMINISTRATIVO = (('O', _('Ostensivo')), ('R', _('Restritivo'))) @@ -151,7 +150,9 @@ class TipoAutor(models.Model): class Autor(models.Model): - user = models.OneToOneField(get_settings_auth_user_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')) diff --git a/sapl/base/urls.py b/sapl/base/urls.py index 1e822dcb0..e06580cfa 100644 --- a/sapl/base/urls.py +++ b/sapl/base/urls.py @@ -14,21 +14,18 @@ from .views import (AppConfigCrud, CasaLegislativaCrud, HelpView, RelatorioMateriasTramitacaoView, RelatorioPresencaSessaoView) - app_name = AppConfig.name urlpatterns = [ -<<<<<<< 6123d2617726dd220c02c2bb3b3c27ed4b136df1 -======= url(r'^sistema/autor/tipo/', include(TipoAutorCrud.get_urls())), url(r'^sistema/autor/', include(AutorCrud.get_urls())), ->>>>>>> Ref Autor, TipoAutor, cria app api DRF 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/', + TemplateView.as_view(template_name='ajuda/index.html'), name='help_base'), url(r'^sistema/casa-legislativa/', include(CasaLegislativaCrud.get_urls()), name="casa_legislativa"), @@ -55,21 +52,13 @@ urlpatterns = [ RelatorioAtasView.as_view(), name='atas'), -<<<<<<< 6123d2617726dd220c02c2bb3b3c27ed4b136df1 -======= - # todos os sublink s de sistema devem vir acima deste ->>>>>>> Ref Autor, TipoAutor, cria app api DRF url(r'^sistema/', permission_required('base.view_tabelas_auxiliares') (TemplateView.as_view(template_name='sistema.html'))), - -<<<<<<< 6123d2617726dd220c02c2bb3b3c27ed4b136df1 -======= 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'), ->>>>>>> Ref Autor, TipoAutor, cria app api DRF ] diff --git a/sapl/base/views.py b/sapl/base/views.py index ac3e3ef56..650c2d055 100644 --- a/sapl/base/views.py +++ b/sapl/base/views.py @@ -1,9 +1,7 @@ -from crispy_forms.helper import FormHelper -from crispy_forms.layout import HTML, Button from django.conf import settings -from django.contrib.auth import get_user_model 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 @@ -16,13 +14,11 @@ 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 TipoAutor, Autor -from sapl.crispy_layout_mixin import to_row, SaplFormLayout, form_actions +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 from sapl.sessao.models import OrdemDia, SessaoPlenaria -from sapl.utils import autor_label, autor_modal from .forms import (CasaLegislativaForm, ConfiguracoesAppForm, RelatorioAtasFilterSet, @@ -38,41 +34,6 @@ def get_casalegislativa(): return CasaLegislativa.objects.first() -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_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 TipoAutorCrud(CrudAux): model = TipoAutor help_path = 'tipo-autor' @@ -89,61 +50,97 @@ class AutorCrud(CrudAux): 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 __init__(self, *args, **kwargs): - # montar_helper_autor(self) - super(CrudAux.UpdateView, self).__init__(*args, **kwargs) - - def get_context_data(self, **kwargs): - context = super( - CrudAux.UpdateView, self).get_context_data(**kwargs) - #context['helper'] = self.helper - return context + 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 + 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 __init__(self, *args, **kwargs): - montar_helper_autor(self) - super(CrudAux.CreateView, self).__init__(*args, **kwargs)""" - - """def get_context_data(self, **kwargs): - context = super( - CrudAux.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) + 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})""" + kwargs={'pk': pk_autor}) class RelatorioAtasView(FilterView): diff --git a/sapl/comissoes/models.py b/sapl/comissoes/models.py index c0c3ec4d3..47321fde6 100644 --- a/sapl/comissoes/models.py +++ b/sapl/comissoes/models.py @@ -1,5 +1,4 @@ -from django.contrib.contenttypes.fields import GenericRelation from django.db import models from django.utils.translation import ugettext_lazy as _ from model_utils import Choices diff --git a/sapl/crispy_layout_mixin.py b/sapl/crispy_layout_mixin.py index 33e6d2608..7f7aca408 100644 --- a/sapl/crispy_layout_mixin.py +++ b/sapl/crispy_layout_mixin.py @@ -1,12 +1,12 @@ from math import ceil +import rtyaml from crispy_forms.bootstrap import FormActions from crispy_forms.helper import FormHelper from crispy_forms.layout import HTML, Div, Fieldset, Layout, Submit from django import template from django.utils import formats from django.utils.translation import ugettext as _ -import rtyaml def heads_and_tails(list_of_lists): diff --git a/sapl/crud/base.py b/sapl/crud/base.py index 1077c55a3..f479d3ea1 100644 --- a/sapl/crud/base.py +++ b/sapl/crud/base.py @@ -13,8 +13,8 @@ from django.db import models from django.http.response import Http404 from django.utils.decorators import classonlymethod from django.utils.encoding import force_text -from django.utils.translation import string_concat from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import string_concat from django.views.generic import (CreateView, DeleteView, DetailView, ListView, UpdateView) from django.views.generic.base import ContextMixin @@ -23,7 +23,6 @@ from django.views.generic.list import MultipleObjectMixin from sapl.crispy_layout_mixin import CrispyLayoutFormMixin, get_field_display from sapl.utils import normalize - logger = logging.getLogger(__name__) ACTION_LIST, ACTION_CREATE, ACTION_DETAIL, ACTION_UPDATE, ACTION_DELETE = \ @@ -882,9 +881,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 +894,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 + \ diff --git a/sapl/materia/forms.py b/sapl/materia/forms.py index 5e434007d..30663200e 100644 --- a/sapl/materia/forms.py +++ b/sapl/materia/forms.py @@ -1,17 +1,14 @@ 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.comissoes.models import Comissao from sapl.crispy_layout_mixin import form_actions, to_row @@ -508,7 +505,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', 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 5a14e7ced..e326b7698 100644 --- a/sapl/materia/models.py +++ b/sapl/materia/models.py @@ -5,7 +5,7 @@ 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) diff --git a/sapl/materia/urls.py b/sapl/materia/urls.py index dc18e47ef..a6f85016a 100644 --- a/sapl/materia/urls.py +++ b/sapl/materia/urls.py @@ -15,11 +15,11 @@ from sapl.materia.views import (AcompanhamentoConfirmarView, ProposicaoRecebida, ProposicaoTaView, ReceberProposicao, ReciboProposicaoView, RegimeTramitacaoCrud, RelatoriaCrud, - StatusTramitacaoCrud, - TipoDocumentoCrud, TipoFimRelatoriaCrud, - TipoMateriaCrud, TipoProposicaoCrud, - TramitacaoCrud, TramitacaoEmLoteView, - UnidadeTramitacaoCrud, recuperar_materia) + StatusTramitacaoCrud, TipoDocumentoCrud, + TipoFimRelatoriaCrud, TipoMateriaCrud, + TipoProposicaoCrud, TramitacaoCrud, + TramitacaoEmLoteView, UnidadeTramitacaoCrud, + recuperar_materia) from .apps import AppConfig diff --git a/sapl/materia/views.py b/sapl/materia/views.py index 71b166c12..329b964be 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,17 +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, Autor, TipoAutor -from sapl.base.views import montar_row_autor +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,8 +33,13 @@ from sapl.materia.forms import AnexadaForm, LegislacaoCitadaForm 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, +<<<<<<< 3276eb12726b741df770d5a6ed2a9a1a83c15849 permissoes_autor, permissoes_materia, permissoes_protocoloadm, permission_required_for_app) +======= + montar_row_autor, permissoes_autor, permissoes_materia, + permissoes_protocoloadm) +>>>>>>> Conc refatoração no Cada de Autor e Tipos de Autor from .forms import (AcessorioEmLoteFilterSet, AcompanhamentoMateriaForm, ConfirmarProposicaoForm, DocumentoAcessorioForm, @@ -48,13 +49,12 @@ from .forms import (AcessorioEmLoteFilterSet, AcompanhamentoMateriaForm, filtra_tramitacao_destino, filtra_tramitacao_destino_and_status, filtra_tramitacao_status) -from .models import (AcompanhamentoMateria, Anexada, Autoria, - DespachoInicial, DocumentoAcessorio, MateriaLegislativa, - Numeracao, Orgao, Origem, Proposicao, RegimeTramitacao, - Relatoria, StatusTramitacao, 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, '') diff --git a/sapl/parlamentares/models.py b/sapl/parlamentares/models.py index ef7afeb7c..9a6954d14 100644 --- a/sapl/parlamentares/models.py +++ b/sapl/parlamentares/models.py @@ -1,14 +1,13 @@ from datetime import datetime -from django.contrib.contenttypes.fields import GenericRelation 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, - restringe_tipos_de_arquivo_img, SaplGenericRelation) + SaplGenericRelation, intervalos_tem_intersecao, + restringe_tipos_de_arquivo_img) class Legislatura(models.Model): diff --git a/sapl/protocoloadm/forms.py b/sapl/protocoloadm/forms.py index 20e77b782..8e9ddd595 100644 --- a/sapl/protocoloadm/forms.py +++ b/sapl/protocoloadm/forms.py @@ -421,6 +421,7 @@ class ProtocoloMateriaForm(ModelForm): super(ProtocoloMateriaForm, self).__init__( *args, **kwargs) + self.fields['tipo_protocolo'].inline_class = True class DocumentoAcessorioAdministrativoForm(ModelForm): diff --git a/sapl/sessao/forms.py b/sapl/sessao/forms.py index d629c608c..f213e1b94 100644 --- a/sapl/sessao/forms.py +++ b/sapl/sessao/forms.py @@ -1,12 +1,12 @@ from datetime import datetime +import django_filters from crispy_forms.helper import FormHelper from crispy_forms.layout import HTML, Button, Fieldset, Layout from django import forms from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.forms import ModelForm from django.utils.translation import ugettext_lazy as _ -import django_filters from sapl.crispy_layout_mixin import form_actions, to_row from sapl.materia.forms import MateriaLegislativaFilterSet @@ -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,7 @@ 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), diff --git a/sapl/sessao/urls.py b/sapl/sessao/urls.py index 672a95f0c..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, diff --git a/sapl/settings.py b/sapl/settings.py index ed27a931f..909703a97 100644 --- a/sapl/settings.py +++ b/sapl/settings.py @@ -26,7 +26,6 @@ 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 +34,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,7 +50,7 @@ SAPL_APPS = ( 'sapl.painel', 'sapl.protocoloadm', 'sapl.compilacao', - 'sapl.api' + 'sapl.api' ) INSTALLED_APPS = ( @@ -72,8 +74,8 @@ INSTALLED_APPS = ( ) + SAPL_APPS -if DEBUG: - INSTALLED_APPS += ('debug_toolbar',) +# if DEBUG: +# INSTALLED_APPS += ('debug_toolbar',) MIDDLEWARE_CLASSES = ( 'django.contrib.sessions.middleware.SessionMiddleware', diff --git a/sapl/static/styles/app.scss b/sapl/static/styles/app.scss index 535bf912e..0b9bbae82 100644 --- a/sapl/static/styles/app.scss +++ b/sapl/static/styles/app.scss @@ -136,10 +136,45 @@ body { // #### conserta radios e checkboxes escondidas pelo drunken parrot ############ // FIXME ajustar após solução definitiva -.checkbox input, .radio input { +/*.checkbox input, .radio input { display: initial; +}*/ +.radio, .checkbox, .radio-inline, .checkbox-inline,{ + margin-top: 0px; + margin-bottom: $grid-gutter-width / 2; + padding-left: $grid-gutter-width ; +} + +.controls-radio, .controls-checkbox { + padding:0; + background-color: white; + border: 1px solid #d6e1e5; + border-radius: 4px; + min-height: $grid-gutter-width; + overflow: hidden; + label.checkbox, label.radio{ + margin: 0; + padding: 0; + padding-right: $grid-gutter-width; + padding-left: $grid-gutter-width * 1.5; + line-height: $grid-gutter-width * 1.3; + min-height: $grid-gutter-width * 1.3; + .icons { + top: ceil($grid-gutter-width / 3); + left: $grid-gutter-width / 2.2; + } + &:hover { + background-color: #d6e1e5; + } + } + .help-block { + margin: $grid-gutter-width / 2; + padding: $grid-gutter-width / 2; + border: 2px dashed darken(#d6e1e5, 5%); + } } + .modal { .alert { margin-bottom: 0; diff --git a/sapl/templates/base.html b/sapl/templates/base.html index f5fa836f4..a4b3db6e6 100644 --- a/sapl/templates/base.html +++ b/sapl/templates/base.html @@ -284,6 +284,8 @@ + + diff --git a/sapl/templates/base/autor_form.html b/sapl/templates/base/autor_form.html index 710747cc8..823d64f03 100644 --- a/sapl/templates/base/autor_form.html +++ b/sapl/templates/base/autor_form.html @@ -32,17 +32,20 @@ $(document).ready(function(){ if (atualizar) { var radios = $("#div_id_autor_related .controls").html(''); data.forEach(function (val, index) { - var html_radio = '
'; + var html_radio = ''; radios.append(html_radio); }); + if (data.length > 1) { $('input[name=autor_related]').change(function(event){ - $('#id_q').val(event.target.parentElement.textContent); + if (this.checked) + $('#id_q').val(event.target.parentElement.textContent); //$('input[name=autor_related]:not(:checked)').closest('.radio').remove(); }); } else { - $('input[name=autor_related]').prop('checked', 'checked') + $('input[name=autor_related]').prop('checked', 'checked'); + $('input[name=autor_related]').closest('.radio').addClass('checked'); } } else{ @@ -52,7 +55,6 @@ $(document).ready(function(){ active('nome'); }); } - $('#id_tipo').change(function(event) { if (event.target.selectedIndex == 0) { $('#id_nome, #id_q').val(''); @@ -68,6 +70,24 @@ $(document).ready(function(){ var pk = $('#id_tipo').val(); update_search(pk); }); + $('input[name=action_user]').change(function(event) { + if (event.target.value == 'C') + $('.new_user_fields, #div_id_username').removeClass('hidden'); + else if (event.target.value == 'N') + $('.new_user_fields, #div_id_username').addClass('hidden'); + else { + $('#div_id_username').removeClass('hidden'); + $('.new_user_fields').addClass('hidden'); + } + }); + + $('input[name=action_user]:checked').trigger('change'); + $('input[name=autor_related]').closest('.radio').remove(); + + var pk = $('#id_tipo').val(); + if (pk) + update_search(pk, $('#id_q').val().length > 0) + }); diff --git a/sapl/templates/base/layouts.yaml b/sapl/templates/base/layouts.yaml index c63135ee6..85be02341 100644 --- a/sapl/templates/base/layouts.yaml +++ b/sapl/templates/base/layouts.yaml @@ -18,12 +18,12 @@ AppConfig: TipoAutor: {% trans 'Tipo Autor' %}: - - content_type descricao:7 + - content_type:4 descricao Autor: {% trans 'Autor' %}: - tipo:3 nome - - username:6 cargo + - user:6 cargo AutorCreate: {% trans 'Cadastro de Usuários Autores' %}: diff --git a/sapl/templates/bootstrap3/layout/checkboxselectmultiple.html b/sapl/templates/bootstrap3/layout/checkboxselectmultiple.html new file mode 100644 index 000000000..a7398ab11 --- /dev/null +++ b/sapl/templates/bootstrap3/layout/checkboxselectmultiple.html @@ -0,0 +1,14 @@ +{% load crispy_forms_filters %} +{% load l10n %} + +
+ {% include 'bootstrap3/layout/field_errors_block.html' %} + {% for choice in field.field.choices %} + + {% endfor %} + {% include 'bootstrap3/layout/help_text.html' %} +
diff --git a/sapl/templates/bootstrap3/layout/radioselect.html b/sapl/templates/bootstrap3/layout/radioselect.html new file mode 100644 index 000000000..b3e6b589e --- /dev/null +++ b/sapl/templates/bootstrap3/layout/radioselect.html @@ -0,0 +1,19 @@ +{% load crispy_forms_filters %} +{% load l10n %} + +
+ {% include 'bootstrap3/layout/field_errors_block.html' %} + + {% for choice in field.field.choices %} + + {% endfor %} + + {% include 'bootstrap3/layout/help_text.html' %} +
diff --git a/sapl/test_urls.py b/sapl/test_urls.py index 4d6f35e6c..4f6f0587b 100644 --- a/sapl/test_urls.py +++ b/sapl/test_urls.py @@ -1,3 +1,4 @@ +import pytest from django.apps import apps from django.contrib.auth import get_user_model from django.contrib.auth.management import _get_all_permissions @@ -6,7 +7,7 @@ from django.contrib.contenttypes.models import ContentType from django.db import transaction from django.utils.translation import string_concat from django.utils.translation import ugettext_lazy as _ -import pytest +from django.utils.translation import string_concat from sapl.crud.base import PermissionRequiredForAppCrudMixin from sapl.materia.views import recuperar_materia @@ -15,7 +16,6 @@ from scripts.lista_urls import lista_urls from .settings import SAPL_APPS - pytestmark = pytest.mark.django_db sapl_appconfs = [apps.get_app_config(n[5:]) for n in SAPL_APPS] @@ -335,7 +335,7 @@ urls_publicas_excecoes = { """ # gerar uma instancia de teste para cada usuário não foi possível. São 500 -urls para cada operador. Isso fez com que o Travis estourasse o tempo de +urls para cada operador. Isso fez com que o Travis estourasse o tempo de processamento do teste... passando de 2hs... até outro modo, a estratégia de encapsular apenas as urls e testar em loop os operadores será mantida. diff --git a/sapl/urls.py b/sapl/urls.py index 84f92363d..b987fc208 100644 --- a/sapl/urls.py +++ b/sapl/urls.py @@ -20,6 +20,7 @@ from django.contrib import admin from django.views.generic.base import TemplateView from django.views.static import serve as view_static_server +import sapl.api.urls import sapl.base.urls import sapl.comissoes.urls import sapl.compilacao.urls @@ -31,7 +32,6 @@ import sapl.parlamentares.urls import sapl.protocoloadm.urls import sapl.relatorios.urls import sapl.sessao.urls -import sapl.api.urls urlpatterns = [ url(r'^$', TemplateView.as_view(template_name='index.html')), diff --git a/sapl/utils.py b/sapl/utils.py index 3b5cbd36f..b4c8b24fa 100644 --- a/sapl/utils.py +++ b/sapl/utils.py @@ -4,6 +4,9 @@ from unicodedata import normalize as unicodedata_normalize import hashlib import logging +import magic +from crispy_forms.helper import FormHelper +from crispy_forms.layout import HTML, Button from django import forms from django.apps import apps from django.conf import settings @@ -15,12 +18,13 @@ from django.contrib.contenttypes.models import ContentType from django.core.exceptions import PermissionDenied, ValidationError from django.utils.translation import ugettext_lazy as _ from floppyforms import ClearableFileInput -import magic from sapl.settings import BASE_DIR sapl_logger = logging.getLogger(BASE_DIR.name) +from sapl.crispy_layout_mixin import SaplFormLayout, form_actions, to_row + def normalize(txt): return unicodedata_normalize( @@ -56,6 +60,41 @@ autor_modal = ''' ''' +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_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 SaplGenericRelation(GenericRelation): def __init__(self, to, fields_search=(), **kwargs): From 7f2f7a1c95bb612e67b56eeae74d4693cba9a987 Mon Sep 17 00:00:00 2001 From: LeandroRoberto Date: Wed, 12 Oct 2016 14:01:17 -0300 Subject: [PATCH 04/16] Alt backend de perm e pag de drf e ref layout topo --- .../{rest_pagination.py => api/pagination.py} | 1 + sapl/api/permissions.py | 17 ++++ sapl/api/urls.py | 2 +- sapl/api/views.py | 10 +-- sapl/base/forms.py | 10 ++- sapl/settings.py | 6 +- sapl/static/styles/app.scss | 88 ++++++++++++++++--- sapl/templates/base.html | 6 +- sapl/templates/base/autor_form.html | 14 ++- 9 files changed, 124 insertions(+), 30 deletions(-) rename sapl/{rest_pagination.py => api/pagination.py} (96%) create mode 100644 sapl/api/permissions.py diff --git a/sapl/rest_pagination.py b/sapl/api/pagination.py similarity index 96% rename from sapl/rest_pagination.py rename to sapl/api/pagination.py index bb7096a55..75941c1d3 100644 --- a/sapl/rest_pagination.py +++ b/sapl/api/pagination.py @@ -1,4 +1,5 @@ from django.core.paginator import EmptyPage +from django.utils.encoding import force_text from rest_framework import pagination from rest_framework.response import Response 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/urls.py b/sapl/api/urls.py index 2d6753308..3c5b6a265 100644 --- a/sapl/api/urls.py +++ b/sapl/api/urls.py @@ -10,7 +10,7 @@ app_name = AppConfig.name # router = DefaultRouter() urlpatterns = [ - url(r'^autor/possiveis/(?P[0-9]*)$', + url(r'^autor/possiveis-pelo-tipo/(?P[0-9]+)$', TipoAutorContentOfModelContentTypeView.as_view(), name='autores_possiveis_pelo_tipo'), ] diff --git a/sapl/api/views.py b/sapl/api/views.py index def65a217..3e839127c 100644 --- a/sapl/api/views.py +++ b/sapl/api/views.py @@ -15,17 +15,17 @@ class TipoAutorContentOfModelContentTypeView(ListAPIView): permission_classes = (IsAuthenticated,) queryset = TipoAutor.objects.all() model = TipoAutor - pagination_class = None def get_queryset(self): queryset = ModelViewSet.get_queryset(self) + if not self.kwargs['pk']: - return queryset + raise Http404() obj = get_object_or_404(queryset, pk=self.kwargs['pk']) if not obj.content_type: - raise Http404 + raise Http404() q = self.request.GET.get('q', '').strip() @@ -45,6 +45,6 @@ class TipoAutorContentOfModelContentTypeView(ListAPIView): for fs in fields_search: q_filter |= Q(**{'%s__icontains' % fs: q}) - return model_class.objects.filter(q_filter)[:10] + return model_class.objects.filter(q_filter) else: - return model_class.objects.all()[:10] + return model_class.objects.all() diff --git a/sapl/base/forms.py b/sapl/base/forms.py index baaa3059d..33b826131 100644 --- a/sapl/base/forms.py +++ b/sapl/base/forms.py @@ -1,4 +1,3 @@ -import django_filters from crispy_forms.bootstrap import FieldWithButtons, InlineRadios, StrictButton from crispy_forms.helper import FormHelper from crispy_forms.layout import HTML, Button, Div, Field, Fieldset, Layout, Row @@ -14,6 +13,7 @@ from django.core.exceptions import ValidationError from django.db import models, transaction from django.forms import ModelForm from django.utils.translation import ugettext_lazy as _ +import django_filters from sapl.base.models import Autor, TipoAutor from sapl.crispy_layout_mixin import (SaplFormLayout, form_actions, to_column, @@ -26,6 +26,7 @@ from sapl.utils import (RANGE_ANOS, ImageThumbnailFileInput, from .models import AppConfig, CasaLegislativa + ACTION_CREATE_USERS_AUTOR_CHOICE = [ ('C', _('Criar novo Usuário')), ('A', _('Associar um usuário existente')), @@ -135,7 +136,6 @@ class AutorForm(ModelForm): type='button')), - Field('autor_related'), css_class='hidden', data_action='create', data_application='AutorSearch', @@ -143,7 +143,11 @@ class AutorForm(ModelForm): autor_select = Row(to_column(('tipo', 4)), to_column(('nome', 8)), - to_column((autor_related, 8))) + to_column((autor_related, 8)), + to_column((Div( + Field('autor_related'), + css_class='radiogroup-autor-related hidden'), + 12))) row2 = Row(to_column((InlineRadios('action_user'), 8)), to_column(('username', 4))) diff --git a/sapl/settings.py b/sapl/settings.py index 909703a97..c42295f86 100644 --- a/sapl/settings.py +++ b/sapl/settings.py @@ -101,10 +101,14 @@ REST_FRAMEWORK = { "DEFAULT_PERMISSION_CLASSES": ( "rest_framework.permissions.IsAuthenticated", ), + "DEFAULT_PERMISSION_CLASSES": ( + # "rest_framework.permissions.IsAuthenticated", + "sapl.api.permissions.DjangoModelPermissions", + ), "DEFAULT_AUTHENTICATION_CLASSES": ( "rest_framework.authentication.SessionAuthentication", ), - "DEFAULT_PAGINATION_CLASS": "sapl.rest_pagination.StandardPagination", + "DEFAULT_PAGINATION_CLASS": "sapl.api.pagination.StandardPagination", "DEFAULT_FILTER_BACKENDS": ( "rest_framework.filters.SearchFilter", "rest_framework.filters.DjangoFilterBackend", diff --git a/sapl/static/styles/app.scss b/sapl/static/styles/app.scss index 0b9bbae82..eff69cd3d 100644 --- a/sapl/static/styles/app.scss +++ b/sapl/static/styles/app.scss @@ -6,16 +6,46 @@ display: inline-block; vertical-align: middle; float: none; + padding: 10px; } nav { &.navbar { - padding: 5px; border-radius: 0; font-size: 15px; } -} + .navbar-nav { + & > li { + & > a { + padding-top: 0px; + padding-bottom: 0px; + line-height: $grid-gutter-width * 2.5; + &:hover { + background-color: $link-hover-color; + } + } + &:first-child { + & > a { + padding-left: 0px; + padding-right: 0px; + } + } + &:nth-child(2) { + & > .dropdown-menu { + right: auto; + } + } + } + &:last-child { + & > li:last-child { + a { + padding-right: 0px; + } + } + } + } +} .masthead { padding: 10px; .nav { @@ -24,7 +54,9 @@ nav { .navbar-brand { color: $headings-color; font-size: 24px; - img { + img.img-responsive { + height: 95px; + width: 95px; margin-right: $navbar-padding-horizontal; } small { @@ -139,12 +171,6 @@ body { /*.checkbox input, .radio input { display: initial; }*/ -.radio, .checkbox, .radio-inline, .checkbox-inline,{ - margin-top: 0px; - margin-bottom: $grid-gutter-width / 2; - padding-left: $grid-gutter-width ; -} - .controls-radio, .controls-checkbox { padding:0; background-color: white; @@ -155,13 +181,13 @@ body { label.checkbox, label.radio{ margin: 0; padding: 0; - padding-right: $grid-gutter-width; - padding-left: $grid-gutter-width * 1.5; + padding-right: $grid-gutter-width / 2.5; + padding-left: $grid-gutter-width * 1.3; line-height: $grid-gutter-width * 1.3; min-height: $grid-gutter-width * 1.3; .icons { - top: ceil($grid-gutter-width / 3); - left: $grid-gutter-width / 2.2; + top: ceil($grid-gutter-width / 4); + left: $grid-gutter-width / 2.5; } &:hover { background-color: #d6e1e5; @@ -417,3 +443,39 @@ p { font-size: 100%; } /* FIM TEMPLATE AJUDA */ + +@media (max-width: 1199px) { + /*.nav > li > a { + padding: $grid-gutter-width $grid-gutter-width / 3; + }*/ + + .masthead { + .vcenter { + padding: 10px; + } + .nav { + margin-top: 65px; + } + .navbar-brand { + color: $headings-color; + font-size: 20px; + img.img-responsive { + height: 60px; + width: 60px; + margin-right: $navbar-padding-horizontal / 2; + } + small { + color: #93A4AA; + font-size: 75%; + line-height: 25px; + } + } + } + +} + +@media (min-width: 1092px) and (max-width: 1199px) { + .container { + width: 1070px; + } +} diff --git a/sapl/templates/base.html b/sapl/templates/base.html index a4b3db6e6..9d732de1a 100644 --- a/sapl/templates/base.html +++ b/sapl/templates/base.html @@ -42,7 +42,7 @@